Microsoft Systems Journal Volume 4

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

Volume 4 - Number 1

──────────────────────────────────────────────────────────────────────────── Quotron Uses Windows to Develop New Market Analysis Tools for Real-Time Data ─────────────────────────────────────────────────────────────────────────── Also see the related article: Utilizing the Object Oriented-Nature of Windows ─────────────────────────────────────────────────────────────────────────── Tony Rizzo and Karen Strauss Traders know that making or losing a million dollars often depends on only one thing──information. This goes a long way in explaining that "Wall Street data obsession"──the drive to be the first to get the latest market information. In 1851, Reuters delivered market information by carrier pigeon. On Wall Street traders rushed stock quotes written on pads of paper to brokerages via messengers called pad-shovers. In 1867 the ticker tape machine was patented and the market underwent a revolution. Quotes became available on a tick-by-tick basis; when a stock moved, a mark appeared on ticker tape machines in every brokerage house on the Street. The ticker tape evolved into the electronic wall ticker, which flashes a continuous stream of quotes. Electronic data collection arrived on traders' desks in 1960 in the guise of the Quotron(R) I. QUOTEVUE(R) followed in 1967, the first machine to provide quotes via a screen display. Before long a brokerage's stature came to be measured by the presence or absence of Quotron terminals on the desks of its brokers. These terminals provided up-to-the-minute quotations──and functioned as a desktop electronic ticker tape. Quotron terminals eventually became the status quo. Quotron 1000 In the financial world a firm's competitive edge depends on its ability to gather data and, even more important, on its ability to quickly analyze and act on it. The latest technology available for this task is the Quotron 1000(TM) (Q1000). The Q1000 is a UNIX(R)-based system of networked minicomputers and software. Quotron provides a large package of financial services and international networking capabilities, most of which are based on the Q1000. Quotron computer systems are located in regional control centers (RCCs) that are found throughout the United States, Europe, Japan, and elsewhere. Workstations consist of dumb terminals and personal computers connected to the Quotron computers through various communications networks (see Figure 1). The majority of workstations within the Quotron system are dumb terminals, but personal computers are making major inroads, slowly taking over the desktops of traders and brokers. Quotron offers a package called the PC1000, which lets a PC act as a workstation on the Quotron network. As the amount of financial information available to the broker grows, it becomes vital to provide better ways to manage data flow. Personal computers are an ideal medium for doing this. First, PCs easily communicate with networks such as that offered by Quotron. Second, real-time data can easily be collected and off-loaded onto a PC. Once data is locally available, users can run it through any analysis and charting package. But even this level of sophistication cannot match the newest wave of technology. The simple ability to download data onto a PC, even from a real- time data feed, is no longer enough. Now brokers must analyze on a tick-by- tick basis, using data from both the domestic and foreign markets at the moment the data becomes available. At the same time, the old character-based interfaces of dumb terminals and PCs can no longer provide a useful window into this data stream. They are giving way to exciting new graphical interfaces. A graphical interface provides powerful new ways of viewing the constant streams of data that confront users. Working at the leading edge of graphical technology, Quotron has announced a new set of three PC-based applications designed to function in the immediacy of such an environment. Quotron Open Windows These applications are called Quotron Open Windows, and they work under the Microsoft(R) Windows operating environment. They were designed and built in a joint effort with Inmark Development Corporation, a firm experienced in developing financial applications. QuotTerm(TM) is a terminal emulation program that duplicates the standard Q1000 terminal. It provides the additional functionality of Windows. QuotTerm uses a proprietary line to the Q1000. QuotData(TM) is an application that allows users to collect and download data from a standard Q1000 data feed. It provides a means of building a user-defined database, and lets a user track up to 2500 securities on a tick-by-tick basis. QuotData maintains the database while running in either the foreground or the background. Via Windows' Dynamic Data Exchange (DDE) protocol QuotData acts as a server for any Windows application, and updates these applications on a real time, tick-by-tick basis. QuotData also builds a historical database at market close, made up of daily opening, high, low, and closing prices of each item in the database. In addition, a historical database containing 10 years of data is being added to the Quotron network. All of this will be available to applications via DDE. QuotChart(TM) is a real-time charting and analysis tool. QuotChart gets its data from QuotData and provides an extensive set of sophisticated charts that reflect tick-by-tick changes in the underlying financial data. Quotron Open Windows requires either an 80286 or 80386 processor, at least 2Mb of RAM, a hard disk drive, one serial port, an EGA, VGA, or higher resolution graphics adapter that supports Microsoft Windows, a bus mouse, and a Q1000 port. Speaking of Windows... Quotron Open Windows was created by Clifford P. Ribaudo, Inmark's President, and Mark Anders, its Executive Vice President. In 1985 they built and marketed Market Maker, a financial analysis and charting tool. Market Maker is character-based and served as the model for development of the new Quotron products. "A lot of ideas for Quotron Open Windows came from this one. We learned about what to do and what not to do," says Ribaudo. After completing Market Maker, Ribaudo and Anders began searching for a new programming environment. First, they wanted multitasking support on DOS- based machines. Second, they wanted better graphical support for their charting tools. "We explored TopView(R), DesqView, GSS(R), CGI; but none of these really suited our needs," says Anders. The environments that supplied multitasking were all character-based. The graphics packages that were available in 1985 were built on standards such as CGI and GSS and were generally not very robust. Then, according to Anders, "We booted up Windows, which we'd gotten through a $25 offer that came with a Quadram EGA card we had purchased. We found that we were looking at a unique environment that really seemed to meet our needs." A quick call to Microsoft for more details led to the discovery of the Windows Software Developer's Toolkit (SDK). "Windows had a graphical user interface that was part of a real multitasking system," says Anders. "There were pull-down menus, support for a mouse, and multitasking." Says Ribaudo, "It seemed to us an excellent model for creating interactive applications. "One of the best things about Windows was that suddenly we found ourselves within an environment that provided many of the tools we needed──dialog and list boxes, check boxes and radio buttons──already implemented as part of the environment. When we developed Market Maker, the character-based interface meant that we ourselves had to carefully calculate and hard code the locations of boxes, menus, and so on. Suddenly we no longer had to pinpoint and track the location of every object in our application." Adds Anders, "One major advantage of Windows is that it really forced us to take a careful look at our user interface. Windows really helped us to organize and maintain a consistent interface. After some extensive testing, Windows clearly emerged as the programming platform of choice." Windows 2.x Both Ribaudo and Anders were glad to see Windows 2.x released. In addition to overlapping windows and a cleaner user interface, it had improved documentation. This aided their learning process. However, says Anders, there is no escaping the fact that "there is a steep learning curve. It was a struggle. I had no experience on other windowing and event-driven environments to make it easier. It was the first time I had seen anything like it. Now, however, it feels as if I was always comfortable with it. At the time it seemed like I would spend weeks at a time stuck on one thing." "Control flow and message passing was the hardest to sort out. Windows has a real personality of its own," adds Ribaudo. The two note, however, that the difficulty of learning Windows is often overstated and that it didn't take them that long to get the hang of it. Once they decided to build the applications under Windows, they set about adapting their Market Maker technology. This eventually led to Quotron Open Windows. QuotTerm The Quotron dumb terminal and PC1000 software do a great job of providing analysts with the basic data needed for even the most complex analysis. The first challenge Inmark and Quotron faced was to come up with a good reason why a company should replace the dumb terminal or PC running the PC1000 software with a Windows product. Quotron and Inmark felt they had to fully emulate the standard Quotron screen of both the PC1000 software and the dumb terminal. At first glance it seems a step back to want to turn the PC into a dumb terminal. But the key issue was to give the analyst a safety cushion. Complete emulation of the older environment would let any person familiar with the standard Quotron terminal quickly use a PC equipped with QuotTerm. "The dumb terminal had to be supported or we wouldn't be able to move people off of it onto a PC," says Ribaudo. "That, and the availability of charting and analysis tools, and a personalized historical database, looked to be enough to convince any broker to switch." QuotTerm therefore provides this total emulation, down to the same screen locations and keystrokes for each service (see Figures 2 and 3☼). The QuotTerm application links directly to a Q1000 computer, and operates independently of the other available tools. But since QuotTerm works under Windows there is much more functionality. The screen colors are all modifiable, there is complete device-independent printer support, there is the ability to selectively modify information-field blink rates, as well as the ability to move and resize the QuotTerm window. With the mouse the user can click on symbols displayed on the screen in order to retrieve full quotes, headlines, and stories. And a user can run other Windows applications while QuotTerm is active. QuotData QuotData communicates directly with the Q1000. It monitors real-time data and updates historical files. QuotData talks with other applications via a proprietary link or DDE. Basically a communications/database front end for the Q1000 system, it can be considered the hub around which all other applications work. QuotData connects to the Q1000 system and requests real-time updates on a symbol-by-symbol basis using the Quotron micro-sdf (selective data feed) capability. Micro-sdf provides a proprietary protocol for the transmission of real-time price updates from the Quotron network through asynchronous lines to other computers. This protocol is implemented by two processes, called microread and microwrite, which run on the Quotron 1000. Microread blocks reading from the serial port that is awaiting requests from the PC. When the port gets a request, the system forwards it to the selective data feed driver, via a UNIX message queue. Seldf then requests price updates from the network and transmits each message to a microwrite over another message queue. Microwrite puts the data in packets and transmits it to the PC. Windows dialog boxes make it simple for the user to make selections, add/delete symbols, set the history parameters, and establish communications links. Users can define a symbol list and indicate if the symbol has associated volume and if it trades on any exchange with after-market hours. The history function lets users specify the amount, in days, of historical data they wish to store and save on disk. QuotData provides data to other applications running under Windows via two types of interapplication transfers. The first is a proprietary data channel that Inmark uses to talk to other Quotron/Inmark applications such as QuotChart. This channel is basically a shared memory queue that the server writes to and that any Quotron/Inmark application needing the data can read. Available memory is the only limitation to the number of these conversations that can take place at once. Second, and perhaps even more important, QuotData fully supports the standard Windows DDE protocol. QuotData can carry on multiple DDE sessions with any Windows application that supports DDE and wants to be "ADVISED" with real-time market data. Ribaudo and Anders will build full DDE support into all of their future products. Says Ribaudo, "What is important, and what has excited developers who have seen QuotData, is that because of DDE they will be able to develop Windows-based applications to work directly with QuotData and need never worry about external connections to the Q1000 itself." Refer to Figure 4 for a high-level overview of the data links supported. Communication through DDE is relatively simple. QuotData will support Text, Comma Separated Values (CSV), and BIFF format. It will return four different types of information, as shown in Figure 5. The information types shown in Figure 6 will be supported in the near future. Microsoft Excel is a good example of a program that was built to work with DDE. In Microsoft Excel, the user builds a spreadsheet that will create whatever report he/she may need. Then it links to QuotData through DDE and gets either historical data or tick-by-tick updates as required. The DDE data that an application needs to know in order to talk with QuotData is shown in Figure 7. In the spreadsheet, every cell needing data would contain a statement such as that shown in Figure 8A, where =INDEX is the Microsoft Excel function needed to establish the link with QuotData, and row# is a value ranging from 1 to 4, indicating what type of data it needs (see Figure 5). Column# is always 1. For instance, if the line shown in Figure 8B was defined in a spreadsheet cell, it would return the last sale price of the security represented by the symbol AAPL (see Figure 9☼). QuotChart QuotChart offers an extensive set of sophisticated charting and technical analysis tools. These tools operate on the real-time data supplied by QuotData via the proprietary channel mentioned above. QuotChart makes available all the major technical analysis studies with a wide variety of charts (see Figure 10). Additional studies can be performed and overlays created using tools such as Gann angles, cycle rule, Fibonacci rule, moving averages, and trend lines. A tool called the vertical wand lets the analyst select different "vertical periods" (sections) of a chart. Once the analyst outlines a section (see Figure 11☼), he/she can do things such as zoom in on that section. Aside from its graphics capabilities, one of the things that sets Windows apart from the other environments Ribaudo and Anders considered is the object-oriented nature of the Windows message-based architecture. As Anders puts it, "Under Windows what you really have are a collection of objects that communicate with each other through an established messaging system-messages are relayed from object to object. This is a really useful concept and supplies us with an elegant solution to some difficult design problems." The real problem lies in being able to handle a constant, and constantly changing, stream of data quickly and efficiently. Further, there is the need to apply different types of analysis to this changing data. If a user wants a moving average on one chart, and a stochastic on another, the system must be able to deal not only with the data but with the underlying mathematical formulas and relationships as well. As Ribaudo says, "We need to be able to provide a means to extend and modify the analytics on the fly." By using a special object-oriented language that they themselves developed, and which works under Windows, Ribaudo and Anders solved this problem. Their language consists of mathematical objects, which process streams of market data that arrive as messages and are then quickly "relayed from object to object." The processing structures in QuotChart are specified so that they look like a combination of a macro language and standard mathematical equations. Equation strings are specified at run time and built "below the surface," in response to selections that the user enters into dialog boxes. This facilitates the rapid development of new analytic features. Says Ribaudo, "To create a new type of analysis we need only create the new `object primitives' in C code and add them to the `instruction set.' The primitives are then combined in the language to create the new market study." For a detailed technical description of this language, see the accompanying sidebar, "Utilizing the Object-Oriented Nature of Windows." Pages and Templates QuotChart also provides two other very useful features: pages and templates. These permit saving of frequently used chart configurations. Pages let users predefine and store every last detail of a chart or series of charts, including all relevant data. The charts on a page all use the same "time axis." A template defines the structure of a page and doesn't contain specific references to data, although symbols can be saved if desired. A template brings up a chart or collection of charts that perhaps define some standard analysis that's applicable to different portfolios. It can be saved, assigned to function keys, and called up at any time and used with any portfolio. With pages, the analyst simply calls up a page and the predefined charts pop up, are linked to the server and updated to reflect the most current market information. Up to four charts can be saved per page, and two pages (up to eight charts) can be displayed on the screen at the same time. An enormous amount of information can be viewed simultaneously. Figures 12 and 13☼ show the page abilities of QuotChart. According to Ribaudo, the Windows environment makes all of this charting activity relatively simple. Windows itself takes care of all the resizing issues. "If a chart's size changes, Windows handles most of the display details, so that creating tools such as a zoom feature for particular areas of a given chart is quite simple. Windows makes the difference." Compare the relatively static charts of Market Maker shown in Figure 14☼ with the charting capabilities of QuotChart, shown in Figure 15☼. What's Next? Both Anders and Ribaudo are committed to the Windows 2.x environment. Says Anders, "You don't spend two and a half years coding a project under a graphical user interface if you don't like the interface or the coding methods." After working on Quotron Open Windows, a major new product by any measure, they are both glad to finally see a "real" product. It is clear to Anders and Ribaudo that graphical interfaces are the key to the next generation of software. When asked what their future directions might be, their answer was quick and to the point. "As soon as this gets out the door and the planned enhancements are in place, we are going to begin looking at the port to OS/2 and Presentation Manager. Figure 1: An overview of the Quotron Network ┌───────────┐ ┌─X.25──┤ Additonal │ ▓▓▒▒░░░░▒▒▓▓ │ │ Databases │ ▓▓Uni-Link▓▓───┘ └───────────┘ ▓▓Network ▓▓───┐ ▓▓▒▒░░░░▒▒▓▓ │ Other ┌───────────┐ │ └Protocols┤ Other │ Quotron's Paged │ Databases │ Format └───────────┘ ┌─────┴─────┐ │ Quotron's │Request for historical data │ CARS │packed into the page format for │ Host │transmission to CARS by Q1000 └─────┬─────┘ │ ▓▓▓▒▒░░░▒▒▓▓▓ └────────────────▓▓▓▒▒░░░▒▒▓▓▓ ▓▓Quotron's▓▓ ▓▓Network ▓▓ Paged response ▓▓▓▒▒░░░▒▒▓▓▓ unpacked by Q1000 ▓▓▓▒▒░░░▒▒▓▓▓ ┌────────┐ │ │ │ │ Remote │ ┌─────┘ │ └───────────────────────┐ │ Users │ │ └────────────────┐ │ └──────┬─┘ ┌───┴───┐ ┌───┴───┐ ┌───┴───┐ │ │ │ ┌─────────────┐ │ │ │ │ └──────┤ Q1000 ├─┤ Peripherals │ │ Q1000 │ │ Q1000 │ │ │ └─────────────┘ │ │ │ │ └──┬─┬──┘ └┬──┬──┬┘ └───┬───┘ ┌───────┘ │ ┌─────────┘ │ └──┐ └────────┐ │ │ │ ┌───┘ │ │ ┌────┴───┐┌────┴───┐ ┌────┴───┐┌───┴────┐┌───┴────┐ ┌────┴───┐ │ PC ││ PC │ │ PC ││ SUN ││ SUN │ │ PC │ │ ││ │ │ ││ ││ │ │ │ └────────┘└────────┘ └────────┘└────────┘└────────┘ └────────┘ A request for historical data generated by an application running on then PC, SUN(R) or Q1000 uses a standardized request format Figure 4: A high-level overview of current Quotron Open Windows Data Links. ┌──────────┐ ┌──────────────────────────────────┐ │ Regional │ │ UNIX-based Q1000 │ │ Control │◄────────►│ ┌────────────────┐ ┌────────┐ │ ┌───────────┐ │ Center │ │ │Logical Database├─►│Terminal├────►│ Quotron │ │ (RCC) │ │ └───────────┬───┘ │ │ │ │ Terminals │ └──────────┘ │ ┌──┴───┐ ┌──▼───┐ │Support ├────┐└───────────┘ │ │Micro-│ │Micro-│ └────────┘ │ │ │ │ read │ │write │ │ │ ┌───────┐ │ └─────┘ └──┬───┘ │ │ │ PC │ │ │ │ │ │ │running│ │ └──█████◄─┘ │ │ │Windows│ └────────▀▀▀▀─────────────────────┘ │ │2.x or │ ┌───────────────────────│─────────────────────────│────┤higher │ │ ┌────────────┘ │ └──────┬┘ │ ┌────────▼────────┐ ┌───────▼─────────┐ │ │ │■──────────────↓↑│ │■──────────────↓↑│ │ │ │ │ │ │ │ │ │ QuotData │ │ QuotTerm │ │ │ │ │ │ │ │ Via a │ └──────────┬─┬───┘ └─────────────────┘ │ Proprietary │ │ └────────────DDE─────────────┐ │ Data Link───────────┤ └────DDE───────┐ │ │ (DDE will │ │ ┌────────▼────────┐ │ be supported │ ┌───────▼─────────┐────────────↓↑│ │ in the next ┌────▼────────────┐────────────↓↑│ Other │ │ release) │ │■──────────────↓↑│ Microsoft │ Windows │ │ │ │ │ Excel │ Applications │ │ │ │ QuotChart │ │──────────────┘ │ │ │ │──────────────┘ │ │ └─────────────────┘ │ └─────────────────────────────────────────────────────────────┘ Figure 5: QuotData returns information to Microsoft Excel. Last Price Net Change Volume of Last Trade Total Volume for the Day Figure 6: New QuotData information that will become available through DDE. Bid Price Bid Size Ask Price Ask Size Year High Year Low Open Interest Previous Close Previous Bid Dividend Yield Earnings PE Price/Earnings X Dividend Fiscal Year End Figure 7: Microsoft Excel uses this information to communicate with QuotData via DDE. Application Name QUOTDATA Topic Name TICKDATA Item Name Security Symbol (e.g. MSFT) Figure 8A: Microsoft Excel command structure for obtaining QuotData information via DDE. =INDEX(QUOTDATA|TICKDATA!Symbol,column#,row#) Figure 8B: Sample Microsoft Excel DDE command. =INDEX(QUOTDATA|TICKDATA!AAPL,1,1) Figure 10: Charting and analysis features of QuotChart volume histogram departure ratio spread moving average and convergence/divergence (MACD) fast and slow stochastics high, low, close bar charts ─────────────────────────────────────────────────────────────────────────── Utilizing the Object-Oriented Nature of Windows ─────────────────────────────────────────────────────────────────────────── Mark Anders and Clifford P. Ribaudo When we set out to create QuotChart, we had a simple objective; create a graphical, technical analysis package that could handle real-time tick-by- tick data. Technical analysis involves applying various mathematical formulas, or studies, to financial price data. Some simple studies include the calculation of moving averages or the range of a data element over a given period of time. Often these studies either cause a smoothing of data or amplify some rate of change, so that trends in the price can be detected. By real time we mean that a recalc is performed each and every time a trade occurs, in much the same way that a spreadsheet changes when you modify one cell. QuotChart differs from many other real-time charting packages in that it does a recalc on every price change, or tick. With most other packages, if you are looking at a chart where each x-axis unit is ten minutes, a recalc on some or all of the studies will only be done after that ten minute period is finished. While writing Market Maker, we learned that some of the most important work done during the creation of an application is in the planning stage, before the actual coding begins. One of our objectives was to make the application as modular as possible. Aside from being good programing practice, since we hadn't chosen an operating environment at the time that we began designing and writing QuotChart, it was important that the user interface layer be separate from the areas responsible for performing calculations and plotting the data on the display. In general our design decisions had to be made not only with a view towards what we planned to release as Version 1.0, but also attempting to anticipate where we wanted to take the product in the future. Because of the overriding need for flexibility, it was decided that the core of the program would be an object-oriented language tailored to process the incoming data stream, and that the user interface layer would generate formulas written in this language. The object-oriented nature was necessary so that each instance of a function call could remember the state of its calculations for faster recalcs. There were disadvantages to this approach. First, there was overhead involved because a formula had to be created, parsed, and code of some type generated. In addition, a compiler had to be written for the language. The advantages, however, were numerous. Isolating the user interface from the core code meant that as long as the syntax of the language remained the same, you could change the actual implementation of language without having to modify the user interface layer. The language can be viewed as a communications mechanism between the user interface and the way the program internally organizes its data. Because of this defined interface between the two layers, the details of how the core code works is completely invisible to the user interface. Since the interface has no knowledge of any internal data structures, which of course could change, it is less likely to be broken when modifying the core, or damage any other areas of the program when it is modified. Portability is also increased. Since the core is only passed strings to compile and execute, it doesn't know or care what the user interface is like. Only those objects having to deal directly with the outside environment, for example the object that actually draws a line, would require modification to a run in a new environment. Each object in the language represents a particular operation on a data stream. Once the piece of code is written which implements that operation, you can reuse it in a variety of settings. For example, perhaps the most frequently used object in the system is the object called Line. As its name implies, it draws a line on the display (or printer). It takes three arguments, the first being the data stream from which it receives its data. The remaining two, which are optional, are names for the line; the first used for display to the user, the second being a unique object name that other objects can use to reference Line's data. The formula used to create a line chart of the last or closing price of Microsoft (MSFT) would be: Line(Close("MSFT"),"Last Price of Microsoft", "l1") This formula computes and draws the last price for Microsoft. If the system was to show data from that line, or allow the user to use the values contained in that line as input for another study, it could refer to it by the name Last Price of Microsoft. Another commonly used object is called SMA (for Simple Moving Average) which computes the sum of the last n prices and divides that by n. Often a technical analyst will overlay moving averages on a chart. If, for example, you wanted to overlay a line of the 7-unit simple moving average of the last price of Microsoft on the above chart you would add the following formula to that chart: Line(SMA(Close("MSFT"),7), "7 unit Simple Moving Average","l2") This could be considered a direct application of the moving average object. Moving averages are also used as elements of studies. For example, Moving Average Convergence Divergence (MACD) takes the difference between two moving averages of different lengths to create one line, called the MACD line, and feeds this result into another moving average to create a second line, called the signal. MACD uses a different type of moving average called an Exponentially Weighted Moving Average (EMA). The formula for this is: Line (EMA(Line((EMA(Close("MSFT"),12)-EMA(Close("MSFT"),26)),"MACD","11"), 9), "Signal","l2") This example serves to illustrate a few points concerning the flexibility gained by the implementation of the language. First, the objects created can be used in a number of different contexts. For instance, notice that the line called MACD takes its input from EMA and returns its output (the Line object returns its input unchanged) to be used as input for another EMA. Another benefit is not as obvious. Because EMA and SMA are both moving averages, much of the code used to implement how each behaves is shared, so that EMA and SMA are subclasses of the moving average class of objects. SMA is also a subclass of the sum object, which computes the sum of the last n data elements. There is an object called Hist which is a relative of Line but produces histogram type output. The code used for the actual drawing is the only code which they do not share. The fact that many objects share code means writing less code, and when you need to add new functionality to a class of objects, you only have to do it in the superclass. For instance (no pun intended!), we needed to be able to display the value of all lines at a certain x value, which of course was easily done by writing the corresponding code in the superclass of Line and Hist. There was another persuasive reason for having the language at the core. While a user interface that allows the user to select from options, such as the period for a moving average, is powerful, eventually some users are going to want to program their own studies. If this capability was not planned for, it would require substantially more work to implement than if it existed from the beginning. In this case, what would really be provided is a free-form user interface to the same language and not an afterthought. One of the interesting things about the creation of QuotChart is that most of the design and implementation of the language was done before Microsoft Windows was selected as the operating environment. We were very lucky that Windows was object-oriented and both lent itself to and encouraged modular design. The ability to easily create menus and have tools such as the dialog editor meant that adding new features was often just a matter of drawing a new dialog box and providing it with the ability to generate a new formula string. It can be tricky programming under Windows, but it has really helped us maintain our goals of modular design without having to invent our own object-oriented user interface. Porting Apple Macintosh Applications to the Microsoft Windows Environment Andrew Schulman and Ray Valdes☼ You're happy with your Macintosh program. It cooperates with other programs under MultiFinder(TM), it runs like the wind in color on the Mac(R) II, and even your customers with Mac 512Ks are fairly happy with the program. Your development goes smoothly using the Lightspeed(R) C compiler and TMON debugger and, though you wish Lightspeed's editor weren't so simplistic, you've put together some dynamite EMACS macros using Quickeys(TM). You wish you had C++, but Apple is coming out with MPW(TM) C++ soon. And you've got to love that Mac II. Why then would you want to port your program to the PC, Windows, and the OS/2 Presentation Manager (hereafter referred to as PM)? A Wider World Windows has become a de facto standard for graphical windowed software in the IBM world, as evidenced by its incorporation into the IBM(R) OS/2 Presentation Manager. Windows can be your entrée to the larger world of the Graphical User Interface (GUI) standard. Not only is Windows on the path to OS/2 Presentation Manager, and from there to the promise of SAA, but the Windows programming model is much closer to that of X Windows than is the Mac's. There's another sense in which porting to Windows will widen your perspective. Not only is the PC software market significantly larger than the Mac market, but there is a much wider range of hardware. Right now, Mac software only runs on screens with square pixels. Windows is fairly device- independent so that, if you port to Windows, your software will run even on screens and printers with pixel ratios like 12:5, 1.43:1, and 1.37:1. You may think such screen ratios odd, and we would agree with you, but these ghastly monitors represent the majority of the real world. Working with Windows If you're a Macintosh programmer who needs to learn how to program for Windows, there is some good news and some bad news. The good news is that everything you know is not wrong. You already know the central ideas behind Windows programming: event-driven input; text is just another form of graphics output; relocatable memory; and resources. Ironically, a Mac programmer who has never sat down at an IBM PC is in a better position to learn Windows than a seasoned DOS programmer who has not yet gone through the infamous "everything you know is wrong" learning curve. If you know Mac programming, then under Windows much of what you know is still true. A Mac programmer has already learned that application code is inert and passive, and simply waits to be activated by a data structure (called an EventRecord on the Mac and an MSG under Windows). A C programmer on the Mac has probably said good-bye to old friends like printf and gets and, sure enough, you won't use these for Windows programs either. Relocatable memory blocks? Mac programmers handle these routinely, so the fact that Windows moves memory around is not an issue. Your Mac knowledge can be leveraged in programming for a market with millions of IBM and IBM-compatible computers. After you've worked with the application program interfaces (API) of several graphical windowed environments, you begin to realize how similar they are. There are many strong resemblances between the Macintosh Toolbox and the Windows API, even some one-to-one correspondences. Take the function PtInRect: how could determining whether a given point falls within a given rectangle not be the same in all systems? Everything Is Different There are many other formal analogies. For example, the Macintosh memory manager provides a function, NewHandle, which returns a handle to a block of relocatable memory, and the Windows kernel provides GlobalAlloc which, when called with the GMEM_MOVEABLE flag, also returns a HANDLE to a block of relocatable memory. A Windows HANDLE sounds just like a Macintosh Handle. There are many such seeming similarities between the Mac and Windows. Believe it or not, that is the bad news. While the Mac, Windows, and Presentation Manager are in some formal sense identical and after a while feel all the same, it's the little subtle differences that matter when you're porting code. Although they are conceptually identical, when you get to the details, a Windows HANDLE is almost nothing like a Macintosh Handle. Here are further illustrations of the same point: Windows regions are not at all like regions on the Mac; the Windows function FindWindow has nothing to do with the Mac function FindWindow; the Windows equivalent for the Mac function FrameRect is the function Rectangle and not the function FrameRect. In this article, we will first port a tiny fragment of Mac graphics code to Windows, illustrating the similarities and differences between Mac's QuickDraw(TM) and Windows' graphical device interface (GDI), and we will show one approach to portable code that takes advantage of the similarities, while isolating you from the differences. We then present a complete application that shows a unified approach to Macintosh and Windows events, windows, and memory management. A PC port is not only desirable, but possible, as portable code is both desirable and possible. The Update Event Kernighan and Ritchie in The C Programming Language, (Prentice Hall, 1988), 2nd edition, p. 5 say, "The only way to learn a new programming language is by writing programs in it," but probably the only way to learn a new windowed graphical environment's API is by writing parts of programs in it. Even "Hello World" in such an environment typically involves more code than a newcomer can be expected to type, especially on a new machine using an unfamiliar editor. One of the more bizarre (until you get used to it) features of GUI environments actually comes to our rescue in the form of redrawing old output. Whether on the Mac or Windows, your program must be prepared to reproduce its output at any time. The reason for this is that the window to which you send your output (text or graphics) may get damaged. When this happens, the Mac sends your application an updateEvt, and Windows sends the damaged window a WM_PAINT message. On the Mac one responds to an updateEvt by bracketing QuickDraw calls between a BeginUpdate and an EndUpdate and, similarly, in Windows the response to a WM_PAINT message is to sandwich GDI calls between a BeginPaint and an EndPaint. So you can see that programming for Windows won't be that unfamiliar. Since your application must be prepared to meet an update event by redrawing the window's contents, an application can confine all of its drawing to this one place. Let's begin by writing some code that will get called whenever our window receives a WM_PAINT message. What we propose to do is take the HELLO "application" (it's actually more of a skeleton) from the Windows Software Development Kit (SDK), remove the call to the function HelloPaint, replace it with some Mac code, and alter the Mac code into something that will compile and run on the PC. We will start with as little alteration as possible, then gradually work up to full Windows code, writing a real version that runs under both systems. The Mac code found in the function StopSign illustrates QuickDraw's MoveTo, Move, and Line functions, and is loosely borrowed from Stephen Chernicoff's superb book Macintosh Revealed (Hayden Books, 1987), 2nd edition, Vol. I, pp. 189-191. The code appears in Figure 1, along with bits of surrounding context, so you can see how the function StopSign would get called on the Mac. The function scales the stop sign to the size of the window into which it's being drawn. If you resize the window, you not only generate a mouseDown inGrow; you also generate an updateEvt that causes the stop sign to be rescaled and redrawn to the new window dimensions. The Hard Part A Mac programmer who wants to put this Mac code into a Windows program faces one hurdle that has nothing to do with the code itself: you have to set up the SDK and the C compiler on your PC/AT(R) or 386 machine, you need a text editor, and you'll soon want a set of tools like grep and diff. Note that Windows/386 lets you perform tasks from within the graphical windowed environment (see Figure 2☼), whereas Windows/286 makes you edit and compile in character mode, just dropping into Windows long enough to test the resulting program. Integrated programming environments such as QuickC(R) compiler and Turbo C(R) unfortunately will not generate code for Windows. If you're used to the "double-click and you're done" simplicity of installing software on the Mac, then installing tools like C 5.1 and the Windows SDK on your 286 or 386 is going to be more difficult. All we can tell you is, once you can compile HELLO.EXE from the SDK, then writing code is going to seem easy. Take One Figure 3 shows our first take at a Windows equivalent. While we could get the exact same Mac code to run on a PC by emulating a GrafPort and a set of functions to manipulate it, we've already worked on such a project and the resulting performance was unacceptably slow. Instead we're using "native" Windows code, while trying to preserve as much of the feel of the Mac as possible. The only parameter the Macintosh version of StopSign needs is a WindowPtr, which comes along with the updateEvt in event.message. Because WindowPtr is just another name for GrafPtr, StopSign uses this single parameter to get both the portRect into which to scale the stop sign, and the GrafPort where output calls are directed. In the Windows version, notice that we pass in two parameters: an HWND and an HDC. An HWND is a handle to a window and an HDC is a handle to a Device Context (DC), which is a data structure maintained by Windows that, like a GrafPort, maintains graphics states such as the current pen, clipping region, current position, font, and so on. While the Mac keeps a very close association between a window and a GrafPort (in fact, a WindowRecord is a GrafPort, with some extra fields stuck on the end), under Windows an HWND often has only an HDC associated with it while the application is servicing a WM_PAINT message. One of the most important tasks of BeginPaint is to give an HDC to an HWND that's received a WM_PAINT. While the pairs BeginUpdate/EndUpdate and BeginPaint/EndPaint are syntactically nearly identical, each serves a little different purpose. The "Magic Cookie" From the code in Figures 1 and 3 we can see that, where the Mac version grabs the dimensions of the window's content region by directly peeking at port->portRect, under Windows we use the API function GetClientRect. The portRect and the client rect are conceptually similar, except the Mac inconveniently includes scroll bars as part of the portRect and we have to compensate for them. A more important difference, though, is that under Windows we made a function call to get at something that is simply a field in a data structure on the Mac. This is a key philosophical difference between the Mac and Windows: Mac programs directly manipulate the fields of a GrafPort (for example, thePort->info) while Windows programs have no knowledge of the internal structure of a DC or of nearly any other Windows data structure, and have to use function calls to find things out (for example, GetInfo(hDC)). However, don't we have a Handle to a DC? No, we have a HANDLE. A Windows HANDLE is not a pointer to a pointer (**hThing is the thing itself) like a Macintosh Handle; it is a "magic cookie." In the call GetClientRect(hWnd, &r), hWnd is the "magic cookie": we pass it in, and when the function returns, r contains our information. We don't know where in the WND data structure the client rect is kept, and we need not; GetClientRect is a black box. In complete contrast, thePort->portRect is more like Pandora's box. The Windows API is not perfect, and in some areas it is inferior to the Macintosh Toolbox; but for data abstraction Windows wins hands down. In other words, the Windows API has a function-call interface (narrow channels of communication between your program and the environment); the Macintosh approach of working directly with the data structures and low- memory globals approach works well right now, but the channel is too wide to constitute an interface. Being Explicit One of the most noticeable differences between the Mac code in Figure 1 and the Windows code in Figure 3 is that, on the Mac a line may be drawn with Line(8 * scale, 0), while in the Windows version we say Line(hDC, 8 * scale, 0). That one little hDC parameter is quite important. Whereas QuickDraw calls operate implicitly on thePort, and therefore establish the current port with SetPort (squirreling away a pointer to the previous port so that it can be restored at a later time), every GDI call takes an HDC as an explicit parameter. This passing around of hDC from function to function as though it were a hot potato will seem tedious for tiny applications with only one window and only one DC but, for real applications with multiple windows, it can result in cleaner code than the frequent GetPort/SetPorts one often sees in Mac code. The Fleeting GrafPort To establish a border of 5 units on all four sides of the stop sign, both versions reduce the size of the drawing area by 10 units and shift its origin southeast by 5 units. On the Mac, this is done with SetOrigin. The previous origin is saved so that, when we finish, we can restore things to the way we found them. Under Windows we could use SetViewportOrg, but using OffsetViewportOrg seemed more suitable, since then we don't need to know what the previous origin was. Why viewport? We'll get back to that when we discuss our second version of the stop sign. A more important point is that in this case it is not absolutely necessary to undo the OffsetViewportOrg at the end of the Windows version. Remember that an HDC is not tied tightly to a Windows window as a GrafPort is to a Mac window. Each time BeginPaint is called, we get a fresh DC. Any alterations we made to a DC, such as altering the origin or selecting a different pen or font, disappear after calling EndPaint. This means that, if you're about to call EndPaint anyway, you may be able to avoid undoing some changes; but it also means that you have to set up what you need each time you call BeginPaint. Many Mac programs will have a hard time with this fleeting GrafPort, because they assume they can ask at any time about, say, GrafPtr->pnLoc. When these programs are ported to Windows, the assumption is that at any time you can call, say, GetCurrentPosition(hDC), not realizing that you often don't have an HDC. There is a way for a Windows program to give each of its windows a DC that persists even after you've called EndPaint and for which EndPaint is, in fact, a NOP. When you establish a window class during the initialization of a Windows program, you can specify its style as CS_OWNDC. All Windows created with this class then have their own DC. Don't do this! However tempting it is for ported Mac code to use CS_OWNDC, don't succumb to this temptation. Using CS_OWNDC results in a lot of unnecessary window invalidation and generally causes problems in the form of weird repainting bugs. Emulating Toolbox Calls Returning to the code in Figure 3, note that we had to write our own Line and Move functions. Where the Mac has LineTo, MoveTo, Line, and Move, Windows has only LineTo and MoveTo. Here we get into one strategy for portable code: rather than rewriting StopSign so it uses only LineTo and MoveTo, we wrote our own Line and Move for Windows. For simple functions like these, emulating the Toolbox under Windows works well. While MoveTo moves the current position to an absolute location in the current GrafPort (on the Mac) or the designated HDC (under Windows), Move offsets the pen relative to its current position. To write a Windows version of Move, we need to know the current position. GetCurrentPosition returns (x,y) packed into an unsigned long; the individual components are broken out using the LOWORD and HIWORD macros from windows.h. Always use these macros to extract such packed information. Never write your own code to do so. While we're supplying these Mac-like routines, it is useful to make them behave as much like other Windows functions as possible. Although we never use the return value, we pass it along. And for consistency's sake we use the FAR PASCAL modifiers. Both the Macintosh Toolbox and the Windows API use the Pascal calling convention, but for different reasons. FAR is something that we'll explain later, when we talk about memory. There's one other change to make: adding stop.c to the Hello make file. While editing the make file, add the -W2 switch to the compiler command line so that you get adequate warnings from the C compiler. To compile the code, all you have to type is MAKE HELLO. All sorts of inscrutable compiler switches will fly by on the screen. If all goes well, make will also run the linker and the RC resource compiler. We've been focusing on the changes to the Mac code in Figure 1 to turn it into the Windows code in Figure 3, but the two versions are actually similar. Both use the same formula to compute the scale by which all coordinates are multiplied. The scale is divided by 18 because that's the largest unit of measurement in the sequence of MoveTo/Line/Move calls. A PC is Not a Mac It's time to run our modified HELLO.EXE. If you're developing under Windows/386, you can just double-click on HELLO.EXE. If you're using Windows/286, first you'll have to go into Windows. Either way, you start up the new application and... it's awful! Unless you're running on a VGA or a monitor with square pixels, the stop sign is horribly elongated. It's particularly nasty on a CGA (see Figure 4☼). The stop sign resizes itself nicely enough when we resize the window (so at least GetClientRect is working for us), but the shape remains distorted. Here we discover that a PC, even a PC running Windows, is not a Mac. Macs have screens with square pixels, and more and more PCs (and all PS/2(R) machines) come with square-pixel monitors, while Windows programs have to run on a variety of monitors, good, bad, and ugly (otherwise known as VGA, EGA, and CGA). Windows programs can make fewer assumptions than Mac programs. For a guide to the world of PC graphics adapters, see Richard Wilton's Programmer's Guide to PC & PS/2 Video Systems (Microsoft Press, 1987). Microsoft Windows will shield you from having to know whether you're running in 640 x 480 or 640 x 200─if you let it. The stop sign is elongated because we're using Windows' default mapping mode, MM_TEXT, in which units of measurement, such as the ones we use in our MoveTo/Move/Line calls, represent pixels on the screen. This results in graphical output that depends on the display being used, but it is closest to the arrangement on the Mac-where all the coordinates represent pixels on the screen, 72 to the inch, and where there is a good match between printers and monitors. This presents a problem: using the default MM_TEXT mapping mode may be the easiest way to port Mac code to Windows, because Mac programs assume that a unit is a pixel and they do their own scaling. But bear in mind that squares will only look square on a VGA. Using the closest analogies between two systems does not always produce good results, so beware of using MM_TEXT for graphics. The units-are-pixels arrangement just happens to works well on monitors with square pixels by coincidence. This is why your development hardware should include at least one CGA monitor. Don't do your testing on one of those beautiful 8 1/2 x 11 paper-white monitors; everything looks great on those, making them nice to work with, but worthless for testing. Take Two Do we to have to check what monitor we're running on, and scale differently in the x and y directions? No, in fact, we can dispense with the (* scale) stuff altogether for the PC version, and let Windows do all our scaling for us. Windows has programmable coordinate systems. We can set up a mapping mode so that the stop sign will come out square, no matter what screen is used. We can set up our own units of measurement so that, for example, 18 units always fills the window no matter what the window's size. But we don't want arbitrary scaling along both the x and y axes; we want the stop sign to fill the window as completely as possible, without distorting its shape. We can do this using the MM_ISOTROPIC mapping mode, which is used in the analog CLOCK program that comes with Windows (and the source code for which comes with the Windows SDK). The Mac doesn't really have any built-in facility for programmable coordinate systems. Macintosh pictures have a picFrame which is scaled to a destination rectangle when you call DrawPicture, resulting in an arbitrary scaling along the x and y axis. This is similar to Windows' MM_ANISOTROPIC mapping mode. The Mac also has the ScalePt and MapPt functions, but there isn't anything as general as Windows' mapping modes. In order to use the isotropic mapping mode, Windows needs to know what unit of measurement you're using and what rectangle these should be mapped onto. For example, our stop sign seems to be in a unit of measurement whose largest unit is 18, and we want 18 to map to the extent of the portRect, minus our border of 5 units on each side-actually not that difficult. The rectangle that you're mapping into (the destRect, so to speak) is called the "viewport," and the unit that you're mapping from is called, unfortunately, the "window." We've been discussing our second version for quite a while, but it's just a tiny change in Figure 5. We set the map mode to MM_ISOTROPIC, and we set our window and viewport extents. Instead of actually deleting all the (* scale) code, we left it in and just set the scale to 1. If you enter the changes in Figure 5, recompile (MAKE HELLO), and run the new HELLO.EXE, you'll see a big improvement. The stop sign keeps its shape, no matter what the window shape, the window size, or what monitor is used. Because the mapping mode takes care of the conversion of "logical units" (a very powerful idea) to device units, we can also use it for the rotation of objects. To rotate the stop sign, just replace the line: SetWindowExt(hDC, 18, 18); with: SetWindowExt(hDC, -18, 18); SetWindowOrg(hDC, 18, 18); More information on using mapping modes for rotation of objects can be found in Brian Myers and Chris Doner's useful book Graphics Programming Under Windows (Sybex, 1988). Take Three Notice that we've been passing StopSign the window handle hWnd so it can do a GetClientRect each time we get an update event (WM_PAINT). Not only is adding this extra parameter a bit of a nuisance, but often when we get a WM_PAINT, the size hasn't changed. We can make a minor change to HELLO.C to keep the current client rect around, updating it whenever we get a WM_SIZE message. If you search through HELLO.C, though, you won't find any explicit handling of size messages. This seems like a Mac program that doesn't handle a mouseDown inGrow; how can we be resizing our windows as we've been doing, if the application doesn't handle this event? In Windows, "window classes" really do behave like classes in object- oriented programming: they have a default behavior. Messages not explicitly handled by a window procedure in its switch statement, fall through to the default case where they are passed to DefWindowProc. A Windows program doesn't manage the tracking of the mouse during window resizing, the way a Mac program does by calling GrowWindow and SizeWindow. Figure 6 shows the changes needed so that we can maintain the client rect ourselves, rather than asking Windows for it every time. We trap for the WM_SIZE message now, updating the global variable client_rect. StopSign no longer takes an HWND parameter; instead, it inspects the global variable client_rect. In a sense we've thinned StopSign's interface, since now it takes one parameter instead of two, but now we're peeking at global variables, which is worse. We'll fix this in our fifth version. Take Four If it makes sense for us to maintain our own state for the window size, it makes even more sense to do the same for the current position, which we've been inquiring after each time in our functions Move and Line. We could add two more global variables, curr_x and curr_y, and use them and update them in Move and Line-except that the current position is also changed by the Windows functions MoveTo and LineTo. Of course, we didn't write our own versions of MoveTo and LineTo, since Windows supplies these itself. In order to keep curr_x and curr_y up to date, we'll have to write tiny front- ends, MyMoveTo and MyLineTo, which simply update our variables and then call Windows' MoveTo and LineTo. It may sound silly, but writing this front-end is our most powerful idea so far. Making our own versions of the "primitives" supplied by the operating environment is the first real step toward portable code. If you don't have a certain routine, it's often useful to create a hypothetical one; textbooks on computer science call this "wishful thinking." Later on, you write the routine that you hypothesized you had. Conversely, when writing portable code it's often useful to do the opposite; the API provides a routine, but you pretend that it doesn't. You call your own interface routine instead; this routine can call the underlying service. When switching from one environment to another, all that you rewrite are your interface routines. Now that we have a little set of four output routines and two variables that they monitor, it makes sense to package all this in a separate module. In our third version we added a global variable (client_rect) that's visible all over the place, but here we've done things properly; curr_x and curr_y are statics that are visible only to the routines that use them in our new module, DRAW.C (see Figure 7). DRAW.H provides the external interface to the functions in DRAW.C. It also uses some #defines to remap all LineTo calls to MyLineTo and all MoveTo calls to MyMoveTo. For consistency checking, DRAW.C #includes DRAW.H. But, in DRAW.C we actually have to call the "real" MoveTo, not MyMoveTo. DRAW.C #defines the preprocessor symbol DRAW_C; if DRAW.H sees that DRAW_C is #defined, it doesn't change the MoveTo and LineTo calls. Writing portable code certainly seems to involve a lot of wrestling with the preprocessor. Don't forget to update the HELLO make file again, this time adding DRAW.C and DRAW.H. Take Five At this point, we had better get this code running back on the Mac again. By writing front-ends for LineTo and MoveTo, we are well on the way to portable code. Instead of raw in-line calls to Windows, we're calling our own routines, which then take care of calling Windows. Except for that hDC in the function parameters, the routines would just as well call the Mac Toolbox. The first thing we must do is get rid of that HDC parameter, yet somehow still have an HDC to pass to the Windows routines. We want each environment to do scaling in its "native" way, yet not have separate code at the application level. Figure 8 shows the changed portions of HELLO.C and new STOP.C. It also shows a fragment of a Macintosh main calling the new portable StopSign function. What is most noticeable about the new STOP.C is that it does not #include "windows.h" and it does not #include "QuickDraw.h". Instead we include a small file, called CANVAS.H, which is the external interface to a new module, CANVAS.C (see Figure 9), that provides one common interface to our two different operating environments. For instance, STOP.C calls the procedure offset_org, which sets the origin of a drawing context (a GrafPort or an HDC). If compiling for the Macintosh (#ifdef MAC), offset_org takes care of calling SetOrigin; otherwise it calls Windows' OffsetViewportOrg. But OffsetViewportOrg requires an hDC parameter and, on the Mac-depending on which GrafPort we want to do a SetOrigin on-we may need to switch thePort. How can we provide the same external interface to these two operations which are perhaps conceptually similar, but so completely different in practice? HCANVAS Continuing with this example, the first parameter to offset_org is hCanvas, a handle to a CANVAS, our new data type provided by CANVAS.C. A CANVAS contains all the information needed to talk to a GrafPort on the Macintosh and to an HWND or HDC under Windows. On the Mac, the window field of a CANVAS contains a WindowPtr; under Windows it contains an HWND. On the Mac, offset_org fiddles with the portRect and possibly switches thePort. Under Windows, we use the macro CANVAS_HDC(hCanvas) to extract the HDC from a CANVAS so we have an hDC to pass to OffsetViewportOrg. We manage to pass around some number (an HCANVAS) at our application level, yet we still have an HDC when the time comes to make the Windows call. Using the HCANVAS to get at a GrafPort or an HDC is handled entirely inside CANVAS.C. But how can an HDC be kept around inside a CANVAS when we know that the HDC is fleeting? We are not using the CS_OWNDC trick. Instead, while we are servicing a WM_PAINT message, the CANVAS contains a valid HDC. It is put there by the function begin_update. When we are done servicing an updateEvt or WM_PAINT, we call end_update which on the Mac calls EndUpdate and DrawGrowIcon, but under Windows calls EndPaint and, most important, sets the HDC in the CANVAS to zero. Any canvas function, like offset_org, whose Windows version requires an HDC, first checks to see if (!CANVAS_HDC(hCanvas)). If offset_org has been called between calls to begin_update and end_update, then there will be a valid HDC and we can make the Windows call. Otherwise, CANVAS_HDC(hCanvas) will be zero. In the code presented in Figure 9, we return when the HDC is zero. This is particularly important because a debugging version of Windows checks for invalid HDCs. If it finds one, it tries to send a FatalExit (RIP) code out to an AUX device and, if you don't have an AUX device, Windows hangs with the "Cannot write to device AUX" dialog box, waiting for you to attach an AUX device. For an introduction to debugging Windows, see Durant, Carlson, and Yao, Programmer's Guide to Windows (Sybex, 1988), 2nd edition, Chapter 15. Rather than simply returning, our code should do something in order to acquire an HDC. This technique is used in the functions env_StartDrawing and env_EndDrawing in ENVRNMT.C, from which CANVAS.C has been extracted, and which is used in the larger application we will discuss later in this article. The Windows version of env_StartDrawing sets the HDC field in a CANVAS by calling GetDC(CANVAS_HWND(hCanvas)). GetDC is a Windows function, and CANVAS_HWND is our macro for extracting the HWND stored in a CANVAS. Our function env_EndDrawing clears the HDC field with: ReleaseDC(CANVAS_HWND(hCanvas), CANVAS_HDC(hCanvas)); CANVAS_HDC(hCanvas) = 0; Env_StartDrawing and env_EndDrawing do something completely different on the Mac. At the application layer, though, the effect is the same. Looking back at STOP.C in Figure 8, note that the multiplication of (x,y) coordinates by a scaling factor is gone. All scaling has been moved to CANVAS.C. The large macro with the strange name MAYBE_SWITCHING_PORT is called from the Macintosh versions of move, line, move_to, and line_to, and multiplies the (x,y) coordinates by CANVAS_SCALE(hCanvas). CANVAS_SCALE(hCanvas) is set by the function set_scale on the Mac, but under Windows, set_scale sets the map mode, the window extent, and the viewport extent. As seen in Figure 9, a CANVAS contains all sorts of things besides GrafPorts or HWNDs and HDCs. Everything that in previous versions was scattered all over the code, like the global variable client_rect from the third version, or the statics curr_x and curr_y from the fourth, are now part of a CANVAS. The CANVAS has become our own machine-independent GrafPort or DC. Raw or Cooked While STOP.C contains no explicit Windows calls, it is still Windows code; it is simply portable Windows code. At the same time, while it contains no explicit Mac Toolbox calls, it is also Macintosh code, because the functions it calls in turn make Mac Toolbox calls. It is important to note that you can have Windows code that doesn't contain "raw" calls to OffsetViewportOrg or to BeginPaint. Something can be Mac code without containing raw calls to SetOrigin or BeginUpdate. Code found in good commercial applications does not resemble the code published in Microsoft Systems Journal or in MacTutor because, in order to show how to use an API, such sources must of necessity present nonportable system-dependent calls intermixed with higher-level code. We call this the raw style of coding, in contrast to the "cooked" form shown in STOP.C. Calling the cooked transform_something, rather than the raw API call TransformSomething, is only the beginning. Most commercial applications require a higher level of abstraction than that. Many commercial applications use typedefs and #defines to alter C so that it's unclear to outsiders and newcomers. However, this is essential for keeping the code close to the problem at hand, and for maintaining portability. Portable code is broken into an application layer (core) and an environment layer (edge). Ideally, the application layer doesn't change, no matter what system it's running under, and only #includes your own .h files. This means you must ensure that the application layer of your application knows nothing about files such as "windows.h" or "QuickDraw.h." This is a litmus test to determine how well you have isolated yourself from system-level dependencies. Depending on the level of your commitment to portability, the "only our own .h files" rule might even extend to the C standard library. For instance, while Microsoft C has the memset routine-declared in <string.h> for ANSI C compatibility and in <memory.h> for UNIX System V compatibility-Lightspeed C on the Mac has a function setmem which does the same thing, except its arguments are in a different order and its declaration is in <unix.h>. Using these routines is preferable to writing your own, because the compiler manufacturers are supposed to implement them efficiently (using REP STOSB on the PC, for example), but you also want to be shielded from minor differences. One solution might be a file, STD.C, to handle all nonstandard standard library routines, and so the environment layer might even include such standard routines as memset. In order to highlight the correspondence between Mac Toolbox functions and their Windows equivalents, our files CANVAS.C (see Figure 9) and MEMMGR.C (see Figure 13) are designed to compile on either environment using the flag #ifdef MAC. But as Rex Jaeschke points out in his new book, Portability and the C Language (Hayden Books, 1988), p. 9, "A general misconception is that exactly the same source code files must be used on all targets such that the files are full of conditionally compiled lines. This need not be the case at all." There ought to be two separate CANVAS.C files-one for the Mac and one for Windows. There is one serious problem with our use of #ifdef MAC. We have assumed that we are either compiling for the Mac or for Windows. But what about the OS/2 Presentation Manager, X Windows, or Display PostScript(R) systems like NeWS, or the NeXT computer? In actual code, you would want multiple versions of a file like CANVAS.C, one for each environment. Additionally, rather than #ifdef MAC, we might have used the various compilers' predefined symbols, such as THINK_C for Lightspeed C 3.0 or the Macintosh symbol defined by Apple's MPW C compiler. The code that we show in CANVAS.C and MEMMGR.C is filled with #ifdef MACs, and while it should be done with a separate CANVAS.C or MEMMGR.C for each environment, remember that there are no #ifdef MACs at our application level. While we're proposing a separate environment-specific module for each environment, also remember that the external interface does not change. There might be a CANVAS.C for the Mac, a CANVAS.C for Windows, and another for X Windows, but there need only be one CANVAS.H. We set up a common interface to disparate systems, using C files such as a Modula-2 IMPLEMENTATION module and H files such as a DEFINITION module. There is an argument against using layers for reasons of portability: efficiency. But note that the same argument can be made against using C rather than assembler. Writing raw Windows or Mac code is in a way equivalent to writing in assembler. Pictures and Metafiles It is true that, now that we're going through the extra layer of CANVAS.C, our stop sign code runs a little slower. Each time we call move_to or line, the first parameter is checked to ensure that it's a valid HCANVAS. Before we can call the underlying Mac or Windows call, we must extract the appropriate field from the CANVAS data structure. It would be preferable to have all this validation the first time we call move_to or line, without continual checking every time we get an updateEvt or WM_PAINT message. If only there was some way to compile our CANVAS calls when the program starts up, and then play back the compiled object whenever we get an updateEvt or WM_PAINT. The Macintosh has a very general facility for doing just that: pictures. Between calls to OpenPicture and ClosePicture, any QuickDraw calls are compiled into a GrafPort's picSave field, instead of being drawn to the screen. The picture can be displayed by calling DrawPicture. When you're done with it, the picture's memory is freed by calling KillPicture. Similarly, Windows has metafiles. Between calls to CreateMetaFile and to CloseMetaFile, GDI calls can be sent to a metafile HDC instead of to a visible HDC on the screen. The metafile can be drawn by calling PlayMetaFile. To free the memory occupied by a metafile, call DeleteMetaFile. In Figure 9, the CANVAS open_picture, close_picture, draw_picture, and kill_picture functions provide a common interface to Mac pictures and Windows metafiles. In Figure 10, StopSign uses these routines, creating the picture once, but drawing it every time. This presents an interesting question. Since the CANVAS (GrafPort or DC) onto which we are drawing the picture can change in size, how does our scaling take place? We've saved QuickDraw or GDI calls into a picture when the CANVAS was one size, but when we play the picture back the CANVAS size may have changed. As we mentioned during the discussion of scaling in our second version, Mac pictures have a picFrame which is scaled to a destination rectangle when you call DrawPicture. This is similar to the Windows MM_ANISOTROPIC mapping mode, in that there is arbitrary scaling in both dimensions. In Windows, the same metafile can be drawn into any mapmode/viewport/window configuration. As Charles Petzold explains in Programming Windows (Microsoft Press, 1988), p.628, the contents of a metafile "are simply units. They will take on meaning only when you play the metafile." What is displayed when you play a metafile depends, not on the configuration when you saved the file, but on the configuration when you play it back. Scaling also works here. We have made Windows metafiles seem similar to Mac pictures. A picture can contain any QuickDraw calls, but only a subset of GDI calls can be compiled into a metafile; in particular, no Getxxxx functions can go into a metafile, and this was the real reason for removing the call to GetCurrentPosition from Line and Move in our fourth version. Since Windows is more restrictive in this situation, we'll have to restrict ourselves on the Mac as well if we want our code to be portable. We feel that accepting these restrictions is better than the other choice, as taken in the XVT Toolkit: that of using Windows bitmaps rather than metafiles as the analog for pictures. In addition to the drawing opcodes that you usually put into a picture on the Mac, you can also use the PicComment function to introduce arbitrary data into a picture. One might, for example, put PostScript commands in a picture comment. They are called comments because they are usually ignored by DrawPicture. However, by setting the commentProc field of the grafProcs field of a GrafPort, you can hook into the stream of PicComments and process them [see Scott Knaster, Macintosh Programming Secrets (Addison-Wesley, 1988), pp. 174-181]. Windows doesn't have anything exactly like this. Using EnumMetaFile to enumerate all the records within a metafile and PlayMetaFileRecord to play back an individual GDI opcode, you can define your own metafile processing. Windows metafiles can easily be stored on disk and shared between programs. Note that PostScript and the Apple(R) LaserWriter(R) are fully supported by Windows, but in a manner different from the LaserWriter driver's use of PicComments. Proof-by-Existence We can agree that an intermediate layer between an application and its environment is a useful mechanism for providing portability. The tricky part is the detail involved in implementing this layer. Windows and the Mac Toolbox are two very complex and rich environments, each with its own idiosyncrasies. In each environment there lurk several "gotchas" waiting to trip the unsuspecting programmer. Things that seem second nature to an experienced Mac programmer are unavailable on Windows, and vice versa. Often these discrepancies are only discovered when attempting to write a large application and port it from one environment to the other. The sample application described in this article represents our attempt to create a medium-sized generic application that raises as many potential problems as possible to provide a proof-by-existence of the validity of our approach. Time did not allow completion of a truly representative application, but the program does illustrate relevant issues regarding windows, events, graphics drawing, memory allocation, and command processing. Application Structure The application, GENAPP, is a first draft of a draw program that allows creation of graphic objects and selection and manipulation of these objects. Draw programs are often called object-oriented programs by way of contrasting them with bitmap-oriented paint programs. With paint programs the user manipulates the bits on the screen almost like physical entities. With a draw program the user manipulates abstract geometric objects such as rectangles or polygons through their screen representation or view. These geometric objects exist in some abstract mathematical space (the world coordinate space) separate from their screen representation. GENAPP highlights this distinction, between the set of abstract geometric objects and their representation on the screen display. This is achieved by providing both a graphics view and a textual view of the data model. For example, a rectangle object has both a graphic representation (the rectangular shape drawn on the screen) and a textual representation (a line of text describing the rectangle's attributes). The user can select and manipulate objects through either the graphics view or the text view. Moreover, there can be multiple graphics views and/or text views onto the same data model. Finally, the application can maintain multiple data models concurrently, in the same way word processing programs let you open multiple documents for editing at the same time. The program is split into these two major parts: ■ an environment-specific layer that serves as an intermediary between the rest of the application and the host environment, be it PC or Mac. This is in the file ENVRNMT.C (filenames have been truncated to eight characters or less because of MS-DOS requirements). ■ an environment-independent core section that represents the heart of the application. In a real-life substantial application, this might represent many modules and tens of thousands of lines of code. In our example, it is one file called APPLCATN.C, containing many of the components of larger applications. The structure of the sample application parallels in miniature the architecture of many illustration, word-processing, page layout, and CAD/CAM programs. The major elements of the environment-independent section (APPLCATN.C) are as follows: ■ The document manager: a document is an entity that contains a single data model and any number of views onto that data model. ■ The view manager: a view maintains a consistent screen representation of a document's data model. There can be either graphics or text views. A view is closely linked with a CANVAS. ■ The data model manager: a data model is owned by a document and contains a collection of objects. In our toy application, this is the only entity that is dynamically allocated via the memory allocation routines provided by ENVRNMT.C. ■ The object manager: implements operations on individual objects, such as drawing or highlighting an object or changing its attributes. Many of these operations are carried out by calling routines in ENVRNMT.C ■ The application-level event loop: requests application-level abstract events (APPEVENTs) from the environment-specific layer (which has its own internal event loop as well). Depending on the type of event received, calls are made to appropriate application-level modules such as the view manager or document manager to handle these events. ■ The menu command dispatcher: called by the application event loop upon receipt of a COMMAND event. The header file APPLCATN.H contains function prototypes for all the routines of the managers listed above. It is therefore a useful summary of what has been implemented and what has been left out. Environment-Specific Layer The file ENVRNMT.C contains environment-specific modules to deal with the following functions: ■ Memory allocation: functions to allocate relocatable chunks of memory, to resize these chunks, to lock them and get a pointer to the object's contents, to unlock an object, and finally to deallocate or dispose of an existing memory block. ■ Menu handling: environment-specific menu initialization, as well as handling of the system menu (known on the Mac as the Apple menu, which contains coresident desk accessory programs). ■ Event processing: the function GetAppEvent, which handles all environment-specific events and transforms them as appropriate into application-level events. ■ Window management: functions to create a physical window on the screen display (controlled by our abstract entity known as a CANVAS), and functions to activate, resize, update, and move these physical windows. The internal CANVAS data structures are maintained in consistency with the physical windows in the environment. ■ Graphics rendering: these functions are closely linked with the window management functions above by means of the CANVAS entity. The functions draw basic graphic shapes on the screen (rectangle, line, ellipse, and text), and maintain a coordinate system which is independent of the screen aspect ratio. ■ Environment initialization: on the Mac, initialize the appropriate managers. Under Windows, register the application window classes. The key factor is that the external interface to this environment- specific code is, like our earlier CANVAS code, environment-independent. What's Missing Due to publication deadlines, large areas of functionality provided by the Mac and Windows are completely ignored by GENAPP and its environment layer. These include all functions having to do with: printing, reading and writing of files, putting up dialog boxes, dealing with scroll bars and scroll events, and tracking the mouse to interactively resize an object. With a few exceptions, it is our opinion that these functional areas are conceptually similar enough that adding them to the existing environment/application framework can be a largely straightforward (albeit lengthy) exercise for the reader. Still, there are probably enough gotchas for the exercise to remain interesting. One messy area is the tracking of the mouse, for interactively resizing or rubberbanding a graphic object. An approach that should prove viable here is the one used by the Mac TEClick, TrackGoAway, or DrawWindow routines. In all these cases, the mouse click is used to initiate an internal mouse tracking process that continues until the mouse button is released. The actual rendering of the graphic object can then be implemented by an application- supplied callback routine. GENAPP provides a skeleton for adding this kind of functionality. Also missing from the sample application is a lot of detail on existing sections in ENVRNMT.C. For example, the memory management section has no function that returns the size of a memory block. Adding one would be trivial for both the Mac and Windows (through calls to Mac GetHandleSize or Windows GlobalSize). It happens that our sample application does not need this particular function. Our section on graphics rendering only supplies a few basic primitives (for rectangle, ellipse, and text). Both the Mac and Windows have many more drawing primitives that can be mapped to each other. One known area of difficulty is the treatment of fonts and text attributes. Windows has a complex largely undocumented font selection method which is both very different from that on the Mac, and substantially changed for Presentation Manager. Getting Input So far, we have examined how an application draws correct output to the screen display. This is done in a portable fashion by introducing an intermediate layer between the application and the GUI. An equally important subject is how the application program gets its input from the user. Programmers writing nongraphics applications formerly had to deal with characters entered at the keyboard, or perhaps coming in from a serial port. As Mac programmers know, the Mac environment dramatically extended the range of possible inputs, to include not just keystrokes, but also mouse actions (mouse up, mouse down) and other possible inputs (network, device driver, and internally derived inputs). Because of the high degree of interactivity between program and user, it became useful to conceive of an application's structure as passive and event-driven, rather than as active and data-oriented (as found, for example, in old-fashioned batch utilities that transform or filter simple streams of data). Unlike batch-oriented programmers, Mac programmers are familiar with the concept of structuring an application as a passive event loop that waits for and responds to a variety of events. These events include inputs from the user as well as internally derived inputs such as window activation and window updating events. To encompass all the information an application would need about an event, the Macintosh defines the EventRecord data structure, and Windows defines the similar MSG data structure. Both structures include what type of event occurred (event.what on the Mac, msg.message under Windows), the time it happened (event.when or msg.time), as well as where the mouse was (event.where or msg.pt). The Mac and Windows are very similar here, but the terminology may be a little confusing. While Mac events are called messages in Windows, note that the Mac EventRecord also refers to a message. This is the extra information that comes along with an event; its meaning differs depending on event.what. For instance, if (event.what == keyDown), then event.message will contain the ASCII code and key code. Under Windows, this same type of information is kept in the fields wParam and lParam (see Figure 11). Many Messages Most of the changes from Mac events to Windows messages involve making familiar concepts more flexible and more general. Inside Macintosh defines eleven types of system events. By way of contrast, the standard Windows header file defines 96 types of messages. What do these 96 messages consist of? ■ Twenty types of mouse messages (rather than two mouse event types on the Mac). These types are necessary to handle one, two, and three button mice. They also treat a double-click as its own type, rather than relying on the application program to detect it (assuming that the window has been registered as one accepting double-clicks). In addition, there is a parallel set of mouse messages dealing with events in the non-client area of a window (known to Mac programmers as what lies outside the inContent part of the window: title bar, close box, etc). Unlike the Mac, where programs have to query the environment with GetMouse calls during null events, Windows considers simple movement of the mouse to be sufficiently interesting to merit its own WM_MOUSEMOVE message. Note that the WM prefix stands for Window Message, as every MSG includes the window for which it is intended. ■ Twelve types of keystroke messages (rather than three on the Mac). Some of these deal with system keys, which are distinguished from other keys. ■ Fifteen types of clipboard messages (rather than none on the Mac). These messages inform the application of changes in the content of the clipboard, in the chain of clipboard viewers, or in various other clipboard-related requests. See "Exchanging Data Between Applications Using the Windows Clipboard," MSJ, (Vol. 3, No. 5). ■ Twenty-three types of window messages (rather than two on the Mac). Some of this population explosion results from parallel sets of messages dealing with events in the content area of a window and outside the content area. Additional message types come from the fact that the Windows environment does the equivalent of a Mac FindWindow call for you in response to a mouse-down event. This means that a user action such as clicking on the close box of a window is abstracted into a WM_CLOSE message, rather than being passed to the application as a mouse-down event requiring further interpretation and decoding. Similarly, Windows does not require the application program to worry about mouse-down events in the menu bar. These are tracked by the environment and abstracted into appropriate menu COMMAND messages. Even though Windows has a WM_NULL message, this is not nearly as important to Windows programs as a nullEvent is to Mac programs. Many Mac programs use nullEvent to handle a multiplicity of housekeeping tasks, such as menu coloring. This has been a problem for Mac programmers wanting to modify their programs to be MultiFinder-friendly. Apple now strongly discourages programmers from doing a lot of processing when handling a nullEvent. This brings up a major difference between Windows and the Mac: Windows was designed from the start to be multitasking. In practice, this is a difficult job because Windows is not an operating system, but an application-level piece of software running on top of MS-DOS(R). This affects its performance, not its design. Because Windows was designed to have multiple applications executing concurrently, it is not surprising that there is much greater support for interapplication communication and data transfer. This is done through flexible user-defined messages (much better than app1Evt on the Mac), clipboard messages, and message protocols such as Dynamic Data Exchange (DDE). An analogous situation exists on the Mac with regard to networking. The Mac was designed to support a cheap built-in local area network (the AppleTalk(R) port has always been standard on Macs, so it is not surprising that the Toolbox has a network event type). In contrast, most PC-compatible machines that Windows runs on presently lack hardware to support local area networks, so this message type is missing from Windows. Events and Messages Fortunately for the programmer, though Windows gives you 96 types of messages, it also provides a default handler for them, called DefWindowProc. This brings up a significant difference between event/message handlers on the Mac and on Windows. On the Mac, applications wait in a loop, requesting events with GetNextEvent, or with WaitNextEvent, which is used by MultiFinder-aware applications and specifying which events they are interested in handling by means of an eventMask. The following code should be very familiar to Mac programmers: while (! done) { if(GetNextEvent( everyEvent, &theEvent)) { switch (theEvent.what) { case mouseDown: doMouseDown; break; case updateEvt: doWindowUpdate; break; ∙ ∙ ∙ } } } In Windows, all programs need a callback function for each type of window they create. This callback function is known as a window procedure, or WndProc. You must call Windows first, to get it to call you, with a message loop that looks similar to the Macintosh event loop: while (GetMessage((LPMSG) &msg, NULL, 0, 0)) { TranslateMessage((LPMSG) &msg); DispatchMessage((LPMSG) &msg); } DispatchMessage is the Windows routine that decides which of the various message handling WndProcs it will call. In some ways it is similar to FindWindow on the Mac, transforming a low-level raw event, like a mouse- down, into a slightly higher-level, more abstract event, like a menu-bar click. Windows has a routine, WindowFromPoint, that does the same thing as FindWindow, but it is hardly ever used because of the all-important DispatchMessage. By the way, note that the return value of the Windows GetMessage is a Boolean value telling the application when to quit. In contrast, GetNextEvent returns a Boolean value telling the application whether or not it should handle the particular event that just passed. The message-handling WndProc on Windows (the one called by DispatchMessage) typically looks like this: long far pascal myWndProc( hWnd, message, wParam, lParam) HWND hWnd; int message; WORD wParam; LONG lParam { switch (message) { case WM_SYSCOMMAND: doSysCommand( hWnd, wParam,lParam); break; case WM_CREATE: doCreate(hWnd, wParam, lParam); break; ∙ ∙ ∙ } } As you can see, the parameters to a WndProc callback are nothing more than the key components of the Windows MSG structure shown in Figure 11. Parenthetically, note that Windows uses a Pascal calling convention similar to that on the Mac, though for different reasons. The Microsoft C compiler requires this non-K&R keyword to be in a different place from where the Macintosh C compiler looks. Note also the additional non-K&R keyword, far, which refers to how this function is addressed in memory. Object-Oriented Windows The significant difference between Mac and Windows event handling is that-as opposed to a single event stream on the Mac that contains both application-specific events as well as system-level events (desktop events such as menu-bar click) for all windows-Windows uses multiple message streams flowing to each type of application window. Windows applications, therefore, have as many message handlers as there are window types. Why did Microsoft make this change? It comes from an approach that extends the Macintosh Toolbox's modular structure toward object-oriented programming. On the Macintosh the various subsystems are controlled by managers, which generally own one particular data structure (such as an EventRecord or WindowRecord). Application programmers call Toolbox routines to manipulate these data structures. (Ignore for now the fact that Apple makes these data structures available for direct manipulation by the programmer.) In Windows, a window is not just a data structure manipulated by WindowManager routines. It is conceived as an objectlike entity of a particular type, or class, and therefore exhibits behavior associated with that class. A window's behavior is defined as its particular responses to messages it may receive. The generic or archetypal window has generic behavior that is defined by the default message handler. This default message handler (DefWindowProc) is provided by the Windows environment. Conceptually the desktop can be thought of as the root or parent window to multiple applications. Applications each have their own main window (with its associated menu bar of commands), and may have any number of subsidiary, or child windows. These windows may all be the same type (representing a document or page), or they may be various types (document window, palette window, status window, etc.). Each of these types can therefore behave differently. Similarly the system has different classes of windows for different purposes: dialog boxes, alert boxes, controls, menus. When an application starts up, it registers its classes of windows with the system. It must provide a message handling procedure for each registered class. This message handler is part of a chain of message handlers, and is passed all messages that flow through from the system to that particular window type. The handler will examine the message stream for message types it is interested in, and pass all other messages to the next handler on this chain, which may simply be the default window class handled by DefWindowProc. When we said earlier that clicking on the close box of a window is abstracted into a WM_CLOSE message-rather than being passed to the application as a raw mouse-down event-this wasn't quite true. The application does see the low-level event (it's called WM_NCHITTEST, for non- client hit test), but most applications just pass it on to DefWindowProc, where it's gradually transformed into higher-level events such as WM_CLOSE. Programmers writing applications specifically for Windows have learned to use the technique called sub-classing, which modifies the behavior of some other existing window class (system or application) by chaining a message handler in front of the handler for an existing window class. This new handler needs to implement the behavior being added or changed, and can then pass other messages to the existing handler to implement the standard behavior. This is an elegant, general, and flexible scheme that unfortunately does not map very well to the more limited system on the Macintosh. In the Mac's defense, it should be noted that there are certain window features on the Mac which are not provided by Windows. For example: ■ On the Mac, you can specify at window-creation time the particular layering order (that is, front-to-back) for the window you are creating. ■ You can also, if you wish, specify a callback procedure to radically customize a window's behavior. For example, you can specify a window- frame routine that might draw elliptically shaped windows rather than rectangular ones. This type of function is called a windowDefProc on the Mac, which sounds like but has nothing to do with Windows' DefWindowProc. Writing portable applications that run on more than one environment requires either a subset of features common to both environments, or simulating the missing functionality on the lesser environment. In the case of window types, we have chosen the former approach in our sample application. The window types used by our application are very simple, and map easily to both Mac and Windows. In the case of events/messages, we have chosen an approach that simulates a small amount of functionality that is missing on the Macintosh but present under Windows. Application Events In our sample application, there is an intermediate layer that shields the core part of the program from the differences between Macintosh events and Windows messages. This is done by defining an entity known as an application event or APPEVENT. The structure of our application is similar to a Macintosh program, in that there is a single top-level event handler that loops through and processes events as they occur. The event handler gets these APPEVENTs from the environment through calls to GetAppEvent. As an example, the application-level event loop in APPLCATN.C looks something like this: while (! done) { env_GetAppEvent(&theAppEvent); switch (theAppEvent.event_type) { case CMD_EVENT: doCommand( &theAppEvent); break; case MOUSEDOWN_EVENT: doMouseDown( &theAppEvent); break; ∙ ∙ ∙ } } The above example is very similar to the Mac event loop shown earlier. The differences are in the data structure used (of type APPEVENT rather than type EventRecord), and the kinds of events processed. There are 12 different types of APPEVENTs, representing what's needed to achieve our purpose. If our sample application were to evolve into greater functionality and complexity, it is likely that additional types of events would be needed. But this would be some number less than 20, rather than the 96 types defined for Windows. The 12 APPEVENT types are shown in Figure 12. The alert reader will note that there are six types of view events, and views, while not yet discussed, are presumably analogous to windows. But the Macintosh has only two event types dealing with windows (update and activate). Where do these other types come from? Our intermediate environment-specific layer contains an internal event loop that actually handles the "real" events provided by the Macintosh or Windows environment. The particular environment-specific events are transformed by this internal handler into the more abstract APPEVENTs seen by the application. Windows, with its 96 message types, has to screen and dispose of uninteresting messages internally. The Macintosh has to handle more primitive actions (such as a mouse-down in the menu bar), and translate these into menu CMD_EVENTS similar to the WM_COMMAND messages found in Windows. Memory Management Memory allocation is one area where the Mac and Windows approaches seem extremely similar. In fact, there appears to be almost a one-to-one correlation between Mac and Windows memory functions. This close correlation masks some very significant differences that can cause major difficulties for experienced Mac programmers. However, once a Mac programmer accepts some rigid but useful constraints, and becomes aware of differences in the underlying hardware memory architecture, writing portable code can be straightforward──as the sample code in APPLCATN.C shows, calling environment-independent memory allocation primitives in ENVRNMT.C. The memory allocation code in ENVRNMT.C is basically a series of one-liners, with one line calling a Mac function and another the corresponding Windows routine. For instance, env_AllocMem allocates a relocatable memory block by calling Mac NewHandle or Windows GlobalAlloc, and env_GraspMem locks a memory block by calling Mac HLock or Windows GlobalLock. Our environment- independent memory functions are shown in Figure 13. These environment-independent one-liners contribute to the deceptive appearance of similarity. On both systems, you request a relocatable chunk of memory and receive what both systems call a handle to an object. But, as we have already said, a Mac Handle is not a Windows HANDLE. A Mac Handle is a 32-bit pointer to a pointer to the actual object in memory. A HANDLE in Windows, however, is an abstract 16-bit integer used internally by the environment to keep track of memory objects. Magic Cookie or **? Macintosh programmers have come to rely on the double-indirection property of handles in order to reference an object's content. That is, if ppo is a Macintosh handle (that is, a pointer to a pointer to an object), then the C language construct *ppo gives you the pointer to the object (po), and **ppo gives you the object's contents (o). For example: typedef struct { int first_field; int second_field; } SOME_OBJECT; Handle aHandle = NewHandle(sizeof(SOME_OBJECT)); (**aHandle).first_field = some_value; (**aHandle).second_field = another_value; In contrast a Windows programmer obtains a pointer to an object by means of a Lock request to the system. This always happens before referencing the object's content, and is then followed by a corresponding Unlock request after use: HANDLE aHandle = GlobalAlloc(GMEM_MOVEABLE, sizeof(SOME_OBJECT)); SOME_OBJECT far *ptr = (SOME_OBJECT far *) GlobalLock(aHandle); ptr->first_field = some_value; ptr->second_field = another_value; GlobalUnlock(aHandle); Although the Mac has similar functions to lock and unlock a memory object, many Mac programmers will only lock an object when repeated references are going to be made. For the quick-and-easy assignment of a value into a single field of an object, it is much more convenient to use the double- indirection method illustrated above. Referencing of a memory object's content via the "star-star" approach is an atomic or indivisible operation (at the C language statement level, though in C++ one could overload the * dereferencing operator), and therefore does not require locking an object, because the Mac Toolbox has no opportunity to shuffle memory around within that C language construct. Even when using the Mac lock/unlock functions, many Mac programmers have learned the exact circumstances under which the Mac Toolbox will shuffle memory, and therefore often use a pointer to a memory object without locking the object. This won't work in Windows. The Mac programmer has two choices for handling this discrepancy between the two environments. One is to simulate the feature missing from the environment by means of an intermediate layer of software at the application level. The other is to restrict oneself to the functionality that is common to both environments. In the case of Mac events and Windows messages discussed previously, it was relatively easy to simulate the Windows message types that were "missing" on the Macintosh: COMMAND_EVENT, QUIT_EVENT, etc. In the case of memory management, however, this kind of simulation is much more difficult. We once wrote a large piece of code to simulate Macintosh-style ** handles on Windows, but there was a significant loss in performance. Our current opinion is that the Mac programmer is better off learning a little discipline and limiting himself to the more restricted style of Windows memory management. Additional Discrepancies It is helpful to note that the lock/unlock functions do not nest on the Macintosh but do on Windows. Every Lock statement needs to be balanced by a corresponding Unlock in Windows. On the Mac, a single Unlock on a particular object will take effect immediately, regardless of how many Lock requests have previously occurred. The Mac uses a single bit per object to maintain lock information, while Windows uses a 16-bit counter incremented and decremented by respective Lock and Unlock requests. In GENAPP the lock operations do not nest at all, so this will work on either system. Another difference is that Windows' GlobalReAlloc, unlike the Mac's conceptually identical SetHandleSize, can return a different handle than the one given at object creation time. This means that applications should have a single place where a handle's value is stored; otherwise copies of that handle that are left lying around in other data structures may become invalid when that block is resized by some other part of the program. A further difference between corresponding functions is that Windows allocates memory in units of 16 bytes. Say you ask for a 1-byte object in Windows, then later request its size with a call to GlobalSize (which is equivalent to GetHandleSize on the Mac). The size returned by GlobalSize will be 16 bytes, while on the Mac it will be 1. For certain application programs that use this value to control further processing, it would be an annoying, but resolvable, difference. Environment-Independent Memory Functions As stated earlier, you can sidestep the mismatch between Windows and Mac memory functions by using an environment-independent layer of functions common to both environments. This approach will impose some constraints on the Mac programmer. Consider the following function from the sample application in APPLCATN.C. AddGraphicsObjectToModel adds a graphics object to the previously allocated data model. Writing the code as a Mac programmer would, with ** memory references and no locking, we get the code shown in Figure 14. In AddGraphicsObjectToModel, the argument data_model is of type HMODEL, and is a handle to a collection of graphic objects. Don't confuse graphic objects (which are geometric entities like a rectangle object or an Ellipse object), with memory objects. The data_model is a memory object which was previously allocated with a call to AllocMem. The data_model, as a dynamic array of graphic objects, must grow in size in order to contain the new graphic object being added to the model. This occurs with a call to ReAllocMem, which on the Mac calls SetHandleSize and in Windows calls GlobalReAlloc. Once the data_model has grown to a larger size, our sample function calls a subordinate routine named MakeGraphicsObject to set the fields in the graphic object with attribute information (such as rectangle dimensions or whether it is round- cornered). The code in Figure 14 would need several modifications to run under Windows: ■ The data_model argument needs to be a pointer to a handle rather than a handle, because its value may change when the memory block is resized (though in this particular piece of code it won't). ■ The data_model needs to be locked and unlocked twice-first to get the value of the num_objects field (and increment this field), and second to call MakeGraphicsObject with a pointer to the new graphics object. Between these two steps, the data_model needs to be unlocked so that it can be resized. ■ The pointer to the data_model needs to be of type PMODEL rather than MODEL * due to the segmented memory architecture of the Intel CPU. Likewise, the argument to MakeGraphicsObject needs to be of type POBJECT rather than OBJECT *. The revised portable version of AddGraphicsObjectToModel is shown in Figure 15. Still More Differences There are some big differences between Windows and the Mac in their underlying operating system and CPU architecture. These differences are not the sort that manifest themselves in formal discrepancies between memory functions in one environment or the other (such as those we have been discussing until now). The manifestation is more subtle, yet differences are more significant. These hardware and operating system differences, in order of importance, are as follows: ■ the 640Kb memory limitation imposed by MS-DOS ■ the size difference between near and far pointers caused by the segmented architecture of the Intel CPU ■ the differences in byte ordering between the Motorola(R) and Intel(R) CPUs. In practice, these differences cause more problems for Mac programmers than the imprecise correlation between memory management functions. The following sections describe these differences in more detail, presenting strategies to deal with the consequent problems. The NUXI Problem Hacker folklore says that it was UNIX(R) programmers who first faced the differences in machine byte ordering between different brands of CPUs. These programmers were porting UNIX software from one hardware platform to another and found that, if you stored the word UNIX as a sequence of bytes on one machine (for example, the PDP-11 minicomputer), then moved this data to another machine that had a different memory architecture (the Sun workstation, for instance), the stored text would be printed out as "NUXI" (depending on how the memory access was made). Other literature calls this the difference between "big endian" and "little endian" machines. Given a 16-bit number (that is, two bytes worth), one CPU will store the high-order value of that number in the byte with the bigger memory address, while another CPU will do the opposite. A similar situation exists with 32-bit long integers in the storage and accessing of the high- order and low-order words of that numerical quantity. GENAPP does not encounter this problem because, in its current form, it does not support reading and writing of data models to disk, and therefore data cannot be transferred from one machine to the other. In real-world applications, we have found the following rules useful for staying out of NUXI trouble: ■ When problems occur, it is because a value was written by one method (for example, bytes), then read back on a different machine by a different method (for example, words). Avoid this. ■ Reading and writing an object a byte at a time will give the same results on both machines. ■ Reading an object's value by using the same C language construct that wrote that object's value will not cause problems (or, at least won't cause problems that can't be remedied by machine-specific header file definitions). So Near, Yet So Far The segmented architecture of the Intel CPU used by Windows machines causes different sized pointers. These two sizes of memory pointers are called near and far, and place the elusive goal of portability a little farther away from the Mac programmer. Many Mac programmers have learned to write software only on the Macintosh and often produce reckless code like the following call to the Mac Toolbox NewWindow function: WindowPtr w = NewWindow( 0, &rect, "\pUntitled", 1,0,-1,0,0); The least of the sins committed by the above code is the lack of naming conventions to give a hint as to the types of arguments to the NewWindow function. It would be nice if C let you use labels inside a function call, so that you could use named parameters: WindowPtr w = NewWindow(wStorage: 0, boundsRect: &rect, title: "\pUntitled", visible: 1, procID: 0, behind: -1, goAwayFlag: 0, refCon: 0); But even if Windows had a NewWindow function that took the same types of arguments as the Mac function, the above code will behave horribly. For one thing, since the Mac Toolbox knows only about Pascal strings, the window title "Untitled" must be converted from a C string (null-terminated string) to a P string (whose first byte contains the strlen) In this code our Mac programmer has used the Lightspeed C '\p' escape, which turns a C string into a P string. On a PC, though, Microsoft C just sees the '\p' as another character. There are Mac programmers who have more experience writing software for other machines (a VAX or Sun workstation), and have therefore a more careful approach to writing portable code. We have seen such programmers write the above function call as follows: WindowPtr w = NewWindow( NIL, &rect, CtoPstr("Untitled"), TRUE,0,-1L,FALSE,0L); These programmers will perhaps become still more careful after the jarring revelation that, in the Windows/Intel environment, not all pointers are created equal and are not interchangeable with long integers. The -1L argument above will throw the rest of the arguments out of phase on the stack. That argument, which is supposed to be a pointer to data, should instead be ((WindowPtr) -1). Likewise, many Motorola and VAX(R) programmers define NIL to be 0L in their standard header file, when it should be ((void *)0) or ((char *)0). Pointers on Intel CPUs can be 16-bit near pointers, pointing to a location in the current local address space (a memory segment up to 64Kb in size). Or they can be 32-bit far pointers, pointing to a location in some other segment in the global address space (whose size is 20-bits, or 1Mb, on the 808x family of processors, but larger on the 80x86 generation of CPUs). Pointers to program code are not necessarily the same size as pointers to program data, depending on a particular convention (called the memory model) which is chosen at compile time. Furthermore a program's static data can exist in a different data segment from the program's automatic variables. Automatic variables are allocated on a program's execution stack, which can be chosen at compile time to exist in a different memory segment from the program's static data. Memory models are defined as follows: small, medium, compact, large, and huge. These names refer to the various possible permutations of near/far pointers as they relate to a program's code and data spaces. For additional details, check out the appropriate section of the C compiler manual you are using for Windows. Before you Mac programmers get too complacent about such things as different-sized pointers, we might point out that, even with the big linear address space of the Motorola chips, Apple has gone to a great deal of trouble to simulate a segmented memory architecture for the Mac, as evidenced by the Segment Loader. Segmented memory makes a lot of sense when you want movable, discardable resources. Saluting Typedef What all this near/far pointer situation means in practice is that, when passing a particular pointer as an argument down a chain of subordinate functions, the routines further down need to be aware of the near/far ancestry of the pointer they have been passed. In some cases it might even be necessary to have two duplicate routines, one of which accepts near pointers as arguments while the other one takes the far variety. In this near/far situation, typedef assumes an importance much greater than its creators may have envisioned. In the original UNIX source code, written for a PDP-11 where ints and pointers were all the same size, typedef is not used very often even with structs. When porting to the more hostile Intel environment, it is critically important to typedef everything, especially pointers. In GENAPP, we were writing APPLCATN.C on the Mac at first, and originally typedef'd our data model as shown in Figure 16. A new data model was then dynamically allocated at run time by a call to env_AllocMem (see Figure 17). On the PC, however, we were using medium model (which defaults to 16-bit near pointers to program data, and 32-bit far pointers to program code). A problem arose because the memory allocation routines, even in medium model, allocate memory objects from the global heap and have to return 32-bit far pointers to these objects. Declaring a pointer to a data model as MODEL * meant that the pointer size defaulted to the 16-bit near size. Our solution was to create a separate type for a pointer to model as follows: typedef MODEL far *PMODEL; /* a 32-bit pointer to data model */ The above NewModel routine then had to be revised (see Figure 18). This change produced a ripple effect in our application due to the fact that the data model was used as a collection of graphic objects. The Object Manager section of APPLCATN.C contained routines to manipulate these graphic objects. Many of these routines were passed an OBJECT * as an argument. This code needed to be changed to use the type POBJECT in place of OBJECT *, where POBJECT was declared as follows: typedef OBJECT far * PMODEL; /* a 32-bit pointer to graphic object */ In addition to the unsung typedef, there is a newer C language construct that assumes great importance in this near/far pointer situation: function prototyping. Function prototyping is an enhancement to the original C language definition and is part of the proposed ANSI standard. Function prototyping and typedefs are the two most powerful weapons in the war against stray pointer bugs. In GENAPP, the header files APPLCATN.H and ENVRNMT.H contain function prototypes for every function in the program, and were invaluable in bringing both real and potential problems to our attention. Large Model Drawbacks Mac programmers, when encountering these unexpected ripple effects of near/far pointers in an existing program, may feel a sense of delight in discovering the compiler convention called large model, which establishes 32-bit pointers to both program code and program data. This closely parallels the Mac; however, there are several strong reasons not to use the large model convention: ■ The large model can cause a significant increase in program size. ■ This increase in size can have a severe impact on program performance. ■ The data segment for static data becomes fixed and unrelocatable, affecting both performance and the possibility of having multiple instances of the program running concurrently. ■ Certain tools and third-party libraries don't support large model (for example, the XVT library is only available in medium model). This just shows that utilizing the closest analogies between two systems may make your port easier in the short run but will not necessarily improve your code. 640Kb + 4Mb = 640Kb Ironically the biggest single memory-related problem that Mac programmers will encounter on Windows is not discernible anywhere in the program listing of GENAPP. This obstacle is the 640Kb memory limitation imposed by DOS. Unlike the problems already discussed, this one has no visible effect on our program code (in terms of the language syntax or constructs used). And it can be safely ignored by toy programs like GENAPP. But writers of larger real-world applications on Windows will face this issue time and again. Apple encourages Mac programmers to think of the Toolbox as if it were almost part of the hardware. The mouse tracking software on the Mac is as close to the hardware as you can get without being hardwired into the 68000 instruction set. This is why the cursor movement on even an old Mac 128 feels smoother and more responsive than the cursor motion on a 386 machine running Windows. Windows is based on lessons learned in part from the Mac, so its API is more elegant and ambitious than the Mac Toolbox. Although its programming interface is better, the resulting programs sometimes have a slightly clunky feel compared to their Mac versions. The Windows environment is, strictly speaking, an application-level program running on top of an operating system which was never designed to support it. The 640Kb that MS-DOS gives you is meant to be shared among the operating system, the windowing environment, your application program's code and data, and the code and data of any other application programs that the user will hope to run at the same time. Mac programmers are used to taking the cover off their machine, putting in 4Mb worth of memory chips and having close to 4Mb of memory show up on the Finder About box. On DOS systems, you can take the cover off the machine, put in 4Mb worth of memory chips, and you'll still have less than 640Kb available when you start Windows. Windows/386 helps alleviate much of this problem for non-Windows old apps (such as Lotus(R) 1-2-3(R)), by giving each of them their own 640Kb virtual machine, but ironically Windows apps (such as Meta Software's Design) are stuck having to share the same cramped 640Kb machine with Windows itself. Is it any wonder that long-time DOS users are excited about OS/2, which breaks the 640Kb barrier? Mac programmers may be unaware of the various kludges that let you add memory to your machine without breaking the 640Kb barrier. Expanded memory is a pretty good kludge, and you'll probably need to know about it to get your Mac program running on the PC. The ultimate kludge is the recently unveiled HIMEM.SYS, which goes through major contortions in order to provide DOS with another 64Kb. HIMEM.SYS is a sure sign that DOS is in its death throes. Forward to OS/2 and Presentation Manager! There are several practical ramifications of this particular MS-Windows constraint: ■ The usual space/time trade-off when writing programs no longer holds. Programmers on other systems are used to increasing a program's data requirements in order to improve execution speed (for example, by caching computed results for later use). On Windows, if you increase the run-time size of your application, you may invoke lots of disk swapping and memory shuffling and suffer a huge performance loss. ■ Space is always at a premium, and needs to be considered in all decisions (for example, in deciding whether to use large model or medium model). One advantage of the additional discipline imposed in writing for Windows is that memory-conserving techniques have become more valuable on the Mac with the advent of MultiFinder, as users find that 2Mb is not really very much when you are running several applications. The small tightly coded application is bound to be preferred in any environment over its fatter, slower competitors. In many other ways as well, MultiFinder is helping bring the Macintosh and Windows worlds closer together. You Can Do It! We have presented many details in this article that make it seem as if the path from the Macintosh to Windows is a dangerous one. The large PC market is inviting, but is it worth the difficulties with nonsquare pixels, eight- character filenames, and having to really lock memory handles? The PC world may seem strange compared to the simplicity of the Mac, but it is an important market, and Windows and OS/2 have done a lot to improve the quality of PC computing. Don't be misled by all the minor problems discussed here into thinking that porting Mac programs to the PC is enormously difficult. Our interest in seeing much of the Mac software migrate to the PC, made us want to include as many details as possible. On the other hand, we have only scratched the surface. We covered QuickDraw/GDI, event management, and memory management, and we presented a general approach to porting code. However such topics as printing, scrolling, the clipboard, dialog boxes, text editing, file management, and more are completely missing from this discussion. In some of these areas, the parallels between Windows and the Mac are very close, while in others the differences are sufficiently large that it should be interesting developing a higher-level interface. Finally an additional benefit you will find, is that programming for more than one environment will make you a better programmer in your favorite environment. Figure 1: Code Fragments from stop_mac.c #include "MacTypes.h" #include "Quickdraw.h" ∙ ∙ ∙ #define SCROLL_BAR_WIDTH 16 #define BORDER 5 void main() { EventRecord event; ∙ ∙ ∙ switch (myEvent.what) { case updateEvt: BeginUpdate((WindowPtr) event.message); StopSign((GrafPtr) event.message); EndUpdate((WindowPtr) event.message); break; ∙ ∙ ∙ } } void StopSign(port) GrafPtr port; { GrafPtr oldPort; Rect r; Point oldOrigin; short scale; GetPort(&oldPort); SetPort(port); r = port->portRect; /* structure copy */ r.bottom -= ((2 * BORDER) + SCROLL_BAR_WIDTH); r.right -= ((2 * BORDER) + SCROLL_BAR_WIDTH); scale = min(r.bottom - r.top, r.right - r.left) / 18; oldOrigin = topLeft(port->portRect); SetOrigin(oldOrigin.h + BORDER, oldOrigin.v + BORDER); MoveTo(5 * scale, 0); Line(8 * scale, 0); Line(5 * scale, 5 * scale); Line(0, 8 * scale); Line(-5 * scale, 5 * scale); Line(-8 * scale, 0); Line(-5 * scale, -5 * scale); Line(0, -8 * scale); Line(5 * scale, -5 * scale); MoveTo(0, 5 * scale); Line(18 * scale, 0); MoveTo(0, 13 * scale); Line(18 * scale, 0); MoveTo(4 * scale, 7 * scale); Line(-2 * scale, 0); Line(0, 2 * scale); Line(2 * scale, 0); Line(0, 2 * scale); Line(-2 * scale, 0); MoveTo(7 * scale, 7 * scale); Line(0, 4 * scale); Move(-1 * scale, -4 * scale); Line(2 * scale, 0); MoveTo(10 * scale, 7 * scale); Line(2 * scale, 0); Line(0, 4 * scale); Line(-2 * scale, 0); Line(0, -4 * scale); MoveTo(14 * scale, 7 * scale); Line(0, 4 * scale); Move(0, -4 * scale); Line(2 * scale, 0); Line(0, 2 * scale); Line(-2 * scale, 0); SetOrigin(oldOrigin.h, oldOrigin.v); SetPort(oldPort); } Figure 3: Take One of Windows Version /* from HELLO.C */ ∙ ∙ ∙ case WM_PAINT: BeginPaint(hWnd, (LPPAINTSTRUCT) &ps); #ifdef OLD_CODE HelloPaint(ps.hdc); #else StopSign(hWnd, ps.hdc); #endif EndPaint(hWnd, (LPPAINTSTRUCT) &ps); ...................................................................... /* STOP.C */ #include "windows.h" #define BORDER 5 BOOL FAR PASCAL Move(HDC hDC, int x, int y) { unsigned long curr = GetCurrentPosition(hDC); return MoveTo(hDC, LOWORD(curr)+x, HIWORD(curr)+y); } BOOL FAR PASCAL Line(HDC hDC, int x, int y) { unsigned long curr = GetCurrentPosition(hDC); return LineTo(hDC, LOWORD(curr)+x, HIWORD(curr)+y); } void StopSign(HWND hWnd, HDC hDC) { RECT r; int scale; GetClientRect(hWnd, &r); r.bottom -= (2 * BORDER); r.right -= (2 * BORDER); scale = min(r.bottom - r.top, r.right - r.left) / 18; OffsetViewportOrg(hDC, BORDER, BORDER); MoveTo(hDC, 5 * scale, 0); Line (hDC, 8 * scale, 0); Line (hDC, 5 * scale, 5 * scale); ∙ ∙ ∙ Move(hDC, 0, -4 * scale); Line(hDC, 2 * scale, 0); Line(hDC, 0, 2 * scale); Line(hDC, -2 * scale, 0); OffsetViewportOrg(hDC, -BORDER, -BORDER); } Figure 5: Take Two of Windows Version /* from STOP.C */ void StopSign(HWND hWnd, HDC hDC) { RECT r; int scale; GetClientRect(hWnd, &r); r.bottom -= (2 * BORDER); r.right -= (2 * BORDER); #ifdef USE_MM_TEXT scale = min(r.bottom - r.top, r.right - r.left) / 18; #else scale = 1; SetMapMode(hDC, MM_ISOTROPIC); SetWindowExt(hDC, 18, 18); SetViewportExt(hDC, r.right, r.bottom); #endif ∙ ∙ ∙ } Figure 6: Take Three of Windows Version /* from HELLO.C */ RECT client_rect = {0}; ∙ ∙ ∙ case WM_PAINT: BeginPaint(hWnd, (LPPAINTSTRUCT) &ps); StopSign(ps.hdc); EndPaint(hWnd, (LPPAINTSTRUCT) &ps); break; ∙ ∙ ∙ case WM_SIZE: /* more efficient way: client_rect.bottom = HIWORD(lParam); client_rect.right = LOWORD(lParam); */ GetClientRect(hWnd, &client_rect); break; ..................................................................... /* from STOP.C */ void StopSign(HDC hDC) /* NEW CODE */ { extern RECT client_rect; RECT r; int scale; r = client_rect; /* structure copy */ r.bottom -= (2 * BORDER); r.right -= (2 * BORDER); ∙ ∙ ∙ Figure 7: Windows Version, Take Four /* DRAW.H */ /* external interface to draw.c */ extern BOOL FAR PASCAL Move(HDC hDC, unsigned x, unsigned y); extern BOOL FAR PASCAL Line(HDC hDC, unsigned x, unsigned y); extern BOOL FAR PASCAL MyMoveTo(HDC hDC, unsigned x, unsigned y); extern BOOL FAR PASCAL MyLineTo(HDC hDC, unsigned x, unsigned y); #ifndef DRAW_C #define MoveTo(hDC,x,y) MyMoveTo((hDC),(x),(y)) #define LineTo(hDC,x,y) MyLineTo((hDC),(x),(y)) #endif ..................................................................... /* DRAW.C */ #include "windows.h" #define DRAW_C #include "draw.h" /* not visible outside this module */ static unsigned curr_x=0, curr_y=0; BOOL FAR PASCAL Move(HDC hDC, unsigned x, unsigned y) { curr_x += x; curr_y += y; return MoveTo(hDC, curr_x, curr_y); } BOOL FAR PASCAL Line(HDC hDC, unsigned x, unsigned y) { curr_x += x; curr_y += y; return LineTo(hDC, curr_x, curr_y); } BOOL FAR PASCAL MyMoveTo(HDC hDC, unsigned x, unsigned y) { curr_x = x; curr_y = y; return MoveTo(hDC, x, y); } BOOL FAR PASCAL MyLineTo(HDC hDC, unsigned x, unsigned y) { curr_x = x; curr_y = y; return LineTo(hDC, x, y); } Figure 8: Portable Version of Stop Sign /* from HELLO.C */ <...> #include "canvas.h" <...> /* in WinMain, after call to CreateWindow() */ new_canvas((WINDOW) hWnd); <...> /* in HelloWndProc... */ HCANVAS hCanvas; <...> case WM_PAINT: #if 1 hCanvas = find_canvas((WINDOW) hWnd); begin_update(hCanvas); StopSign(hCanvas); end_update(hCanvas); #else BeginPaint( hWnd, (LPPAINTSTRUCT)&ps ); StopSign(ps.hdc); EndPaint( hWnd, (LPPAINTSTRUCT)&ps ); #endif break; case WM_SIZE: resize(find_canvas((WINDOW) hWnd), LOWORD(lParam), HIWORD(lParam)); break; ...................................................................... /* from MACHELLO.C */ ... #define MAC #include "canvas.h" <...> /* in main(), after call to NewWindow() */ new_canvas((WINDOW) windowptr); <...> case updateEvt: hCanvas = find_canvas(event.message); begin_update(hCanvas); StopSign(hCanvas); end_update(hCanvas); break; ...................................................................... /* STOP.C-uses CANVAS facility */ /* no windows.h */ /* no MacTypes.h, no QuickDraw.h */ #include "canvas.h" #define BORDER 5 #define min(a,b) ((a)<(b)?(a):(b)) void StopSign(HCANVAS hCanvas) { short width, depth; get_size(hCanvas, &width, &depth); width -= (2 * BORDER); depth -= (2 * BORDER); set_scale(hCanvas, width, depth, 18); offset_org(hCanvas, BORDER, BORDER); move_to(hCanvas, 5, 0); line(hCanvas, 8, 0); line(hCanvas, 5, 5); ∙ ∙ ∙ move(hCanvas, 0, -4); line(hCanvas, 2, 0); line(hCanvas, 0, 2); line(hCanvas, -2, 0); offset_org(hCanvas, -BORDER, -BORDER); } Figure 9: Some CANVAS Routines, Extracted from ENVRNMT.C /* canvas.h */ typedef unsigned HCANVAS; /* handle to canvas */ #ifdef MAC typedef void *WINDOW; /* WindowPtr */ typedef void *HPIC; /* PicHandle */ #define NEAR /**/ #else typedef unsigned short WINDOW; /* HWND */ typedef unsigned short HPIC; /* HANDLE */ #endif HCANVAS new_canvas(WINDOW window); HCANVAS find_canvas(WINDOW window); void kill_canvas(HCANVAS hCanvas); void move(HCANVAS hCanvas, short x, short y); void line(HCANVAS hCanvas, short x, short y); void move_to(HCANVAS hCanvas, short x, short y); void line_to(HCANVAS hCanvas, short x, short y); void get_xy(HCANVAS hCanvas, short *x, short *y); void resize(HCANVAS hCanvas, short x, short y); void get_size(HCANVAS hCanvas, short *x, short *y); void offset_org(HCANVAS hCanvas, short x, short y); void set_scale(HCANVAS hCanvas, short x, short y, short units); void begin_update(HCANVAS hCanvas); void end_update(HCANVAS hCanvas); void debug(char *mask, ...); void open_picture(HCANVAS hCanvas); void close_picture(HCANVAS hCanvas, HPIC *hPic); void draw_picture(HCANVAS hCanvas, HPIC hPic); void kill_picture(HPIC hPic); #define NUM_CANVASES 10 #define ILLEGAL_HCANVAS 0xFFFF #define ILLEGAL_WINDOW 0 ..................................................................... /* canvas.c-abridged version of envrnmt.c */ /* just contains graphics-related routines for Mac and Windows */ /* Andrew Schulman 20-Oct-1988 */ /* revised by Ray Valdes 23-Oct-1988 */ #ifdef MAC #include <unix.h> #include "Quickdraw.h" #include "WindowMgr.h" #else #include <memory.h> #include "windows.h" #endif #include "canvas.h" typedef struct { WINDOW window; short x,y; HPIC pic; #ifdef MAC WindowRecord wRecord; short scale; #else PAINTSTRUCT ps; /* includes HDC */ short right,bottom; #endif unsigned long user_data; } CANVAS; /* macros to simplify access to the environment block */ #define CANVAS_RIGHT(hCanvas) (canvas[hCanvas].right) #define CANVAS_BOTTOM(hCanvas) (canvas[hCanvas].bottom) #define CANVAS_X(hCanvas) (canvas[hCanvas].x) #define CANVAS_Y(hCanvas) (canvas[hCanvas].y) #define CANVAS_USERDATA(hCanvas) (canvas[hCanvas].user_data) #define CANVAS_WINDOW(hCanvas) (canvas[hCanvas].window) #define CANVAS_PIC(hCanvas) (canvas[hCanvas].pic) #ifdef MAC #define CANVAS_GP(hCanvas) ((GrafPtr) CANVAS_WINDOW(hCanvas)) #define CANVAS_WP(hCanvas) ((WindowPtr) CANVAS_WINDOW(hCanvas)) #define CANVAS_SCALE(hCanvas) (canvas[hCanvas].scale) #define CANVAS_PORTRECT(hCanvas) (CANVAS_WP(hCanvas)- >portRect) #else #define CANVAS_PAINTSTR(hCanvas) (canvas[hCanvas].ps) #define CANVAS_HDC(hCanvas) (CANVAS_PAINTSTR(hCanvas).hdc) #define CANVAS_HWND(hCanvas) ((HWND) CANVAS_WINDOW(hCanvas)) #endif CANVAS canvas[NUM_CANVASES] = {0}; /****************************************************************/ static void NEAR valid_canvas(hCanvas) HCANVAS hCanvas; { if (hCanvas >= NUM_CANVASES) debug("No such hCanvas: %u", hCanvas); #ifndef MAC /* Windows lets us do a little more error checking */ if (! IsWindow(CANVAS_HWND(hCanvas))) debug("Illegal hWnd %u in hCanvas %u", CANVAS_HWND(hCanvas), hCanvas); #endif } /***************************************************************/ HCANVAS new_canvas(window) WINDOW window; { register HCANVAS hCanvas; for (hCanvas=0; hCanvas < NUM_CANVASES; hCanvas++) if (CANVAS_WINDOW(hCanvas) == ILLEGAL_WINDOW) { CANVAS_WINDOW(hCanvas) = window; return hCanvas; } /* still here */ return ILLEGAL_HCANVAS; } /* this is just temporary until we have application event queue */ HCANVAS find_canvas(window) WINDOW window; { register HCANVAS hCanvas; for (hCanvas=0; hCanvas < NUM_CANVASES; hCanvas++) if (CANVAS_WINDOW(hCanvas) == window) return hCanvas; /* still here */ return ILLEGAL_HCANVAS; } /*******************************************************************/ #ifdef MAC #define memset(a,b,c) setmem((a),(c),(b)) #endif void kill_canvas(hCanvas) HCANVAS hCanvas; { valid_canvas(hCanvas); /* set all fields to zero */ memset(&canvas[hCanvas], 0, sizeof(CANVAS)); } /*******************************************************************/ /* since Move,Line,MoveTo,LineTo are so similar, write them with macros */ #ifdef MAC #define MAYBE_SWITCHING_PORT(hCanvas,FUNC,x,y) \ { \ if (CANVAS_GP(hCanvas) != thePort) \ { \ GrafPtr oldport = thePort; \ SetPort(CANVAS_GP(hCanvas)); \ FUNC(x * CANVAS_SCALE(hCanvas), \ y * CANVAS_SCALE(hCanvas)); \ SetPort(oldport); \ } \ else \ FUNC(x,y); \ } #else #define UPDATING_XY(hCanvas,FUNC,OP,x,y) \ { \ if (! CANVAS_HDC(hCanvas)) \ return; \ CANVAS_X(hCanvas) OP x; \ CANVAS_Y(hCanvas) OP y; \ FUNC(CANVAS_HDC(hCanvas), \ CANVAS_X(hCanvas), CANVAS_Y(hCanvas)); \ } #endif /*******************************************************************/ void move(hCanvas, x, y) HCANVAS hCanvas; short x,y; { valid_canvas(hCanvas); #ifdef MAC MAYBE_SWITCHING_PORT(hCanvas, Move, x, y); #else UPDATING_XY(hCanvas, MoveTo, +=, x, y); #endif } void line(hCanvas, x, y) HCANVAS hCanvas; short x,y; { valid_canvas(hCanvas); #ifdef MAC MAYBE_SWITCHING_PORT(hCanvas, Line, x, y); #else UPDATING_XY(hCanvas, LineTo, +=, x, y); #endif } void move_to(hCanvas, x, y) HCANVAS hCanvas; short x,y; { valid_canvas(hCanvas); #ifdef MAC MAYBE_SWITCHING_PORT(hCanvas,MoveTo,x,y); #else UPDATING_XY(hCanvas, MoveTo, =, x, y); #endif } void line_to(hCanvas, x, y) HCANVAS hCanvas; short x,y; { valid_canvas(hCanvas); #ifdef MAC MAYBE_SWITCHING_PORT(hCanvas,LineTo,x,y); #else UPDATING_XY(hCanvas, LineTo, =, x, y); #endif } void get_xy(hCanvas, x, y) HCANVAS hCanvas; short *x, *y; { valid_canvas(hCanvas); #ifdef MAC *x = CANVAS_GP(hCanvas)->pnLoc.h * CANVAS_SCALE(hCanvas); *y = CANVAS_GP(hCanvas)->pnLoc.v * CANVAS_SCALE(hCanvas); #else *x = CANVAS_X(hCanvas); *y = CANVAS_Y(hCanvas); #endif } /*******************************************************************/ #ifdef MAC #define SCROLL_BAR_WIDTH 16 #endif void resize(hCanvas, x, y) HCANVAS hCanvas; short x,y; { valid_canvas(hCanvas); #ifdef MAC /* nothing */ #else CANVAS_RIGHT(hCanvas) = x; CANVAS_BOTTOM(hCanvas) = y; #endif } void get_size(hCanvas, x, y) HCANVAS hCanvas; short *x, *y; { valid_canvas(hCanvas); #ifdef MAC { Rect r; r = CANVAS_PORTRECT(hCanvas); /* structure copy */ *x = (r.right - r.left) - SCROLL_BAR_WIDTH; *y = (r.bottom - r.top) - SCROLL_BAR_WIDTH; } #else *x = CANVAS_RIGHT(hCanvas); *y = CANVAS_BOTTOM(hCanvas); #endif } void offset_org(hCanvas, x, y) HCANVAS hCanvas; short x,y; { valid_canvas(hCanvas); #ifdef MAC if (CANVAS_GP(hCanvas) != thePort) { GrafPtr oldport = thePort; /* or use GetPort */ SetPort(CANVAS_GP(hCanvas)); SetOrigin(CANVAS_PORTRECT(hCanvas).left + x, CANVAS_PORTRECT(hCanvas).top + y); SetPort(oldport); } else SetOrigin(CANVAS_PORTRECT(hCanvas).left + x, CANVAS_PORTRECT(hCanvas).top + y); #else if (! CANVAS_HDC(hCanvas)) return; OffsetViewportOrg(CANVAS_HDC(hCanvas), x, y); #endif } void set_scale(hCanvas, x, y, units) HCANVAS hCanvas; short x,y,units; { valid_canvas(hCanvas); #ifdef MAC CANVAS_SCALE(hCanvas) = min(x,y) / units; #else if (! CANVAS_HDC(hCanvas)) return; SetMapMode(CANVAS_HDC(hCanvas), MM_ISOTROPIC); SetWindowExt(CANVAS_HDC(hCanvas), units, units); SetViewportExt(CANVAS_HDC(hCanvas), x, y); #endif } /*******************************************************************/ void begin_update(hCanvas) HCANVAS hCanvas; { valid_canvas(hCanvas); #ifdef MAC BeginUpdate(CANVAS_WP(hCanvas)); #else BeginPaint(CANVAS_HWND(hCanvas), &CANVAS_PAINTSTR(hCanvas)); /* CANVAS_HDC() is set via CANVAS_PAINTSTR() */ #endif } void end_update(hCanvas) HCANVAS hCanvas; { valid_canvas(hCanvas); #ifdef MAC EndUpdate(CANVAS_WP(hCanvas)); DrawGrowIcon(CANVAS_WP(hCanvas)); #else EndPaint(CANVAS_HWND(hCanvas), &CANVAS_PAINTSTR(hCanvas)); CANVAS_HDC(hCanvas) = 0; /* important! */ #endif } /*******************************************************************/ void open_picture(hCanvas) HCANVAS hCanvas; { valid_canvas(hCanvas); #ifdef MAC CANVAS_PIC(hCanvas) = (HPIC) OpenPicture(CANVAS_PORTRECT(hCanvas)); #else CANVAS_PIC(hCanvas) = CANVAS_HDC(hCanvas); /* save old DC */ CANVAS_HDC(hCanvas) = CreateMetaFile(NULL); #endif } void close_picture(hCanvas, hPic) HCANVAS hCanvas; HPIC *hPic; { valid_canvas(hCanvas); #ifdef MAC ClosePicture(); *hPic = CANVAS_PIC(hCanvas); #else *hPic = CloseMetaFile(CANVAS_HDC(hCanvas)); CANVAS_HDC(hCanvas) = CANVAS_PIC(hCanvas); /* restore old DC */ #endif } void draw_picture(hCanvas, hPic) HCANVAS hCanvas; HPIC hPic; { valid_canvas(hCanvas); #ifdef MAC /* perhaps use this for ANISOTROPIC scaling */ /* may have to switch grafPorts back and forth */ DrawPicture(hPic, CANVAS_PORTRECT(hCanvas)); #else PlayMetaFile(CANVAS_HDC(hCanvas), hPic); #endif } void kill_picture(hPic) HPIC hPic; { #ifdef MAC KillPicture(hPic); #else DeleteMetaFile(hPic); #endif } /*******************************************************************/ #include <stdio.h> #ifdef MAC typedef char *va_list; #define va_start(ap,v) ap = (va_list)&v + sizeof(v) #else #include <stdarg.h> #endif void debug(mask) char *mask; { char buf[255]; va_list args; va_start(args, mask); vsprintf(buf, mask, args); #ifdef MAC ParamText(CtoPStr(buf), 0L, 0L, 0L); StopAlert(RESOURCE_ID, 0L); #else MessageBox(GetActiveWindow(), buf, "Debug!", MB_OK); #endif } Figure 10: Using Pictures and Multiple Windows /* from Windows HELLO.C */ ∙ ∙ ∙ static int closed_canvases=0; ∙ ∙ ∙ char buf[80]; register int i; for (i=0; i<NUM_CANVASES; i++) { sprintf(buf, "Window #%u", i+1); hWnd = CreateWindow((LPSTR)szAppName, (LPSTR)buf, WS_TILEDWINDOW, CW_USEDEFAULT,0,CW_USEDEFAULT,0, (HWND)NULL, /* no parent */ (HMENU)NULL, /* use class menu */ (HANDLE)hInstance, /* handle to window instance * (LPSTR)NULL /* no params to pass on */ ); (void) new_canvas((unsigned long) hWnd); ∙ ∙ ∙ } ∙ ∙ ∙ case WM_DESTROY: kill_canvas(find_canvas((unsigned long) hWnd)); closed_canvases++; if (closed_canvases == NUM_CANVASES) { KillStopSign(); PostQuitMessage(0); } break; ∙ ∙ ∙ ................................................................... /* from STOP.C-version uses pictures */ ∙ ∙ ∙ static HPIC hPic=0; void StopSign(HCANVAS hCanvas) { ∙ ∙ ∙ if (! hPic) { open_picture(hCanvas); move_to(hCanvas, 5, 0); line(hCanvas, 8, 0); ∙ ∙ ∙ line(hCanvas, -2, 0); close_picture(hCanvas, &hPic); debug("picture created: %u", hPic); } draw_picture(hCanvas, hPic); ∙ ∙ ∙ } void KillStopSign() { kill_picture(hPic); debug("picture killed: %u", hPic); } Figure 11: Macintosh EventRecord vs. Windows MSG /* from EventMgr.h */ typedef struct { int what; long message; long when; Point where; int modifiers; } EventRecord; /* from windows.h */ typedef struct tagMSG { HWND hwnd; WORD message; WORD wParam; LONG lParam; DWORD time; POINT pt; } MSG; Figure 12: Environment-Independent Memory Functions NULL_EVENT QUIT_EVENT CMD_EVENT MOUSEDOWN_EVENT MOUSEUP_EVENT OPENVIEW_EVENT CLOSEVIEW_EVENT MOVEVIEW_EVENT RESIZEVIEW_EVENT ACTIVATEVIEW_EVENT UPDATEVIEW_EVENT CHAR_EVENT Figure 13: Environment-independent memory allocation functions from ENVRNMT.C. #ifdef MAC typedef char ** Handle; /* Macintosh handle */ typedef char * MEMPTR; typedef MEMPTR * MEMBLOCK; #else typedef short HANDLE; /* Windows handle */ typedef char far * MEMPTR; typedef HANDLE MEMBLOCK; #endif typedef long MEMSIZE; MEMBLOCK env_AllocMem(size) MEMSIZE size; { #ifdef MAC return (MEMBLOCK) NewHandle((Size) size); #else return GlobalAlloc(GMEM_MOVEABLE, size); #endif } VOID env_ReAllocMem(handle,size) MEMBLOCK * handle; MEMSIZE size; { #ifdef MAC SetHandleSize((Handle)*handle,(Size)size); #else *handle = GlobalReAlloc(*handle,size,GMEM_MOVEABLE); #endif } MEMPTR env_GraspMem(handle) MEMBLOCK handle; { #ifdef MAC HLock((Handle)handle); return (MEMPTR) *handle; #else return (MEMPTR) GlobalLock(handle); #endif } VOID env_UnGraspMem(handle) MEMBLOCK handle; { #ifdef MAC HUnlock((Handle) handle); #else GlobalUnlock((HANDLE) handle); #endif } VOID env_FreeMem(handle) MEMBLOCK handle; { #ifdef MAC DisposHandle((Handle) handle); #else GlobalFree((HANDLE) handle); #endif } MEMSIZE env_CompactMem(size_wanted) MEMSIZE size_wanted; { #ifdef MAC return (MEMSIZE) CompactMem(size_wanted); #else return (MEMSIZE) GlobalCompact(size_wanted); #endif } Figure 14: Nonportable Version of AddGraphicsObjectToModel OBJECTID AddGraphicsObjectToModel( data_model , object_attributes) HMODEL data_model; /* handle to a data_model */ ATTR object_attributes; /* attributes of new graphics object */ { /* id is an index into the data model, represents identifier * for newly created graphics object */ OBJECTID id; /* id of new graphics object, and post-increment * the number of objects in this data_model */ id = (**data_model).num_objects++; /* grow the data_model to contain the new graphics object */ env_ReAllocMem( (Handle) data_model, (MEMSIZE) (sizeof(int) + ((**data_model).num_objects * sizeof(OBJ)))); /* call MakeGraphicsObject() with a pointer to the * new graphics object */ MakeGraphicsObject( &((**data_model).objects[i]), object_attributes); /* return the id of the newly created graphics object */ return id; } Figure 15: Portable Version of AddGraphicsObjectToModel OBJECTID AddGraphicsObjectToModel( data_model , object_attributes) HMODEL * data_model; /* not a handle, but pointer to handle */ ATTR object_attributes; { OBJECTID id; PMODEL m; /* a pointer to data model */ /* get pointer to data_model, and lock data_model in memory */ m = (PMODEL) env_GraspMem(*data_model); /* get id of graphics object, post-increment num_objects */ id = (m->num_objects)++; /* unlock the data_model so that we can resize it */ env_UnGraspMem(*data_model); /* resize data_model, passing pointer to handle in case * the value of the handle needs to be changed by ReAlloc */ env_ReAllocMem( (MEMBLOCK *) data_model, (MEMSIZE) (sizeof(int) + ((id + 1) * sizeof(OBJ)))); /* lock the data_model again, and get a pointer to it */ m = (PMODEL) env_GraspMem(*data_model); /* call the MakeGraphicsObject() routine with pointer to * new graphics object. The argument is of type POBJECT, * rather than OBJECT *, to compensate for segmented * memory referencing on Intel CPUs. */ MakeGraphicsObject((POBJECT)&(m->objects[id]), object_attributes); /* unlock data_model */ env_UnGraspMem(*data_model); /* return id of newly created graphics object */ return id; } Figure 16: Typedef'd Data Mode typedef struct{ int num_objects; /* number of graphic objects */ OBJ objects[1]; /* variable length array */ } MODEL; typedef Handle HMODEL; /* a handle to data model */ Figure 17: Using env_AllocMem to Dynamically Allocate a New Data Model HMODEL NewModel { HMODEL hModel; /* handle to data model */ MODEL * pModel; /* pointer to data model */ hModel = (HMODEL) env_AllocMem(sizeof(MODEL)); pModel = (MODEL *) env_GraspMem(hModel); /* lock */ pModel->num_objects = 0; env_UnGraspMem( hModel ); /* unlock */ return hModel; } Figure 18: Allocating the New Pointer to Model Type HMODEL NewModel { HMODEL hModel; /* handle to data model */ PMODEL pModel; /* pointer to data model */ hModel = (HMODEL) AllocMem( sizeof(MODEL) ); pModel = (PMODEL) GraspMem( hModel ); /* lock mem object */ pModel->num_objects = 0; UnGraspMem( hModel ); /* unlock memory object */ return hModel; } Developing Applications with Common Source Code for Multiple Environments ─────────────────────────────────────────────────────────────────────────── Also see the related article: Development Tools for the Macintosh ─────────────────────────────────────────────────────────────────────────── Michael Geary☼ A tale of three systems could easily be told of software written for one system and ported to another. "It was the best of programs, it was the worst of programs..." Writing a program that will run on more than one kind of machine or operating system isn't easy, especially if it's supposed to run on Presentation Manager (hereafter referred to as PM), Windows, and the Macintosh(R). Here we have three vastly different windowing/operating environments running on three different operating systems on two incompatible hardware platforms. Yet the three have much in common. Windows and Presentation Manager share a common user interface, and the Mac(R) user interface is quite similar. This might lead one to expect──or at least hope for──some similarities in the programming environment, and there are some. The three systems do provide many of the same general features, but they are implemented in very different ways. Windows and PM aren't too far apart; even though the detailed coding isn't the same between them, PM's ancestry shows quite clearly. There is a direct correspondence between many of the functions and messages in the two, and overall program architecture is, or can be, virtually identical. There are some major differences, to be sure. Presentation Manager applications can take advantage of all the features of OS/2, including things that have been on many a Windows programmer's wish list: true preemptive multitasking, multiple threads of execution, named pipes. But PM doesn't have every feature of Windows. One glaring omission is the multiline edit control. In Windows you just set a style bit and you get a decent little text editor-the same one used in Notepad, in fact. If you want this in Presentation Manager, you'll have to code it yourself. Still if you stick to their common features, Windows and PM are similar enough to make it relatively easy to port code between them. The Mac Toolbox (that is, the set of function calls available in the Mac's ROM and System files) is a horse of a different color. Besides taking a different approach to many things, the interface is generally built at a lower level than its PC counterparts. While Mac applications are event- driven, they don't really have the message-based architecture that is one of the strengths of Windows and PM. Mac applications have to fuss around with a lot of details to do things that are simple in the other two systems. For example, it's easy to create a standard Mac window complete with a title bar, close box, and zoom box-but if you want the user to actually be able to move, size, or zoom the window, or even to close it, you've got some code to write. When the mouse-down event message comes in, the application must determine where the mouse was pressed (albeit with a simple function call) and then call the proper function to manipulate the window. In Windows and PM this is done for you by DefWindowProc or the frame window and control classes, and the application is notified of the new window status via a message which can be processed or ignored. This makes simple programs simpler, while still giving programmers the opportunity to customize this kind of window handling. This particular difference in architecture complicates the portability issue in more ways than just difficulty of programming. Some kinds of programs are impossible to write on the Macintosh except by resorting to kludges that can't be guaranteed to work in all cases. The toughest are utility programs that manipulate the windows of other applications. Writing my Tiler program was a simple job under Windows and would be just as easy under PM (notwithstanding the fact that Tiler's functionality is already part of the PM shell). All I had to do, after getting the other applications' window handles and determining where the windows should be placed, was to call SetWindowPos for every window. (In PM, just one WinSetMultWindowPos call would do them all with less screen activity.) The interesting thing is that Tiler works fine even with applications like Clock, whose window contents are size dependent. Their windows receive WM_MOVE and WM_SIZE windows, just as if the user moved or sized them, so any position- or size- dependent calculations happen as a matter of course. Several Mac applications do have tiling and other window arrangement options-but only for their own windows. I don't know of any general-purpose tiling desk accessory for the Macintosh. The Mac does have MoveWindow and SizeWindow functions, but if a desk accessory turned them loose on an application's windows, things would not go as planned. There is a big difference between these functions and their equivalents in Windows or PM. On the Mac, there is no WM_MOVE or WM_SIZE message; in fact, there isn't even a window function that these messages can be sent to. In the normal course of events the MoveWindow and SizeWindow functions are called only by the application itself, whether because of mouse clicks or a menu item. The only reason the application knows it must deal with a new window size is because the application itself just called those functions. A Tiler on the Mac would probably move the windows correctly, but it would not do any size- related calculations. For example, scroll bars would remain at their old relative positions instead of being adjusted to fit the new window size. Even MultiFinder(TM) has to resort to various kinds of tricks to get its job done. There is a revealing discussion of these kinds of struggles from an insider's point of view in Phil Goldman's "MultiFinder Revealed," in the August 1988 BYTE. Mac Advantages Just when you think you're involved in too many system details, you discover some of the Mac's greatest strengths. For example, the Mac Toolbox has hooks that let an application substitute its own code for the normal Toolbox code. Windows and PM, of course, have a set of message hook functions, and window functions themselves serve this same purpose. But on the Mac, the intercept functions, called defprocs, are found in many more places. For example, QuickDraw(TM) allows you to replace any of its lowest- level drawing functions with your own, and you can give the Control Manager a defproc to define special controls, like the speaker volume slider in the Control Panel. The Control Manager takes care of mouse tracking and other details, calling back to the defproc to do things like draw the slider and its background. In Windows or PM you would have to implement this kind of control yourself, with much less help from the operating environment. The Mac provides a higher level of support in other areas, too. There are Toolbox calls to put up standard file open and save dialogs, a far cry from the situation in PM and Windows where each application implements its own dialog for these functions. This fact made it possible for Apple to introduce the Hierarchical File System right beside the Mac's old flat file system, without breaking too many existing applications. Imagine the situation if each application had implemented its own file dialogs; there would have been no method built into them to switch directories. Since most applications used the standard file dialogs, an update automatically makes these applications reasonably current. Furthermore, this paves the way for slick little utilities like Findswell. This program adds an extra button to the standard file dialog in most applications; pressing this button lets the directories on a disk be searched for a file name. Just imagine trying to write a utility like this for Windows or PM, where each application is burdened with providing its own file open and save dialogs. No way! As good as the Mac Toolbox is, it just doesn't have either the elegance of a message-based architecture or OS/2 niceties like threads; Mac programmers find themselves getting "down and dirty" more often than PM or Windows programmers, I suspect. This brings out another strength of the Mac: its more open architecture and documentation. Windows and PM prevent dirty programming tricks by not documenting their internal architecture, providing only high-level functions to manipulate things like window structures. This may keep application programmers "well behaved," but it makes life difficult when the facilities provided don't do the job. Furthermore, this philosophy makes it hard to debug competently. The times when my code crashed in Windows, I would have given my eyeteeth to know the details of a window structure or the ever-mysterious Burgermaster, just to give me a fighting chance of figuring out what my code was doing wrong. The Mac and its documentation take an entirely different approach. Inside Macintosh documents nearly all the internal structures used by the Mac Toolbox, which are closely guarded secrets in PM and Windows. There are lots of suggestions concerning what might change in future systems, but at the top of my list is the suggestion that programmers get every bit of information that might be useful. This would bother a traditional operating system designer, because it makes it easy for the application programmer to take control at any level. It's possible to run wild with this and write a program that will break with every new system software release, but on the Mac you can dig into a lot of the internal data structures and still be "well-behaved." This openness has helped make many Mac applications and utilities possible. Windows Version 2.03 did open up the Windows architecture considerably, and Presentation Manager continues that improvement. Portability Issues The deeper an application digs into the internal workings of any system, the harder it is to port. This is true in traditional programming environments as well as the complex windowing systems we're dealing with here. In the worst case, it may be necessary to recode the entire application for each system-a brute force method of porting, but sometimes there is just no choice. One situation that can force complete recoding is the lack of a common programming language. Fortunately several good C compilers are available on the Mac, and C is the language of choice for Windows and PM applications. The Mac Toolbox was designed to be called directly from programs written in Lisa(R) Pascal, so it follows the calling sequence and data formats of Lisa Pascal. Pascal is still very popular on the Mac, but there are too many differences between the Pascal compilers on the different machines to make it a good choice for portability. If C++ were available for developing Windows, PM, and Mac applications, I'd use it instead of C. With inline functions, classes, and inheritance──and all its other object-oriented facilities──C++ is more than an incremental improvement over C for writing portable programs. Given a good library of classes, C++ could hide the underlying environments much more conveniently, and probably more efficiently, than C. But C++ compilers aren't yet available for every system we are considering, so C it is for now. Even though C doesn't help much with the differences in Application Programming Interfaces (APIs) and application structure, it does a reasonable job of letting you write code that will run on different systems. There are still a few pitfalls, and the Pascal orientation of the Mac Toolbox (and to a lesser degree of OS/2 and Windows) introduces additional problems. The "traditional" portability issues have been well covered in the literature, so I'll just touch on a few. Byte and word ordering. The Motorola(R) 68000 and the Intel(R) x86 series processors order their data in opposite directions. The 68000 is a "big endian" machine, that is, the high-order byte or word comes before the low- order one. The x86 machines are "little endian," with just the opposite data ordering. This affects any binary files meant to be portable between machines, along with data structure definitions and data access macros such as HIWORD and LOWORD in Windows. Number lengths. Is an "int" a 16-bit or 32-bit value? This problem is easily avoided by never using the native C data types and instead typedefing everything and using the new types only. A good convention is to typedef all the C types with the same name in uppercase, for example, typedef short SHORT. Then, for all code that interfaces with the underlying system, use SHORT or LONG instead of INT, where SHORT is 16 bits and LONG is 32 bits. ANSI C vs. K&R C. In the beginning everyone used Kernighan and Ritchie, but now ANSI is coming into widespread use. Microsoft(R) C, used for most Windows and PM applications, is nearly a full ANSI standard compiler (granted that the ANSI standard is still in draft form). None of the Mac C compilers are quite up to speed in this area. MPW(TM) C and Lightspeed(R) C do include some of the more useful ANSI features such as prototypes and the wonderful vsprintf function, but the implementations aren't as complete. MPW C even has the chutzpah to accept prototypes syntactically but fail to do any of the type checking implied by them. Hopefully updates will remedy these kinds of deficiencies, but meanwhile it helps to use the newer ANSI features sparingly. C vs. Pascal calling sequences. Since the Mac Toolbox was written to be called from Pascal, the popular Mac C compilers offer a choice between Pascal and C calling sequences. Microsoft C also provides both kinds of calling sequences, but its Pascal calling sequence is somewhat different from the Mac's. Both provide for the main difference, parameters being pushed left-to-right instead of right-to-left as in the C calling sequence, and the pascal keyword is used in each case to switch to the Pascal calling sequence. However, the pascal keyword has to be used in different places. In Lightspeed C and MPW C, you would declare a pascal-sequence function returning a long like this: pascal long foo( void ); Microsoft C, on the other hand, requires this: long pascal foo( void ); This difference makes the pascal keyword rather useless for porting, unless you do something really clunky like this: #ifdef MICROSOFT_C #define P1 #define P2 pascal #else /* Macintosh */ #define P1 pascal #define P2 #endif P1 long P2 foo( void ); Rather than going to these lengths, it may be best to avoid the Pascal calling sequence except for code that directly interfaces to the Mac Toolbox or to the PM or Windows API functions. This should only cause problems in code that assumes one calling sequence or the other. PMWIN.H, the PM's header file, does have a number of structure definitions and macros that assume the Pascal calling sequence. C strings vs. Pascal strings. Nearly all the Mac Toolbox functions expect strings to be in the Lisa Pascal format: a length byte followed by that many bytes of text. (Pascal strings are limited to 255 bytes.) Contrariwise, Windows, OS/2, and the C compilers on both machines use strings in the zero-terminated C format (ASCIIZ). The Mac C compilers provide various ways of dealing with this, both with conversion functions and, in some compilers, with a 'p' prefix for string constants; "pString" would be the same thing as "String", but in Pascal format. Strings loaded from resources are the native type for each environment: Pascal on the Mac, C on Windows and PM. User Interface Portability When porting code between different windowing systems, the traditional portability problems are really the least of our worries. Since programs on these systems are so API-intensive and the APIs are so different, the code that talks directly to the underlying environment must change for each system (except for the possibility mentioned above, of using macros to make PM and Windows look more alike). And the Mac's user interface, though similar to Windows and PM, is not the same, though you can get them them to be as similar as you want, depending on how much code you want to write. After all, there's a bitmap graphics engine underneath each of these systems, and you can draw whatever you want on the screen and interact with the user however you choose, regardless of whether it fits the user interface conventions of the system that you're on. The only reason I mention this is that people have tried it. Don't. On the Mac, your application should look and feel like a Mac application; on Windows and PM, it should look and feel like a Windows/PM application. Then, within those constraints, your Mac and Windows/PM versions should be as similar as possible. Trust me on this. Or if you don't trust me, read the reviews of Mac products that are "un-Mac-like," or of Windows/PM packages that have their own ideas about user interaction. That's not to say that logical extensions to either user interface aren't worthwhile, but remember that Mac users will also be using other Mac software, just as Windows and PM users will use other software for those systems. (Of course, this doesn't apply to true standalone turnkey applications, where the computer is dedicated to running specific software.) Child Windows What differences in these user interfaces do we have to consider? Most significant are child windows, the appearance and features of a standard document window, and the use of menus. Windows and PM have child windows that can be nested to any level, overlap, and be manipulated just like top-level windows. Many applications use these invisibly as a programming convenience, but the Multiple Document Interface (MDI) uses child windows visibly and extensively. The Mac has no child windows. It is possible to build them, but this involves a lot of reinventing the wheel. Fortunately the Mac's user interface convention corresponding to MDI doesn't involve child windows. In MDI, there is an application workspace window that contains the application's menu bar, and document windows are created as child windows in this window. Mac applications move everything up one level, as it were; they take over the whole screen, with the application menu bar at the top, and document windows are top-level Macintosh windows. There is a straightforward path for porting multiple-document applications between the Mac, Windows, and PM; use MDI on the latter two and the standard Mac conventions on the Mac. The Mac does have controls corresponding to all the control window classes in Windows, although not to the new frame controls added in PM. These are not implemented as child windows, but through a separate control manager and other pieces of code. Although they are more difficult to program, these controls are generally more flexible and powerful than the child window controls in Windows or PM. In particular, the text editor and list manager go far beyond their counterparts in PM and Windows. Menus Menus are quite different among the three systems. Although Windows and Presentation Manager nominally have the same user interface, some things like menus really aren't the same; they are just used in the same way by convention. Under Windows, top-level windows can have menu bars, but child windows cannot, since the child window ID (dialog ID) and the menu handle are overloaded into the same field in Windows' internal window structure. Each item in the menu bar can be an actual menu item or have a pull-down menu associated with it, and top-level windows and child windows can each have Control (System) menu icons, with the associated pull-down menus. Menu items in a menu bar or a pull-down menu can be either text or bitmaps. A Presentation Manager menu is no more than a control window class. As with any other window class, you can create a menu window at any level of the window hierarchy. Menu bars and pull-down menus are just two variations on the same menu window class. The standard frame window in PM creates a number of different menu windows to implement the standard PM/Windows interface; the menu bar is a menu window, the Control menu icon is another menu window, and the Minimize/Maximize icons make up a third menu window. (It seemed strange to me at first to think of those icons as menus, but after all a menu can include bitmaps, and these icons are just like the system menu icon, but without pull-down menus.) The various pull-down menus are themselves menu windows, invisible (and in fact children of the HWND_DESKTOP window) until needed for display. But PM menus are not limited to this standard usage-except in the user interface guidelines. The Macintosh originally had one menu bar at the top of the screen, with a pull-down menu for each menu item. By convention, the first item of every application is a tiny Apple(R) logo, whose pull-down menu is analogous in spirit if not in function to the Control menu of a Windows or PM application. Menu bar items are text only, in the standard system font (the Apple logo is simply a character in the Chicago font). Pull-down menus are normally text, but an application can supply a menu defproc to vary a pull- down menu's standard appearance. Mac applications generally adhere to this basic menu structure, but over the last few years the Macintosh menu manager has been enhanced in several ways. Pop-up menus can appear anywhere on the screen, and with a little work these can simulate PM menus. Mac applications have begun to use these, especially for multiple-choice items in dialog boxes. Hierarchical menus let pull-down menu items easily call forth an additional complete pull-down menu. Menus too long to fit on the screen now automatically scroll as needed during menu selection. Some new applications, notably HyperCard(R), have implemented tear-off menus. Especially convenient for tool or pattern palettes, these are pull-down menus that can easily be dragged to other screen locations, where they remain visible as separate palette windows. These features are great, but portability in this case leads to a lot of work or to the lowest common denominator-something like Windows crossed with the original Mac menu structure. Just as multiple-document applications can use either MDI or the Mac's menu bar, a single-document application in Windows can take its main window's menu bar and make it the menu bar on the Mac. Windows applications having menus in more than one window require some reworking. Also, applications can't count on scrolling menus; any menu that might be too long, such as a Font menu, must be checked against the screen height. If it's not going to fit, a list box or something else should be used. Aside from the menu appearance and location, the actual menu items used can be very similar between the Mac and both Windows and PM. An application should have a File menu and an Edit menu, followed by additional application-specific menus. The standard File and Edit menus are nearly identical across systems; the main exception is the different use of Command-key and Alt-key shortcuts, which is easily resolved by maintaining separate resource files for each system. And in this case that's as easy as trying to convert resource files back and forth. There are some differences in standard menus. For example, on the Mac, the About... box is at the top of the Apple menu, followed by the desk accessories and the MultiFinder menu. In most Windows and PM applications, this migrates to the bottom of the File menu, right below the Exit item that replaces the Mac's Quit item. This Exit item violates the IBM(R) Common User Access (CUA) standard, which calls for an Exit menu in the menu bar, with Exit and Resume items in its pull-down menu, and frankly, this is one violation that is necessary. Document Windows and Dialog Boxes Windows and PM have a common style for standard document windows and dialog boxes, although in each case there are other options. Modal dialog boxes on the Mac are virtually identical to their Windows and PM counterparts, but standard document windows are somewhat different. The title bar is the same, except a close box replaces the Control menu icon, providing only its Close function, and a zoom box replaces only the Maximize icon. There is nothing to replace the Minimize icon, since Mac application windows do not normally have an iconic state. There is no thick frame for window sizing──just the size box in the lower right corner. Scroll bars, if present, work identically. Interestingly, none of these differences really affect portability; they affect visual appearance and the exact mouse (or keyboard) interface, but provide essentially the same functionality except for the lack of an iconic state. As to the contents of a document window or dialog box, dialog boxes are similar across all three systems. Although recent Mac dialog boxes have started to use pop-up menus and other new interface features, the typical Windows or PM dialog box would look at home on the Mac and vice versa. This includes both modal and modeless dialog boxes. (There is no distinction between system modal and application or window modal on the Mac; under MultiFinder, modal dialog boxes are all system modal.) One feature of Windows and PM dialog boxes not often seen on the Mac is the Alt-letter shortcuts for navigating around the box. This is one case where CUA improved on the original Windows and Mac user interfaces; in a large dialog box the Alt-letters can be very convenient. They are worth implementing on the Mac, although there isn't any built-in support for them. The content of document windows is for the most part application dependent. Fortunately, the same kinds of user interface techniques are appropriate on all three systems. Users expect to find virtually the same keyboard and mouse interface on the Mac as in Windows and PM. Applications for the latter systems usually have a more substantial keyboard interface since they must work without a mouse. Portability Techniques "Vanilla" applications with relatively standard document windows, dialog boxes, and menus are the easiest to port. Specialized programs like Tiler, Spellswell(TM)──or most Mac desk accessories for that matter-are tougher. But in the context of using windows and menus in fairly standard ways, there's room to build a competitive, high quality application──vanilla with all the toppings. Packages like PageMaker(R) show that it's possible to move lots of code among these systems with a high degree of user interface refinement and compatibility. It's just a small matter of programming... But wait, we're supposed to design before we code, right? We had better figure out what approach to use. Coding in C gives us a reasonable degree of language portability, but what's the best way to deal with the different APIs involved? There isn't really one best way; it depends on the situation. Besides maintaining separate source code for each system, another kind of brute force technique is to keep common source code but use a zillion #ifdef's to isolate machine specific code. This is usually a bad bet; it ends up harder to read and maintain than separate source. Macros are a fine tool for isolating system dependencies. It's a pity C's macro processor is so limited, because it gives just a taste of the kinds of things that can be done with macros. Even with its limitations, the C preprocessor lets you define simple text-substitution macros with parameters, and that alone is plenty. The Presentation Manager Conversion Guide includes a comprehensive example of using macros to port the Cardfile program from Windows to PM. C macros don't do the whole job, so the Cardfile example also has a fair number of #ifdef's and machine-specific helper functions. The macros do reduce the number of #ifdef's enough that they don't hurt the code's readability very much. When porting between Windows and Presentation Manager, this macro approach works very well, because of the inherent similarity of the two systems. It's also very efficient. When the Mac comes into the picture, the macro scheme starts to fall apart. Because of the radically different architectures, it takes a considerable body of machine-specific code no matter how you approach the problem. The question becomes how best to divide the application between system- independent code and system-specific code, and which services the system- specific code provides to the rest. One way to look at this is to view the independent code as running in a virtual operating environment that is the same on all systems, and the specific code as implementing that environment for each system. The next question is just what that virtual operating environment is. It can provide its own unique set of APIs, which are implemented by the system- specific code on each system; or it can use the APIs of one of the systems directly, in which case code on the other systems mimics those APIs. For example, if you were starting with a Mac application and wanted to port it to PM and Windows, it would be very useful to have the same Mac Toolbox calls available on all three systems. Barring the existence of some commercial library providing these functions, there's a lot of coding involved in this, to say the least. It is true that you wouldn't have to implement every feature of the base system, only those particular features that your application actually uses. Making up new APIs for coding the application is also a valid approach, as Aldus did in writing a version of PageMaker that runs on the Mac and Windows (and shortly for Presentation Manager). As described in "Aldus: Preparing PageMaker for the Move to Windows" MSJ, (Vol. 1, No. 2), Aldus split the project into "core code" and "edge code." The core code is the same on both systems, and the edge code interfaces to the target systems, providing a common set of functions that the core code uses. There is also a commercial product that uses this method to provide portability between the Mac and Windows. The Extensible Virtual Toolkit (XVT) from Advanced Programming Institute, Ltd. provides a common interface for these two systems and is being extended to support Presentation Manager and X Windows as well. XVT provides a layer on top of each of the target environments, with its own API, its own program structure, and its own user interface. XVT uses a "vanilla with all the toppings" user interface like the one I've been describing. Windows versions use the Multiple Document Interface or a single document window, and Macintosh versions use the standard Mac multiple window interface. Dialog boxes and menus work as expected on both systems. XVT does not provide as comprehensive or flexible an environment as any of the native environments, but it provides enough to write a large variety of applications. It doesn't support some features, such as modeless dialogs, but there are plans for it to do so in future releases. It's possible to implement features not supported by XVT by directly calling the native environment, but this is nonportable. Nonetheless, it's possible to write fairly sophisticated programs using XVT alone; XVT-Draw, a drawing program written purely in XVT code, is a full-featured drawing program that shows the power available. I'm looking forward to seeing the Presentation Manager version of XVT. One benefit of XVT's approach is that it's possible to invent an API that is simpler and easier to program than any of the target systems. Although XVT was written to facilitate portability, many people have used it as a simpler way to program just one of the target systems. It also provides very high-level support for a few things commonly used in applications. For example, standard Font and Style menus can be generated simply by setting a flag, and there is a complete Help system that can be built into XVT applications. As an experiment I took the other approach to portability, picking one environment as the base and writing native code for it, then implementing portions of that environment on the other systems. Since Presentation Manager is my favorite of the three in terms of its APIs presenting the greatest amount of functionality, I used that as the base system. I also wanted a taste of what is involved in implementing an SAA-compatible programming environment. Although there are now very few applications written to SAA standards, many are coming, and it would be a plus to be able to run them on hardware other than IBM's. I also expect PM to become the single largest end-user and business environment, so it makes good sense to focus the primary attention there, and to port my application to Windows and the Mac as secondary markets. Of course this also suggests, and I would certainly like to see, that all those excellent Macintosh applications be ported to Presentation Manager. (See the accompanying article which discusses porting Macintosh applications to Windows and PM.──Ed.) Taking one system as the base environment simplifies the problem in two areas. It eliminates the need to invent common APIs, and it eases debugging of at least one version of the application, because it runs as native code on the base system. It also means that useful features of the base system become available on the others. In the case of PM as the base, this means, for example, that child windows──and window functions in general──become available on the Macintosh. When I was first programming the Mac a few years ago, I had never heard of child windows so I didn't miss them. But now that I've had a chance to use them in my Windows and PM programming, I don't know how I got along without them. Sleuth As a test case for this portability method, I used a souped-up version of a program I wrote for Windows some time ago, which I now call Sleuth. It is written as a native OS/2 application using Presentation Manager. I wrote two libraries; WinPM for Windows, and MacPM for the Macintosh that provide just enough Presentation Manager functionality to let Sleuth run on those systems with no change to its source code. Sleuth (see Figure 1 for PM source code) creates a main window that is a standard document window-movable, resizable, and scrollable horizontally and vertically. In this window, Sleuth lists all existing windows, visible and invisible, in an outline showing the hierarchical relationship of parent and child windows. Each entry in the list shows the basic information about that window: its handle, ID, class name, rectangle, and text. You can double-click any entry to show all the information Sleuth can find out about a particular window, and see the information in a new window that Sleuth opens for each entry you double-click. You can also use the cursor keys to select an entry instead and then hit Enter or select the "Show Detail" menu item. Figures 2-5☼ show Sleuth running on OS/2 and the Macintosh; note that the OS/2 version shows information about more windows than the others. That's because the native version of Sleuth captures information about every window created by Presentation Manager, regardless of the application that created them. The other versions only know about the windows created by the Presentation Manager emulator, either MacPM or WinPM, which essentially means only the windows created by Sleuth itself. While Sleuth on its native environment is useful for looking at how other applications do things, it isn't on the other systems. However, if I were implementing a "serious" application on MacPM and WinPM, the Sleuth window is the first thing I would use in debugging the rest of the program. (I would also add a message trace window as in Microsoft's Spy program; that isn't in this version of Sleuth.) Even as simple as it is, Sleuth uses a lot of Presentation Manager's functionality. Many of its features──menus, text painting, scrolling──are the same things that would be used by, say, a simple text editor. In fact, an interesting thing happened as I was implementing MacPM (Sleuth's first excursion into foreign territory). I had to sweat out a lot of code to get a small portion of Sleuth running: the event/message manager, window manager, and all sorts of utility functions such as mapping between PM and Mac coordinates. Big chunks of the PM version of Sleuth were #ifdef'd out for a long time while I just tried to get one message through a window function correctly. Once I got past a certain point, things started coming together much more smoothly. With the overall framework in place, filling in the bits and pieces became much easier. MacPM MacPM provides enough of the PM functions and messages to let Sleuth, as written for PM, run on the Macintosh. All that is required is recompiling the code on the Mac, using MacPM's .H files, and linking it with the MacPM code. Only MacPM makes any native Macintosh calls; Sleuth doesn't even know it is running on a Macintosh. MacPM makes no attempt to completely emulate Presentation Manager. It provides only those portions of PM that Sleuth actually uses. In many cases, I did try to code MacPM functions and messages the "right" way, even though Sleuth only uses some of their options, but I did take shortcuts. The prime goal was to get a particular application running. With different applications, there are different areas you have to concentrate on, but the central features──PM-style message and window management──are similar. You can build any degree of compatibility you wish into a library like MacPM, up to virtually 100 percent. The best way to understand MacPM is to follow how it is used by Sleuth. If you have the MacPM code on your Macintosh, you can trace through it using Lightspeed C's source debugger while you read this description. (Full source code for Sleuth and MacPM is available from any of the bulletin boards listed on the back page of this issue.──Ed.) Initialization As with all good PM programs, Sleuth starts out with calls to WinInitialize and WinCreateMsgQueue. These functions are in MpmInit.c. In PM, WinInitialize just initializes the heap- and atom-management functions, leaving the graphics and windowing initialization for WinCreateMsgQueue. MacPM follows the same pattern. WinInitialize (see Figure 6) does some Macintosh memory management initialization and returns a dummy HAB (anchor block handle) value. (I've cheated already. Not only have I skipped the heap and atom initialization──since MacPM doesn't yet support those functions──but by just pretending to have an anchor, I'm risking getting lost at sea. Since MacPM is linked in as part of a single application, it can get away with not really allocating anchor blocks and a few other things.) WinInitialize also opens up a resource file called MacPM.rsrc. Macintosh resources are used similarly to PM and Windows resources, but there is more flexibility about where resources are located. Some resources are found in the System file or in ROM, others in the application executable file, and still others are in resource files explicitly opened with OpenResFile. Under Lightspeed C, it's a common practice to have a separate resource file during development. This helps provide a quick turnaround; there is no need to merge the resources into the application file. When the application is ready to ship, a standalone application file can be built and the OpenResFile call removed. (Or the call can even be left in; it will try to open the file but fail, and we can just ignore the error return code.) WinCreateMsgQueue (see Figure 7) is where the real work begins. First it does the rest of the generic Macintosh initialization that you will find at the beginning of any Mac application-initializing QuickDraw, the Event Manager, the Window Manager, and assorted friends. Then it calls MpmInitSysValues to initialize the system values that WinQuerySysValue can access. These values are derived from appropriate Macintosh functions or values-hard-coded where the corresponding Mac value is an absolute number of pixels. For example, the width of a standard Mac vertical scroll bar is 16 pixels, now and forever. This is hard-coded into nearly every Mac application. Next, WinCreateMsgQueue registers all of the predefined window classes, making a call to WinRegisterClass for each one. This function is pretty straightforward. First it checks if the class is already defined, and if not it allocates a block of memory and stores the class information in it. This block includes a CLASSINFO structure as defined in PM, along with a few other pieces of information about the class. The class blocks are kept in a linked list so WinCreateWindow can search through them. In theory, MacPM should have the PM atom-management functions to search for a class name, but I took a shortcut using a "quick-and-dirty" hashing function. WinCreateMsgQueue wraps up its work by creating two predefined windows: the desktop and object windows. Presentation Manager apparently creates these windows as special cases; they don't have window classes like other windows. In MacPM, it seemed simpler to make up an additional window class, WC_DESKTOP, and then call WinCreateWindow to create these two windows as it does the others. They are still special cases in that they have no corresponding Mac windows. The desktop window lets you write to the entire screen, just as in PM. Object windows aren't really implemented in MacPM; it creates the top level object window but doesn't do much else with it. Creating a Window With WinCreateWindow (see Figure 8), found in MpmCreate.c, we are getting into the real inner workings of MacPM. As I mentioned earlier, the Macintosh has nothing corresponding to PM's hierarchy of windows. There is only a single window level, corresponding to a main window in PM. There are no child windows nor a desktop window. Nor does the Mac have the concept of a window function to which messages can be sent. Macintosh events are a rough subset of PM messages, and the Mac provides functions to find out which window a message relates to, but the interface is generally at a lower level. For example, there is no WinDefWindowProc to handle default conditions; if you don't include code to check for, say, a mouse click in the close box, then the user's going to have a tough time closing your window. Contrast this with the WM_SYSCOMMAND/SC_CLOSE message in PM or Windows, which you can handle specially if you want or just disregard and pass through to WinDefWindowProc. Since all these facilities are so fundamental to PM coding, WinCreateWindow creates a window structure that can hold the information necessary for a PM window-desktop, main (top-level), or child. There is a window function pointer, taken from the window class, along with all the WinCreateWindow information; style flags, owner and parent window handles (along with next/previous sibling and first/last child window handles), PM-style window size and location, etc. To communicate with the Macintosh world, the structure also holds a pointer (WindowPeek) to the corresponding Macintosh window structure. All child windows of a given main window have the same window pointer. For child windows that implement button and scroll bar controls, there is also a handle (ControlHandle) to the actual Macintosh control. There is also a Mac adjustment rectangle, rectAdj, to help translate PM window coordinates into the proper Macintosh coordinate space. The PM child windows (and main window) are all defined the same way they are in the real PM. Coordinates are always relative to the parent window, and the Y coordinate counts up from the bottom──unlike the Mac and Windows, where Y counts from the top down. Most Mac Toolbox functions expect coordinates to be relative to the Mac's window client rectangle. The rectAdj field contains the displacement (for all four sides) to get from the Mac window rectangle to the Mac client rectangle. With this adjustment, it's possible to convert any PM coordinate to the corresponding Mac coordinate, and vice versa. The file MpmMap.c contains several mapping functions that convert points or rectangles back and forth between Mac and PM coordinates. As you might guess, these functions are used extensively throughout MacPM. One last field in the MacPM window structure is the "window kind." There are four different kinds of windows as far as MacPM is concerned, so WinCreateWindow saves one of the following values in the window structure and then takes care of some special processing for each: ■ WK_OBJECT (object window): No special processing; not really supported in this version of MacPM. ■ WK_DESKTOP (desktop window): Save the screen boundaries and a pointer to the Mac's full-screen graphics port (WMgrPort). ■ WK_MAIN (top-level Macintosh window): Create a Macintosh window. The Mac provides several styles of windows, so we choose the best style based on the PM style flags and then call the Mac's NewWindow function to create the actual window. Not all PM styles are supported; for example, a borderless window will get a border anyway. It's possible to create a borderless Mac window, but it takes more code (in Mac terms, a window defproc), and Sleuth doesn't need one anyway. NewWindow returns a WindowPeek pointer to the Macintosh window structure; save that pointer in our window structure. ■ WK_CHILD (child window): Set up the appropriate "relative" fields──such as hwndNextSibling, hwndTopChild, etc.──for this window and its parent and siblings. This wasn't necessary for WK_MAIN windows, since we relied on the Macintosh's window list for main windows. But for child windows we have to do all this bookkeeping. At this point, the window structure is completely initialized, and WinCreateWindow sends a WM_CREATE message to the window. Note that the window is invisible at first, regardless of the WS_VISIBLE flag. Next, WinSetWindowPos is called in order to set the window position and send the WM_ADJUSTWINDOWRECT, WM_MOVE, and WM_SIZE messages. WinSetWindowPos and WinSetMultWindowPos are interesting functions we'll come back to a little later. One final detail and then WinCreateWindow's job is done. Since the window was created invisible, we now check the WS_VISIBLE flag and call WinShowWindow if necessary. This makes the window visible and sends a WM_SHOW message to the window function. Standard Windows After WinCreateMsgQueue returns, Sleuth registers its own client window class and then creates its main window with WinCreateStdWindow (see Figure 9). This function, found in MpmFrame.c, is responsible for creating a document window as well as the standard frame control windows and client window. It sounds like it needs to do a bunch of WinCreateWindow calls, and that's basically what it does. WinCreateStdWindow assigns a default window size and position for cases where WS_VISIBLE is specified. (If WS_VISIBLE isn't set, it creates the window with size and position values zeroed out, under the assumption that the application will position the window before making it visible.) Then a WinCreateWindow call creates the frame window itself. Now it's time to create the frame control windows according to the FCF_ flags passed to WinCreateStdWindow. Here we call an internal function, MpmFrameUpdate, in order to create the proper windows. MpmFrameUpdate is also called if the frame window gets a WM_UPDATEFRAME message. (However, the early PM documentation I worked with wasn't very clear about how this should operate, so it's unlikely that I implemented it correctly.) When all of this is finished, WinCreateStdWindow creates the client window, and then-if WS_VISIBLE is specified-sends a WM_FORMATFRAME message to the frame window and calls WinShowWindow in order to make it all visible. WM_FORMATFRAME tells the frame window function in MacPM to position all its child windows according to the current frame window size. Frame Window Details Some of the frame window initialization is handled inside WinCreateStdWindow, but as much as possible is parceled out to the individual window functions. In addition to the WM_UPDATEFRAME and WM_FORMATFRAME processing in MpmFnwpFrame, several of the frame controls have special initialization in their window functions. Scroll bars have the most work to do, so let's take a closer look at how they are created and positioned. The code for scroll bars and scrolling is in MpmScroll.c. The scroll bar window function, MpmFnwpScrollBar (see Figure 10), creates each Macintosh scroll bar control when it receives the WM_CREATE during WinCreateWindow. It uses a straightforward call to the Mac's NewControl function, except for one little trick. NewControl does not have a parameter by which you can tell it whether you want a vertical or horizontal scroll bar. Instead, it looks at the scroll bar dimensions, and whichever direction is larger──vertical or horizontal──is the kind of scroll bar you get. So, MpmFnwpScrollBar makes up an arbitrary rectangle to insure that NewControl creates the right kind of scroll bar. (Remember that the window may be created with zero height and width, and the Mac wouldn't know if it should be a vertical or horizontal scroll bar.) If the window is later resized, you might get the wrong kind of scroll bar if it is sized too small in either direction, but as long as window sizing has a large enough minimum bound, this isn't a problem. These "made up" dimensions aren't the correct ones, but that's no problem, because the scroll bar is still invisible. Before it is displayed, along come the WM_MOVE and WM_SIZE messages to clean things up. Their processing is straightforward, simply calling the Mac's MoveControl and SizeControl functions. With the scroll bar now properly positioned, the WM_SHOW message that comes along next can call the Mac's ShowControl function to make the scroll bar visible. One subtle point here is that a scroll bar that is created via WinCreateStdWindow actually gets the WM_MOVE and WM_SIZE messages twice. The scroll bar, along with the other frame controls, is created with a zero size during the WM_UPDATEFRAME processing. The first WM_MOVE and WM_SIZE messages are sent at that time. Next the WM_FORMATFRAME processing makes a call to WinSetMultWindowPos to set the actual positions, sending a second pair of WM_MOVE and WM_SIZE messages. This is not that uncommon a situation in PM; windows are often created with zero size then sized before being made visible, and the PM documentation recommends that WM_SIZE processing be skipped if the size is zero. The menu and system menu windows also have their work cut out for them, but it's handled a little differently. For one thing, they don't occupy any space in the Macintosh window-they are child windows only for the sake of sending and receiving messages, not because they are visible as windows. The menu bar, of course, is visible at the top of the Macintosh screen, and it certainly doesn't look like a child of the frame window. It really is a child window, however, since WinQueryWindow (hwndFrame, FID_MENU, FALSE ) will find it, and you can send messages to it and your client window will receive WM_COMMAND and other messages from it. The idea of using an invisible child window──or an invisible window of any kind──may seem strange at first. Of course, in this particular case, I had no choice; I wanted MacPM to emulate PM, but with the Macintosh menu style at the top of the screen. The invisible child window is about the only way to accomplish that. Invisible windows can be very handy in other situations, though. The mere fact that you can send messages to them and vice versa gives you an opportunity to do something approaching object-oriented programming. On the menu window code, I really cheated. In PM there's a full set of menu messages that let you manipulate menus dynamically, most of which would be simple enough to implement on the Mac──with the caveat that the Mac has only the single menu bar. Either the application can have only one window with a menu bar (as an ASSERT in MacPM enforces now), or you would have to come up with a scheme for merging the various windows' menus into the one menu bar. For that kind of application, the best approach would be to write it as an MDI window. There's a very convenient parallel between the MDI window with its menu bar and overlapping child windows, and the Macintosh screen with its menu bar at the top and overlapping windows below. In fact, as I write this I am running MindWrite and Lightspeed C on the Mac, and each one has a "Window" menu at the end, just like MDI. (The Mac applications actually call it "Windows," but that's close enough for rock 'n' roll.) Nice as this daydreaming can be, there's none of it in this version of MacPM. Instead, WinCreateStdWindow calls MpmMenuLoad (see Figure 11)──found in MpmMenu.c──if the FCF_MENU flag is set. MpmMenuLoad does some very simple, generic Mac menu initialization. It loads in the menu bar definition from the resource file, looks for an Apple menu and adds the desk accessory menu to it, then displays the menu bar. The last frame control with special initialization is the title bar. If the FCF_TITLEBAR flag is set, WinCreateStdWindow calls WinSetWindowText to pass the pszTitle parameter along to the title bar control. Next, WinSetWindowText sends a WM_SETWINDOWPARAMS message to the window in question, with the WPM_TEXT and WPM_CCHTEXT flags and the corresponding fields in the WNDPARAMS structure set. The title bar window function, MpmFnwpTitleBar, responds to WM_SETWINDOWPARAMS by calling the Mac's SetWTitle function to set the displayed window title. MpmFnwpTitleBar is also able to respond to WM_QUERYWINDOWPARAMS, getting the window title with the Mac's GetWTitle function and passing it back to the caller. The only complication in these messages is that the text strings must be converted back and forth between the C and Pascal string formats. Mac programmers working in C quickly get used to this since the Mac Toolbox functions all expect (and return) Pascal-style strings, which have no zero terminator but instead have a length byte at the front. Lightspeed C does have Pascal-style string constants for this situation, but that doesn't help in this case since Sleuth is pure PM-style C code and passes C-style strings into MacPM. The remaining frame controls have no special initialization. They will have some work later, when they receive mouse and other messages, but first, we have to create those messages and get them to their destination. The code for doing this is in MpmMsg.c. Events With MacPM, a message isn't just a message, it's an event. MacPM, just like Windows or the real PM, has event messages along with a host of other kinds of messages. I've discussed several initialization and notification messages, which any PM programmer should be familiar with. These messages go directly to the destination window function via a WinSendMsg call. This is one of the simplest functions in MacPM; other than a validity check on the window handle, all it does is make an indirect function call to the window function. This function address is a cinch to get to and call with the MYWNDOF macro: return ( *MYWNDOF (hwnd).pfnwp )( hwnd, msg, mp1, mp2 ); That's WinSendMsg at a glance-rather easy. Now for the fine print. After Sleuth creates its main window and does some other initialization, it falls into a typical PM message loop: while( WinGetMsg( hab, &qmsg, NULL, 0, 0 ) ) WinDispatchMsg( hab, &qmsg ); It happens that Mac applications are constructed around a similar main event loop: while( TRUE ) { fMine = GetNextEvent( everyEvent, &theEvent ); MyDispatchEvent( &theEvent ); } GetNextEvent is a Mac Toolbox function that returns the next event, very much like WinGetMsg. (The return value from GetNextEvent tells whether the event occurred in an application window or in a system window, that is, a desk accessory.) MyDispatchEvent isn't part of the Mac Toolbox, but a function that would be part of this imaginary application to take care of the incoming events. Before GetNextEvent picks them up, the Mac events are held in an event queue much like PM's message queue. The Mac provides events for the following conditions: mouse down and up, key down and up (and key repeat), window update and activate/deactivate. These, of course, correspond closely to PM messages. The Mac has a few other events that don't map well into any PM messages, so MacPM disregards them. For the events that have corresponding messages, things are almost straightforward. For instance, when we click the mouse in Sleuth's client window we go into the main message loop, inside the WinGetMsg function. Since WinGetMsg isn't supposed to return until it has a message for us, it sits in its own idle loop: while( ! WinPeekMsg( hab, pqmsg, hwnd, msgMin, msgMax, PM_REMOVE ) ) WinWaitMsg( hab, msgMin, msgMax ); Here's the tricky part: WinWaitMsg is the function that is finally supposed to wait for a message; and sure enough, it is indeed waiting in yet another idle loop: while( ! WinPeekMsg( hab, &qmsg, NULL, msgMin, msgMax, PM_NOREMOVE ) ); Note the difference between those two WinPeekMsg calls. The PM_REMOVE option in the first one tells WinPeekMsg to pull the message from the queue, because that's what WinGetMsg is supposed to do. The second one has the PM_NOREMOVE option, because WinWaitMsg is supposed to wait until a message is available and then return, but with the message still in the queue. So you can see that WinPeekMsg gets called somewhat redundantly during the course of a single WinGetMsg call──first, inside WinWaitMsg to find out that the message is available, then again inside WinGetMsg to actually pull the message. While this isn't very efficient, it certainly made the coding easier──once I had both options of WinPeekMsg debugged, the other message functions fell together with the simple code you see above. WinPeekMsg is where life becomes interesting. It helps that Sleuth doesn't use the filtering options of WinPeekMsg, with which you can specify the minimum and maximum message numbers along with the window handle you're interested in. The window handle would be no problem, but implementing the message numbers would be a nuisance. There is one special case internally in MacPM: WinSetActiveWindow calls WinPeekMsg specifically looking for WM_ACTIVATE messages only so it can have the WM_ACTIVATE sent to the window at the proper time. The Mac normally queues this event. WinPeekMsg handles this as a special case, setting a different event mask for the Mac Toolbox call. The event mask tells which events you want to receive. Before asking for any events, WinPeekMsg calls the Mac's SystemTask function. This yields control to desk accessories (and under MultiFinder, to other applications). Then WinPeekMsg calls either GetNextEvent or EventAvail, depending on whether you specified PM_REMOVE or PM_NOREMOVE. (EventAvail is the same as GetNextEvent except it leaves the event in the queue.) If an event is returned, WinPeekMsg then calls one of several internal functions to convert the event into the equivalent PM message, which then gets passed back to whatever called WinPeekMsg. Mousing Around Mouse movement requires special treatment. The Mac does not have a mouse- move event; it has events for mouse-down and mouse-up, but mouse movement doesn't generate an event. However, when no event is pending at all, GetNextEvent returns a special null event. Since the mouse position is passed along with every event, as in PM, whether or not the event has to do with the mouse, it's not too much trouble for WinPeekMsg to check the mouse position when it gets a null event. If the mouse has moved, a WM_MOUSEMOVE message is generated. If not, WinPeekMsg just returns FALSE to indicate no message is available. Mouse clicks are easier to handle than mouse movement-single clicks are, anyway. The Mac provides mouse-down and mouse-up events, so it's simply a matter of passing these through as PM messages. Double clicks are a little more complicated. Like many things on the Mac, there is no automatic support for these; the application must determine whether two mouse clicks are close enough in time and space to be a double-click. MacPM should compare the time and location of each mouse-down event with the most recent mouse-up event and, if they are close enough, generate a WM_BUTTON1DBLCLK message instead of a WM_BUTTON1DOWN. I say "should" because this isn't coded in this version of MacPM. Each one of these mouse events is passed through the MpmMsgMouse function (see Figure 12), which decides which window gets the message. First, it determines the proper main Mac window with a call to the Mac's FindWindow function. This not only tells which window the mouse was in, but which area, such as the size box or the close box. It also distinguishes mouse clicks in a system window (that is, a desk accessory) from those on the desktop itself. If the mouse is in one of our windows, the mouse message has to be passed to the correct child window. Some return codes from FindWindow indicate this directly, like inGrow, which indicates the mouse is in the size box. In these cases, the proper child window is determined directly from the return code (for example, the FID_SIZEBORDER window for inGrow). The inContent return code takes a little more work. The MpmMsgFindChild function does the trick here; it scans through all the child windows of the main window, looking for a match on the child window rectangles. The appropriate child window then gets the mouse message, or the frame window gets it, if no child window matches. In theory, there should be some WM_HITTEST messages flying around at this point. I didn't bother with these since things were working pretty well without them and, to be honest, I wasn't sure from the preliminary PM documentation just how WM_HITTEST was supposed to work. An application using child windows in a more complex way might need the WM_HITTEST messages. Keyboard and mouse messages require some translation to convert them to the proper PM form, but this is nothing special. Mouse messages do need translation from the Mac's coordinate system to PM's upside down system-which is a minor nuisance, but one that crops up several places in the code. The mapping functions in MpmMap.c take care of this where necessary. The Active Life The Mac's concept of active and inactive windows is very much the same as that in PM and Windows; the topmost visible main window is the active window, and all others are inactive. All windows in each of these systems are "active" in the sense that the application may display information in them, but the topmost window has a different visual appearance to highlight it. The Mac, however, places greater emphasis on the difference between active and inactive windows; the scroll bars and size box disappear on inactive windows, and a mouse click in an inactive window brings the window to the top but is otherwise disregarded. Unlike Windows and PM──in which you click in any visible portion of any window, and it does what you expect──the Mac takes a second click to "really" click the mouse in the window. This is a flaw in the original Macintosh user interface guidelines, and not all Mac applications follow it; some work like PM and Windows, making all windows respond immediately to clicks. The Finder(TM), for example, works partially this way. But I followed the majority and made mouse clicks in MacPM work according to the guidelines, even though it's less convenient. This is certainly something to consider changing; I don't think Mac users would object if their windows were more responsive. In any case, MpmMsgActivate processes the Mac's activate event, turning it into a WM_ACTIVATE message with the appropriate activate/deactivate flag. There is a little trick in this function which shows up in several others as well. Don't forget that WinPeekMsg can be called with either the PM_REMOVE or PM_NOREMOVE option. If called with PM_NOREMOVE, then later it will be called again to pick up the same message with the PM_REMOVE option. So the little trick is that, in the PM_NOREMOVE case, MpmMsgActivate does nothing more than return the PM message. When called again with the PM_REMOVE option, it goes ahead and updates the grow icon and scroll bars to reflect the new active/inactive state. Painting Update events are the trickiest ones of all. These generate WM_PAINT messages, of course, but an update event pertains to an entire Macintosh window; the Mac doesn't know we have child windows inside it. Therefore MacPM must parcel out the WM_PAINT messages to the child windows, making sure each one has the right coordinate system and clipping region. This is done in MpmPaint.c, and it's where I really started to wonder if child windows are worth it. To be honest, I could have gotten by with less work on the child window painting for this particular application. But I wanted to find out what's involved in emulating the individual WM_PAINT messages that get sent to child windows in PM. MpmMsgPaint is where it all starts, and it's simple enough; it just calls WinUpdateWindow, once it determines that the PM_REMOVE flag was set on the original WinPeekMsg call. WinUpdateWindow is the beginning of some serious cheating. In the real PM, you can call WinUpdateWindow individually on any child window, and that window will be painted immediately, while any other pending updates remain deferred. The problem here is that the Mac has BeginUpdate and EndUpdate functions that perform the same tasks as the functions WinBeginPaint and WinEndPaint in PM. However, these functions apply to the entire Macintosh window; they know nothing of child windows. I could have mimicked the PM operation exactly, but it was simpler to say that any WinUpdateWindow call updates the entire Macintosh window, including all child windows. Given this restriction, WinUpdateWindow does the BeginUpdate, paints the grow icon and all controls (for example, the scroll bars) with Mac Toolbox calls, then calls MpmPaintWindow to send WM_PAINT messages to the frame window and all child windows. Note that these are sent, not posted, since they must all be mimicked before the EndUpdate call found at the end of WinUpdateWindow. MpmPaintWindow is a simple recursive function that sends a WM_PAINT message to a window and then calls itself for each first- level child of that window, thus sending WM_PAINT to all child windows at any level of nesting. Now the fun begins. The WM_PAINT we're most interested in is the one sent to Sleuth's client window function. Like all good WM_PAINT handlers (at least ones using the cached micro-PS), this begins with a WinBeginPaint call, which returns the presentation space handle for use in painting. MacPM has to do a bit of work to create this PS, even though a MacPM presentation space barely qualifies as a nano-PS, much less a micro-PS. For that matter, MacPM's "cache" of PS's is awfully tiny; only a single presentation space is reused whenever WinGetPS (see Figure 13) is called. WinBeginPaint itself is easy enough. It just calls WinGetPS and then converts the Mac's visible region bounding rectangle to PM coordinates. (During update processing, the visible region is clipped down to the former update region.) Then WinGetPS copies the entire GrafPort structure from the specified Mac window (or the Mac desktop) into the new PS. A GrafPort is the Mac's structure that corresponds roughly to a device context in PM or Windows. In addition to copying the structure itself, a couple of CopyRgn calls are required to duplicate the visRgn and the clipRgn from the original GrafPort. Once that's done, the visRgn in this new GrafPort must be clipped down further to take the child window coordinates into account. (I could have left the visRgn alone and modified the clipRgn instead, and perhaps avoided the need to copy the GrafPort, but it seemed simpler and safer this way.) That's not hard in this version of MacPM, since it doesn't support the WS_CLIPCHILDREN and WS_CLIPSIBLINGS styles. However, it does support WS_PARENTCLIP, or more significantly, the absence of WS_PARENTCLIP. If WS_PARENTCLIP is set on a child window, the window lacks its own clipping region, therefore using that of the parent, and it's up to the child window to avoid drawing outside its boundaries. When WS_PARENTCLIP is missing, the child window needs its own clipping region, so WinGetPS takes the intersection of the parent's clipping region and the child window's rectangle and makes that the new clipping region for the child. If WS_CLIPCHILDREN and WS_CLIPSIBLINGS were supported, they would be handled next. Each would basically be a loop over the window's children or siblings, subtracting the appropriate rectangles from the window's clipping region. Since Sleuth doesn't use either of these window styles, the easiest way to implement this was to just say "no". Now you see why WinGetPS makes a copy of the GrafPort it needs. With all of this modification to the clipping region, as well as other possible modifications we would need to make if we wanted to implement additional PM features, the safest thing was to make a private copy of the whole structure. There are probably plenty of optimizations that could be made but, more importantly, it works. That's our presentation space such as it is; so now what does Sleuth do with it? Just two things: a GpiErase and a series of GpiCharStringAt calls. In MpmGPI.c (see Figure 14) these functions were remarkably simple to implement, because all the dirty work was already done. GpiErase is nothing more than a call to the Mac function EraseRgn, passing it the visRgn from our modified GrafPort in the pico-PS. GpiCharStringAt breaks down into GpiMove──which calls the Mac's MoveTo function──and GpiCharString, which is a simple call to the Mac's DrawText function. Although they took a lot of work, the simplicity of these GPI functions gives me hope that other simple GPI drawing functions would also be easy to implement on the Mac. Some of the fancier GPI stuff would, I suspect, be a lot of work since the Mac doesn't have exact equivalents. When Sleuth is done painting its client window, it calls WinEndPaint, which does nothing more than make a call to WinReleasePS, which in turn just frees up the pico-PS by discarding its regions and marking it as no longer in use. One other topic in window painting is invalidation. Sleuth does call the WinInvalidateRect function whenever it wants to force its client window to be repainted, and this function is very straightforward. It just converts the rectangle to a Mac rectangle and then calls the Mac's InvalRect function. It's then up to the Macintosh event manager to generate an update event. After all of this discussion about WM_PAINT, one strange fact is that none of the window functions in MacPM itself provide much in the way of WM_PAINT processing. Since the frame control windows all happen to be Macintosh "controls" as well (or invisible), they were all drawn back at the beginning of WinUpdateWindow, when it called the Mac's UpdtControl function. They don't have to do anything else. The only window function that handles WM_PAINT is the frame window function, MpmFnwpFrame, which does not really paint anything; the Mac Toolbox takes care of the drawing of the window frame. It does erase the client area background, by sending a WM_ERASEBACKGROUND message to the client window. The client window can either erase the background itself on this message and return TRUE, or else return FALSE and let the frame window do the erasing. This is done with a GpiErase call, bracketed by WinGetPS and WinReleasePS. This WM_ERASEBACKGROUND processing doesn't really achieve the purpose that it does in PM, which is to cause synchronous background erasing in an asynchronously painted client window──because I neatly sidestepped the whole issue of synchronous vs. asynchronous painting. MacPM uses its own strange brew when it comes to window painting. Scrolling Because Sleuth's windows are scrollable, MacPM must provide the two pieces of PM window scrolling: scroll bar tracking and messages, and the WinScrollWindow function. Scroll bars on the Mac are a pain to deal with. Not only must you position them yourself──there's no asking for and getting a standard scroll bar──but your code must get much more involved in tracking them. Unlike PM and Windows, in which a scroll bar can track itself and send messages to its owner, application code must call the proper tracking function (depending on which part of the scroll bar was clicked) and deal with more of the busywork itself. Sleuth, being a PM application, doesn't have to worry about this, but MacPM does. MpmScroll.c has all the details. When the mouse is clicked in a scroll bar, this sends a WM_BUTTON1DOWN message to MpmFnwpScrollBar. After converting the PM mouse coordinate to a Mac point, this calls the Mac's FindControl function, which identifies the Mac control containing that point, and which part of the control it is. We already know which Mac control should be returned here, since the WM_BUTTON1DOWN was here initially because it hit this particular child window, and MacPM's window structure for a control includes the Mac control handle. So this is a good place for an ASSERT macro to trap out, if for some reason FindControl returned a different control from what we expected. (You will find a lot of ASSERTs in MacPM; they are an invaluable debugging tool.) The piece of information we are really after from the FindControl call is the "part code," which tells whether the mouse was in the thumb, page up/down area, or one of the arrows. We need to know whether the mouse is in the thumb or not because we're about to call the Mac's TrackControl function, one of the more bizarre functions in the Mac Toolbox. It does the actual mouse tracking, either dragging a gray rectangle for the thumb or monitoring mouse movement and release in the arrows or page areas. TrackControl looks simple enough; you pass it the control handle, mouse position, and a pointer to a callback function that is called repeatedly until the mouse is released. The weird thing is that you have to use two different callback functions, one for thumb tracking and one for everything else, and they take different sets of parameters. The arrow and page callback is reasonably straightforward; it receives as parameters the control handle and part code, and it only gets called if the mouse is inside the proper tracking limit rectangle (that is, the arrow or the page area). In MacPM this function, MpmTrackScrollBar, just sends a WM_HSCROLL or WM_VSCROLL message to its owner, with the SB_ command code determined by the Mac's part code. Then as in PM it's up to the owner to set the new scroll bar position and do any needed window scrolling. The thumb tracking callback is where things get difficult. This function has no parameters at all; it must figure everything out by reading the mouse position each time it's called. The function has to calculate the new scroll bar value itself, based on how far the mouse has moved and on the original scroll bar value and range. This isn't a difficult calculation, but it's not trivial, and it's silly to require the application to calculate it, given that TrackControl is capable of calculating the final scroll bar position when the mouse button is released. It should have made this same calculation during tracking and passed it to the thumb tracking function as a parameter. But that's not the worst of it. You've probably noticed that when you drag a scroll bar thumb──on PM, Windows, or the Mac──it will continue to track if the mouse is within a certain distance of the scroll bar, but it jumps back to its original position if you move too far. Move it closer and it resumes tracking. TrackControl takes care of this as far as the visual aspect. But does the thumb tracking callback know whether tracking is suspended because the mouse is too far away? Well, it can just compare the new mouse position with...what? Dumb question. I forgot that the thumb tracking callback doesn't get told anything. Although I haven't disassembled TrackControl, I suspect it's calling another Toolbox function called DragGrayRgn, which tracks the mouse and drags a gray outline of a rectangle or other region. One of the parameters to this function is a "slop rectangle," which determines how far the mouse may stray before tracking is suspended. This slop rectangle would be just the thing the thumb callback needs, but it's stored somewhere on the stack, and is inaccessible to the thumb callback. Need I say that after much experimentation I finally gave up on including a thumb tracking function in MacPM? The only harm done is that the SB_SLIDERTRACK notification isn't supported, so Sleuth does not scroll its window during thumb tracking, only afterward. If you've ever wondered why nearly all Mac applications wait until thumb tracking is complete before actually scrolling their windows, this is why. At least 99 percent of the Mac programmers in the world came to the same conclusion I did: thumbs down. Even without worrying about thumb tracking, there's one more minor complication with the thumb. When you release the mouse button, TrackControl calculates the new scroll bar position and sets the scroll bar to that position. However in PM, when the SB_SLIDERPOSITION notification is sent, the new position has not yet been set. An SBM_QUERYPOS message returns the old position, but it's the responsibility of the application to set the new position. MpmFnwpScrollBar handles this by saving the old thumb position in a local static variable at the beginning of thumb tracking, at which time the local static flag fTrackThumb is also set, including the moment the SB_SLIDERPOSITION is sent. Then it's a simple matter for the SBM_QUERYPOS code to check fTrackThumb and return either the old value or the current value (in the normal case). When Sleuth follows up by sending an SBM_SETPOS message, the position has already been set, but the redundant call does no harm. Enough complaining about scroll bars. The other part of scrolling, WinScrollWindow, is a lot simpler. Sleuth calls this function when it is ready to actually scroll its client window. WinScrollWindow first checks for child windows and adjusts their positions if the SW_SCROLLCHILDREN flag is set. (There aren't any child windows inside Sleuth's client window, so it doesn't use SW_SCROLLCHILDREN.) Then it picks up the window rectangle for the window to be scrolled, converts it to Mac coordinates, and passes the resulting rectangle to the Mac's ScrollRect function. This function scrolls the bits in the window and then calculates the invalidated region. It doesn't actually invalidate the region and cause it to be repainted, it merely calculates it. So, WinScrollWindow then calls the Mac's InvalRgn function to accumulate this region into the window's update region. Movement and Sizing Even though the child windows for FID_TITLEBAR and FID_SIZEBORDER aren't actually visible in MacPM, their window functions (in MpmFrame.c) provide the same services as in PM itself. In each case, the WM_BUTTON1DOWN message will cause the appropriate mouse tracking and window movement or sizing. MpmMsgMouse directs this message to the FID_TITLEBAR or FID_SIZEBORDER window when the Mac's FindWindow function returns the inDrag or inGrow window area code. For FID_TITLEBAR, the MpmFnwpTitleBar function takes the message and converts the mouse location back to Mac coordinates, then calls the Mac's DragWindow function. This function performs the mouse tracking and actually moves the window when the button is released, if the mouse was inside a bounding rectangle that was passed to DragWindow. MpmFnwpTitleBar calculates this bounding rectangle somewhat arbitrarily, four pixels inside the actual screen size (not including the menu bar). After the button is released, MpmFnwpTitleBar checks to see if the window actually did move, and if so, sends a WM_MOVE message to its owner (the frame window). Window sizing is similar, but a little more complicated. MpmFnwpSizeBorder takes the WM_BUTTON1DOWN message in this case, and calls the Mac's GrowWindow function to do the mouse tracking. Unlike DragWindow, GrowWindow doesn't actually resize the window──it just returns the new mouse position. Then MpmFnwpSizeBorder picks up the old frame window size and position by using WinQueryWindowPos and sets the new size and position with WinSetMultWindowPos. WinSetMultWindowPos is more convenient than WinSetWindowPos in this particular case, since it takes an SWP structure that is exactly like the one filled in by WinQueryWindowPos. WinSetMultWindowPos takes care of sending the WM_MOVE and WM_SIZE messages to the frame window, which in turn sends itself a WM_FORMATFRAME message to reposition the frame controls and client window. Before calling GrowWindow, MpmFnwpSizeBorder should send the frame window a WM_QUERYMINMAXINFO message, but instead it merely sets arbitrary tracking limits: 100-pixel minimum height and width, no maximum limit. Menu Selection The menu window function MpmFnwpMenu, located in MpmMenu.c, does triple duty because there are three different "menus" to consider: the menu bar, the system menu (close box on the Mac), and the minimize/maximize icons (zoom box on the Mac). In PM each of these is a true menu and they are all treated the same. In MacPM the close box and zoom box are special cases but, to be consistent with PM, they are all handled in MpmFnwpMenu. MpmMsgMouse directs the WM_BUTTON1DOWN message to one of these three frame controls and, since they are all of class WC_MENU, MpmFnwpMenu gets the message in each case. For FID_SYSMENU (the close box), it's straightforward. MpmFnwpMenu simply calls the Mac's TrackGoAway function and, if that function returns TRUE, then it sends a WM_SYSCOMMAND message with the SC_CLOSE command to its owner, the frame window. MpmFnwpFrame passes this command message through to the client window, and Sleuth's client window function merely passes it on to WinDefWindowProc, which posts a WM_QUIT message to the application queue. FID_MINMAX (the zoom box) is almost as simple, but I cheated a little with this one. Back in MpmMsgMouse, the FindWindow function returned two different area codes for the zoom box, inZoomIn or inZoomOut, depending on whether the window is currently zoomed or not. The Mac provides a simple ZoomWindow function to do the actual zooming or un-zooming, but that function expects to receive the inZoomIn or inZoomOut code to tell it which to do. So, I simply passed that code along in the high-order word of mp2 in the WM_BUTTON1DOWN message. That word happens to be unused in PM, so I was able to get away with this nonstandard use. Given that little trick, MpmFnwpMenu calls the Mac's TrackBox function and if the function returns TRUE, sends either an SC_RESTORE or SC_MAXIMIZE message to its owner, the frame window. MpmFnwpFrame takes over from there, calling the Mac's ZoomWindow function and sending the appropriate WM_MOVE and WM_SIZE messages. There's one more thing that MpmFnwpMenu has to deal with and that is the menu bar itself. When the mouse button is pressed in the menu bar, MpmMsgMouse directs the WM_BUTTON1DOWN message to the FID_MENU control window. MpmFnwpMenu sends a WM_INITMENU message to its owner, then calls the Mac's MenuSelect function to do the mouse tracking. This function returns the Mac menu ID; the high-order word is the resource ID for the pull-down menu, and the low-order word tells which item was selected on the menu. Unfortunately, while resource IDs may be chosen as desired, the menu item number in the low-order word is simply a sequential index into the menu. This is different from PM or Windows, where you can assign your own menu IDs to each menu item. It means that if you want to move a menu item to a new position, you must update your definitions in the C code and recompile. For MacPM I could have set up a way to allow arbitrary menu ID's──the way that Windows and PM allow them──but it was simpler to just use the Mac's numbering convention, assigning menu IDs in the PM version that would match the Mac's menu IDs. The code in MpmFnwpMenu has to check one more thing. The Apple menu has a list of desk accessories as well as menu items for the application itself. If the menu selected was a desk accessory, it starts up that desk accessory with an OpenDeskAcc call. Otherwise, it sends a WM_COMMAND message to its owner, the frame window. The frame window function passes this message along to the client window, where Sleuth's window function handles it. Header Files In MacPM I followed the principle that the Presentation Manager C code should run unchanged. I would have liked to apply this to the .H files as well as the .C files, because the OS/2 .H files contain all the necessary declarations. However, because of the compiler differences I mentioned earlier, it didn't quite work out this way. I was able to use the .H files from OS/2 with Lightspeed C, but it took some manual editing to get them to compile. Here's what I had to change: Conditional compilation. ANSI C has a number of new preprocessor features that aren't present in Lightspeed C. The only one used much in the OS/2 .H files is the #if defined(name) construct. This serves the same purpose as the older #ifdef but allows checking for more than one symbol at a time. The OS/2 .H files use this in many places, in a form like the following: #if ( defined (INCL_WINFRAMEMGR) | !defined (INCL_NOCOMMON) ) Since I didn't care about the check for INCL_NOCOMMON, I edited them to work with Lightspeed C: #ifdef INCL_WINFRAMEMGR Fortunately most of the conditional compilations were already in the #ifdef form (they only test for a single symbol), so there weren't too many places that needed editing. Prototypes for function pointer declarations. Although Lightspeed C accepts function prototypes, it does not accept them for pointer types, such as: typedef VOID (PASCAL FAR *PFNEXITLIST) ( USHORT ); I took care of these with the simple expedient of commenting out the parameter declarations: typedef VOID (PASCAL FAR *PFNEXITLIST) ( /*USHORT*/ ); NEAR, FAR, and PASCAL keywords. Since Lightspeed C and Microsoft C both support the "pascal" keyword differently, I decided to forego the use of the Pascal calling sequence. Most uses of the "pascal" keyword in the .H files are coded as PASCAL (which is #define'd as pascal), so I just changed the #define to make PASCAL an empty string. The same thing applies for NEAR and FAR, since they aren't relevant in the Mac environment: #define PASCAL #define NEAR #define FAR One slightly odd thing is that OS2DEF.H fails to use these #define'd symbols in several places, falling back to the lowercase names. This doesn't seem intentional and will hopefully be corrected in a future version of this file. In the meantime, I added two more #define's to effectively disable "near" and "far": #define near #define far I couldn't do this with "pascal", since Lightspeed C needs this keyword for its own .H files, which define all the Mac Toolbox entry points. More manual editing was called for: changing "pascal" to "PASCAL" where it occurred in OS2DEF.H. The other OS/2 header files didn't have this problem. Different calling sequence (order of parameters). Deciding not to use the Pascal calling sequence led to a complication. There are several data structures and macros in PMWIN.H that assume the Pascal calling sequence. The SWP data structure, for example, is defined so that the parameters passed in a call to SetWindowPos can be directly referenced as an SWP structure. To allow this, I reversed the order of the fields in these structures, because the C calling sequence pushes the parameters in the opposite order from the Pascal sequence. Similarly, I had to change the COMMANDMSG, CHARMSG, and MOUSEMSG macros and their associated structures to work with the C calling sequence. Using the C calling sequence was a mixed blessing; it led to extra work but avoided the need to edit every single function declaration to allow for the different positioning of the "pascal" keyword. WinPM As you can see, getting Sleuth running under MacPM was a lot of work. So much, in fact, that I must confess I didn't get a working WinPM written in time for this article. In lieu of actual code, let's take a look at some of the issues involved in putting together a WinPM, starting with window classes. Because Windows resembles PM more than the Mac does, it would be handy to use existing Windows features. I would be tempted to use the window class mechanism in Windows instead of coding it from scratch, as in MacPM. I wonder, however, if this would be a good strategy. Window classes under PM are much simpler than under Windows, with many of the items in the Windows WNDCLASS structure eliminated. Instead, the frame window class and the FCF_ and FS_ options in WinCreateStdWindow handle these items. For instance, under Windows all windows of a given class have the same icon, whose handle is stored in the WNDCLASS structure. Under PM, the window class does not determine the icon; each frame window can have its own icon, and it's normally loaded in from the resource file according to the frame window ID. Depending on the application these differences could be glossed over, but Sleuth is a tough cookie with issues like this. It calls functions like WinQueryClassInfo, which have no direct counterpart in Windows. This function is a particular problem. Under Windows, there is no direct way to get at the class information given a class name. You can do it, but you must create an invisible window of the desired class, just to call GetClassWord and GetClassLong to pick up the class information. With problems like this, it may be worthwhile to keep the class support code from MacPM. You would still need to register the window classes with Windows, so there is some redundancy. Messages Message passing is another place where it's tempting to directly use Windows facilities, since window functions are so similar in the two systems. That's what the macro binding system in the Presentation Manager Conversion Guide does. The goal of attempting to maintain "pure" PM code seems to lead to too many problems. For starters, a window function in Windows takes a 16-bit window handle, and its two parameters are different lengths: 16-bit and 32-bit. In PM, all these items are 32-bit. You could fudge the declarations in PMWIN.H to make everything the same length as in Windows, but this still wouldn't handle the differing contents of the message parameters. I'd probably leave the PM window functions alone, with 32-bit window handles and two 32-bit parameters. Unlike MacPM, where all the message processing had to be coded from scratch, this could be handled by having WinPeekMsg call the Windows function PeekMessage and then do the proper conversions. WinDispatchMsg and WinSendMsg would not talk to Windows at all; they would directly call the PM window function, avoiding the problem of different function formats. For messages that are sent from Windows directly to the window function instead of being posted to the queue, the problem is a little different. Instead of giving Windows the address of the PM window function, there would have to be a common window function that Windows would call in all cases. This function would convert the parameters appropriately, find the address of the correct PM window function (probably using space in the "window extra" data), and call the PM window function. Frame Controls Window function differences notwithstanding, it makes sense to create child windows for all the frame controls, just as PM does. I'd take the same approach as in MacPM, where scroll bars are visible child windows in their expected positions, but the title bar, menu bar, and other frame controls are invisible existing only for the purpose of message passing. Windows, of course, already supports child window scroll bars as well as "standard" scroll bars. I would forget about using standard scroll bars; the only extra burden for using child window scroll bars is that their size and position must be explicitly calculated, which is not difficult. For the other frame controls, invisible child windows would make it easy to mimic their operation under PM. Painting/Scrolling With the limited use Sleuth makes of GPI, it's not hard to provide the same functionality by converting the GPI calls into the equivalent GDI calls, the way MacPM does. Also, the window painting logic is much more straightforward than under MacPM. The various shortcuts I mentioned earlier won't be necessary; Windows generates WM_PAINT messages for child windows and top- level windows the same way PM does. A thin layer is needed to take care of the differences in WinBeginPaint vs. BeginPaint, and then I expect the rest would work pretty smoothly. Application or DLL? One simplifying assumption in MacPM was that it had to support only a single application, since the MacPM code is linked in with the application. In Windows this same approach could be used, but the right way to code a WinPM would be as a dynamic-link library (DLL), so it could support more than one application. The mechanics of linking to a DLL are no problem, but a complicating factor is that WinPM would then have to handle multiple applications calling it. This would rule out using some static variables the way MacPM does. Instead, the proper data structures would have to be allocated as needed. One thing that causes problems in Windows DLLs is that they have no way to automatically find out when they are being unloaded. There's a library initialization entry point, but no library termination function. Fortunately the PM API takes care of this with its WinInitialize and WinTerminate functions. On the WinTerminate call-or on WinDestroyMsgQueue-the WinPM DLL can release any resources it has allocated for that application. A Reader Exercise Due to publication deadlines, there are several things I left unimplemented in Sleuth and in WinPM and MacPM. If you would like to try spiffing it up a bit, here are some areas you could improve. The detail windows in Sleuth (see Figure 15☼) don't exist. To make them work, you'd have to add some code, but this should require little or no change to WinPM and MacPM as far as creating and destroying these windows; the code's already there for that. It would take a little work to get double-click messages from MacPM. This isn't hard, but MpmMsgMouse would have to keep track of the time and position of the last mouse-up, and check each mouse- down to see if it looks like a double click. Then it would simply generate a WM_BUTTON1DBLCLK and not a WM_BUTTON1DOWN. The "Show Detail" menu item should do the same thing as a double-click. This means that Sleuth should have some code for selecting a line with a single click or with the cursor keys. MacPM has to be able to generate keystroke messages for this. There's an "About Sleuth..." menu item, but it doesn't do anything. Dialog boxes are an area that I omitted completely from MacPM and WinPM, so it would take some work (and careful thought) to implement them. One little thing in MacPM that you could fix is the group of items on the Edit menu, which should be grayed out when Sleuth's own windows are active. Those items are really there for desk accessories and should be enabled only when a desk accessory is active. Also, they need to call the SystemEdit function to make them actually work. There's some debugging to do with desk accessories; there are really three different environments they can run in: non-MultiFinder, MultiFinder using the DA Handler layer, and MultiFinder using the application layer. (The last choice is what happens if you hold the Option key down while starting a desk accessory under MultiFinder.) I got desk accessories working in some cases but not in all these situations. I had mentioned earlier a couple of places where the WM_QUERYMINMAXINFO message should be sent, but MacPM and WinPM just choose arbitrary rectangles instead. There's a bug in MacPM where, if a scroll bar is disabled and you click in it, the system seems to freeze until you click the mouse somewhere else ten (yes, ten) times. Then it suddenly catches up with all those clicks. Finally, if you're feeling really ambitious, you could try implementing the SB_SLIDERTRACK message. You'll find some commented-out code in MacPM where I tried to get this one working. Have fun! No Free Lunch As we've seen, there is no easy solution to the problem of portability across windowing environments as disparate as Presentation Manager, Windows, and the Macintosh. Is it worth the work? Perhaps I should ask instead if you can afford not to do it. Each of these systems represents a large (or in the case of Presentation Manager, soon to be very large) market, and people really appreciate having the same or similar software available on both the Mac and PC. The approach I've taken with MacPM and WinPM seems like a fruitful one, but I sure would have liked it if someone else had done the dirty work for me! Depending on your application, a toolkit like XVT might do a good job for you. My personal hope is that C++ with some good class libraries will make porting much easier. But I suspect that regardless of how good the tools get, the old saying will still be true: there's no free lunch. Figure 1: Complete Code Listing for the PM Version of Sleuth SLEUTH.C - Source code listing for Sleuth /* Sleuth.c */ #define INCL_DOSPROCESS #define INCL_GPI #define INCL_WIN #undef NULL #include <os2.h> #include <malloc.h> #include <stdarg.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include "Sleuth.h" /*-----------------------------------------------------------------*/ /* The display for a window looks like this in collapsed mode: */ /* */ /* Window HHHH:HHHH [id] {class} (L,B;R,T) "text" */ /* */ /* or like this in expanded mode: */ /* */ /* Window handle: HHHH:HHHH Owner window: HHHH:HHHH */ /* Class name: {class name} */ /* Window text: "text" */ /* Class style: HHHHHHHH */ /* Window style: HHHHHHHH */ /* Class function: HHHH:HHHH Window function: HHHH:HHHH */ /* Window ID: DDDDD Process ID: HHHH Thread ID: HHHH */ /* Message queue handle: HHHH Window lock count: DDDDD */ /* Window extra alloc: DDDDD */ /* Window rectangle: Left=D, Bottom=D, Right=D, Top=D */ /* {blank line} */ /* */ /* Total number of lines for one window display: 11 */ /*-----------------------------------------------------------------*/ #define LINES_PER_WINDOW 11 #define WINDOW_WIDTH 160 /* Structure of information for each window. */ #define CLASSMAX 40 #define TEXTMAX 40 typedef struct { HWND hwnd; /* Window handle */ CLASSINFO class; /* Class info */ CHAR szClass[CLASSMAX]; /* Class name */ CHAR szText[TEXTMAX]; /* Window title or contents */ ULONG flStyle; /* WS_ window style */ HWND hwndOwner; /* Owner window handle */ PFNWP pfnwp; /* Window function */ USHORT usWindowID; /* Window ID */ PID ProcessID; /* Process ID */ TID ThreadID; /* Thread ID */ HMQ hmq; /* Message queue handle */ RECTL rclWindow; /* Window rect, screen coord. */ SHORT sLockCount; /* Window lock count */ SHORT sLevel; /* Child window nesting level */ FLAG fSelect:1; /* Is this window selected? */ FLAG fHasText:1; /* Does the window have text? */ } INFO; typedef INFO * PINFO; /* Pointer to INFO array */ /* Static variables. */ PIDINFO pidi; HAB hab; HMQ hmq; HWND hwndDesktop, hwndObject; HWND hwndSleuth, hwndFrame, hwndHorzScroll, hwndVertScroll; PINFO pInfoBase; SHORT sWinCount; /* # of windows in system */ BOOL fExpand = FALSE; /* Expanded display mode? */ SHORT sLinesEach = 1; /* 1 or LINES_PER_WINDOW */ SHORT sCharX; /* Character width in pixels */ SHORT sCharY; /* Character height in pixels */ SHORT sDescenderY; /* Descender height */ SHORT sExtLeading; /* Vert. space between chars */ HPS hpsPaint; /* PS for SleuthPaint */ POINTL ptlPaint; /* Current pos for SleuthPaint */ CHAR szClass[10]; /* Our window class name */ CHAR szTitle[40]; /* Our window title */ /* Function prototypes. */ VOID main( VOID ); PSZ SleuthClassName( PSZ ); BOOL SleuthGetAllWindows( VOID ); VOID SleuthGetWindowTree( HWND hwnd, SHORT sLevel ); BOOL SleuthInit( VOID ); PSZ SleuthMsgName( USHORT ); VOID SleuthPaint( HWND hwndPaint ); VOID CDECL SleuthPaint1( CHAR* szFormat, ... ); VOID SleuthQueryScrollBar( HWND hwndBar, SHORT* psPos, SHORT* psMin, SHORT* psMax ); SHORT SleuthScroll( HWND hwnd, USHORT msg, USHORT idBar, USHORT cmd, SHORT sPos ); VOID SleuthSetOneBar( HWND hwndBar, SHORT sMax ); VOID SleuthSetScrollBars( VOID ); VOID SleuthTerminate( VOID ); MRESULT EXPENTRY SleuthWndProc( HWND, USHORT, MPARAM, MPARAM ); /* Application main program. */ VOID main() { QMSG qmsg; /* Initialize application, quit if any errors */ if( ! SleuthInit() ) return; /* Main message processing loop */ while( WinGetMsg( hab, &qmsg, NULL, 0, 0 ) ) WinDispatchMsg( hab, &qmsg ); /* Application termination */ SleuthTerminate(); } /* Convert a class name into its printable form. Normal class */ /* names are returned unchanged; the special WC_ "names" are */ /* converted into text. */ typedef struct _CLASSNAMES { NPSZ szNum; NPSZ szName; } CLASSNAMES; typedef CLASSNAMES NEAR * NPCLASSNAMES; CLASSNAMES aClassNames[] = { { "#1", "WC_FRAME" }, { "#2", "WC_DIALOG" }, { "#3", "WC_BUTTON" }, { "#4", "WC_MENU" }, { "#5", "WC_STATIC" }, { "#6", "WC_ENTRYFIELD" }, { "#7", "WC_LISTBOX" }, { "#8", "WC_SCROLLBAR" }, { "#9", "WC_TITLEBAR" }, { "#10", "WC_SIZEBORDER" }, { NULL, NULL } }; PSZ SleuthClassName( pszClass ) PSZ pszClass; { NPCLASSNAMES npNames; if( pszClass[0] != '#' ) return pszClass; for( npNames = &aClassNames[0]; npNames->szNum; npNames++ ) if( strcmp( pszClass, npNames->szNum ) == 0 ) return npNames->szName; return pszClass; } /* Gather up information on all windows in PM and fill in the */ /* INFO structure for them. */ BOOL SleuthGetAllWindows() { sWinCount = 0; /* Pick up both trees, from hwndDesktop and hwndObject */ SleuthGetWindowTree( hwndDesktop, 0 ); SleuthGetWindowTree( hwndObject, 0 ); /* Set scroll bars based on new window count */ SleuthSetScrollBars(); /* Force our window to be repainted */ WinInvalidateRect( hwndSleuth, NULL, TRUE ); return TRUE; } /* Gather information on all windows in the tree starting at hwnd, */ /* and add an entry to the INFO array for each one. */ VOID SleuthGetWindowTree( hwnd, sLevel ) HWND hwnd; SHORT sLevel; { PINFO pInfo; HWND hwndChild; /* Count the window and allocate an INFO entry */ sWinCount++; if( ! pInfoBase ) pInfoBase = malloc( sizeof(INFO) ); else pInfoBase = realloc( pInfoBase, sWinCount*sizeof(INFO) ); if( ! pInfoBase ) exit( 9 ); pInfo = pInfoBase + sWinCount - 1; /* -> INFO for this window */ /* Gather up this window's information */ pInfo->hwnd = hwnd; pInfo->fSelect = FALSE; pInfo->fHasText = FALSE; pInfo->class.flClassStyle = 0L; pInfo->class.pfnWindowProc = 0L; pInfo->class.cbWindowData = 0; pInfo->flStyle = WinQueryWindowULong( hwnd, QWL_STYLE ); pInfo->hwndOwner = WinQueryWindow( hwnd, QW_OWNER, FALSE ); pInfo->pfnwp = WinQueryWindowPtr( hwnd, QWP_PFNWP ); pInfo->usWindowID = WinQueryWindowUShort( hwnd, QWS_ID ); WinQueryWindowProcess( hwnd, &pInfo->ProcessID, &pInfo->ThreadID ); pInfo->hmq = WinQueryWindowPtr( hwnd, QWL_HMQ ); WinQueryWindowRect( hwnd, &pInfo->rclWindow ); pInfo->sLockCount = WinQueryWindowLockCount( hwnd ); pInfo->sLevel = sLevel; if( hwnd == hwndDesktop ) strcpy( pInfo->szClass, "WC_DESKTOP" ); else if( hwnd == hwndObject ) strcpy( pInfo->szClass, "WC_OBJECT" ); else { WinQueryClassName( hwnd, sizeof(pInfo->szClass), pInfo->szClass ); WinQueryClassInfo( hab, pInfo->szClass, &pInfo->class ); if( ! WinIsRectEmpty( hab, &pInfo->rclWindow ) ) WinMapWindowRect( hwnd, WinQueryWindow(hwnd,QW_PARENT,FALSE), &pInfo->rclWindow ); } pInfo->szText[0] = '\0'; if( pInfo->ProcessID == pidi.pid ) pInfo->fHasText = /* wrong... */ !! WinQueryWindowText( hwnd, sizeof(pInfo->szText), pInfo->szText ); /* Recurse through all child windows */ for( hwndChild = WinQueryWindow( hwnd, QW_TOP, FALSE ); hwndChild; hwndChild = WinQueryWindow( hwnd, QW_NEXT, FALSE ) ) SleuthGetWindowTree( hwndChild, sLevel+1 ); } /* Initialize the application. */ BOOL SleuthInit() { HDC hps; FONTMETRICS fm; SHORT sScreenX; SHORT sScreenY; ULONG flFrameFlags; /* Pick up the basic information we need */ DosGetPID( &pidi ); hab = WinInitialize( 0 ); hmq = WinCreateMsgQueue( hab, 0 ); hwndDesktop = WinQueryDesktopWindow( hab, NULL ); hwndObject = WinQueryObjectWindow( hwndDesktop ); sScreenX = (SHORT)WinQuerySysValue( hwndDesktop, SV_CXSCREEN ); sScreenY = (SHORT)WinQuerySysValue( hwndDesktop, SV_CYSCREEN ); /* Calculate character size for system font */ hps = WinGetPS( hwndDesktop ); GpiQueryFontMetrics( hps, (LONG)sizeof(fm), &fm ); sCharX = (SHORT)fm.lAveCharWidth; sCharY = (SHORT)fm.lMaxBaselineExt; sDescenderY = (SHORT)fm.lMaxDescender; WinReleasePS( hps ); WinLoadString( hab, NULL, IDS_CLASS, sizeof(szClass), szClass ); WinLoadString( hab, NULL, IDS_TITLE, sizeof(szTitle), szTitle ); WinRegisterClass( hab, szClass, SleuthWndProc, 0L, 0 ); flFrameFlags = FCF_SIZEBORDER | FCF_TITLEBAR | FCF_MINMAX | FCF_SYSMENU | FCF_MENU | FCF_ICON | FCF_VERTSCROLL | FCF_HORZSCROLL | FCF_SHELLPOSITION; hwndFrame = WinCreateStdWindow (hwndDesktop, WS_VISIBLE, &flFrameFlags, szClass, szTitle, 0L, NULL, ID_SLEUTH, &hwndSleuth) ; hwndHorzScroll = WinWindowFromID( hwndFrame, FID_HORZSCROLL ); hwndVertScroll = WinWindowFromID( hwndFrame, FID_VERTSCROLL ); WinSetWindowPos( hwndFrame, NULL, sScreenX * 1 / 20, /* X: 5% from left */ sScreenY * 2 / 10, /* Y 20% from bottom */ sScreenX * 9 / 10, /* nWidth: 90% */ sScreenY * 7 / 10, /* nHeight: 70% */ SWP_MOVE | SWP_SIZE ); /* Make our window visible now, so it's included in the list */ WinShowWindow( hwndFrame, TRUE ); /* Post a message to ourself to trigger the first display */ WinPostMsg( hwndSleuth, WM_COMMAND, MPFROMSHORT(CMD_LOOK), 0L ); return TRUE; } /* Paint our window. */ VOID SleuthPaint( hwnd ) HWND hwnd; { SHORT sWin; SHORT X; SHORT sScrollY, sScrollX; RECTL rclPaint, rclClient; PINFO pInfo; CHAR szQuote[2]; /* Get the PS and erase it */ hpsPaint = WinBeginPaint( hwnd, NULL, &rclPaint ); GpiErase( hpsPaint ); /* Find out how big the window is and how it's scrolled */ WinQueryWindowRect( hwnd, &rclClient ); sScrollX = (SHORT)WinSendMsg( hwndHorzScroll, SBM_QUERYPOS, 0, 0 ); sScrollY = (SHORT)WinSendMsg( hwndVertScroll, SBM_QUERYPOS, 0, 0 ); /* Calculate horizontal paint pos from scroll bar pos */ X = /* ( 1 */ - sScrollX /* ) */ * sCharX; /* Calculate index into INFO array and vertical paint pos, from scroll bar pos and top of painting rectangle */ sWin = ( ( (SHORT)rclClient.yTop - (SHORT)rclPaint.yTop ) / sCharY + sScrollY ) / sLinesEach; ptlPaint.y = (SHORT)rclClient.yTop + sDescenderY - ( sWin * sLinesEach - sScrollY + 1 ) * sCharY; pInfo = pInfoBase + sWin; /* Loop through and paint each entry */ while( sWin < sWinCount && (SHORT)ptlPaint.y + sCharY >= (SHORT)rclPaint.yBottom ) { /* Set X position and indent child windows */ ptlPaint.x = X + pInfo->sLevel * sCharX * (fExpand ? 4 : 2); szQuote[0] = szQuote[1] = '\0'; if( pInfo->fHasText ) szQuote[0] = '"'; if( ! fExpand ) { /* Paint the one-liner */ SleuthPaint1( "%08lX [%04X] {%s} (%d,%d;%d,%d) %s%s%s", pInfo->hwnd, pInfo->usWindowID, SleuthClassName( pInfo->szClass ), (INT)pInfo->rclWindow.xLeft, (INT)pInfo->rclWindow.yBottom, (INT)pInfo->rclWindow.xRight, (INT)pInfo->rclWindow.yTop, szQuote, pInfo->szText, szQuote ); } /* Increment to next INFO entry */ sWin++; pInfo++; } WinEndPaint( hpsPaint ); } /* Paint one line of text, using the global variables hpsPaint and */ /* ptlPaint. The #ifdef PM_MACINTOSH is because Lightspeed C */ /* doesn't like the ... notation, and Microsoft C doesn't like to */ /* do without it! */ #ifdef PM_MACINTOSH VOID CDECL SleuthPaint1( szFormat ) #else VOID CDECL SleuthPaint1( szFormat, ... ) #endif CHAR * szFormat; { va_list pArgs; CHAR szBuf[160]; va_start( pArgs, szFormat ); GpiCharStringAt( hpsPaint, &ptlPaint, (LONG)vsprintf( szBuf, szFormat, pArgs ), szBuf ); va_end( pArgs ); ptlPaint.y -= sCharY; } /* Get a scroll bar's range and position. More convenient than */ /* sending the messages every time. */ VOID SleuthQueryScrollBar( hwndBar, psPos, psMin, psMax ) HWND hwndBar; SHORT* psPos; SHORT* psMin; SHORT* psMax; { MRESULT mrRange; *psPos = (SHORT)WinSendMsg( hwndBar, SBM_QUERYPOS, 0, 0 ); mrRange = WinSendMsg( hwndBar, SBM_QUERYRANGE, 0, 0 ); *psMin = SHORT1FROMMR(mrRange); *psMax = SHORT2FROMMR(mrRange); } /* Scroll hwnd and adjust idBar according to cmd and sPos. */ SHORT SleuthScroll( hwnd, msg, idBar, cmd, sPos ) HWND hwnd; USHORT msg; USHORT idBar; USHORT cmd; SHORT sPos; { HWND hwndBar; SHORT sOldPos; SHORT sDiff; SHORT sMin; SHORT sMax; SHORT sPageSize; RECTL rcl; /* Get old scroll position and scroll range */ hwndBar = WinWindowFromID( WinQueryWindow(hwnd,QW_PARENT,FALSE), idBar ); SleuthQueryScrollBar( hwndBar, &sOldPos, &sMin, &sMax ); /* Calculate page size, horizontal or vertical as needed */ WinQueryWindowRect( hwnd, &rcl ); if( msg == WM_HSCROLL ) sPageSize = ( (SHORT)rcl.xRight - (SHORT)rcl.xLeft) / sCharX; else sPageSize = ( (SHORT)rcl.yTop - (SHORT)rcl.yBottom) / sCharY; /* Select the amount to scroll by, based on the scroll message */ switch( cmd ) { case SB_LINEUP: sDiff = -1; break; case SB_LINEDOWN: sDiff = 1; break; case SB_PAGEUP: sDiff = -sPageSize; break; case SB_PAGEDOWN: sDiff = sPageSize; break; case SB_SLIDERPOSITION: sDiff = sPos - sOldPos; break; case SBX_TOP: sDiff = -30000; /* Kind of a kludge but it works... */ break; case SBX_BOTTOM: sDiff = 30000; break; default: return 0; } /* Limit scroll destination to sMin..sMax */ if( sDiff < sMin - sOldPos ) sDiff = sMin - sOldPos; if( sDiff > sMax - sOldPos ) sDiff = sMax - sOldPos; /* Return if net effect is nothing */ if( sDiff == 0 ) return 0; /* Set the new scroll bar position and scroll the window */ WinSendMsg( hwndBar, SBM_SETPOS, MRFROMSHORT(sOldPos+sDiff), 0 ); WinScrollWindow( hwnd, msg == WM_HSCROLL ? -sDiff*sCharX : 0, msg == WM_VSCROLL ? sDiff*sCharY : 0, NULL, NULL, NULL, NULL, SW_INVALIDATERGN ); /* Force an immediate update for cleaner appearance */ WinUpdateWindow( hwnd ); return sDiff; } /* Set one scroll bar's position and range. */ VOID SleuthSetOneBar( hwndBar, sMax ) HWND hwndBar; SHORT sMax; { SHORT sPos, sOldMin, sOldMax; SleuthQueryScrollBar( hwndBar, &sPos, &sOldMin, &sOldMax ); if( sMax <= 0 ) sMax = sPos = 0; if( sMax != sOldMax ) { WinSendMsg( hwndBar, SBM_SETSCROLLBAR, MPFROMSHORT(sPos), MPFROM2SHORT(0,sMax) ); WinEnableWindow( hwndBar, !!(sMax) ); } } /* Set both scroll bars according to the window size and the */ /* number of INFO entries. */ VOID SleuthSetScrollBars() { RECTL rcl; WinQueryWindowRect( hwndSleuth, &rcl ); SleuthSetOneBar( hwndHorzScroll, WINDOW_WIDTH - (SHORT)rcl.xRight / sCharX ); SleuthSetOneBar( hwndVertScroll, sWinCount*sLinesEach - (SHORT)rcl.yTop/sCharY ); } /* Terminate the application. */ VOID SleuthTerminate() { WinDestroyWindow( hwndFrame ); WinDestroyMsgQueue( hmq ); WinTerminate( hab ); exit( 0 ); } /* Window function for Sleuth's main window. */ MRESULT EXPENTRY SleuthWndProc( hwnd, msg, mp1, mp2 ) HWND hwnd; USHORT msg; MPARAM mp1; MPARAM mp2; { switch( msg ) { /* Tell PM that we don't need no stinkin' upside down coordinates! */ case WM_CALCVALIDRECTS: return MRFROMSHORT( CVR_ALIGNLEFT | CVR_ALIGNTOP ); /* Menu command message - process the command */ case WM_COMMAND: switch( COMMANDMSG(&msg)->cmd ) { case CMD_ABOUT: return 0L; case CMD_EXIT: WinPostMsg( hwnd, WM_QUIT, 0L, 0L ); return 0L; case CMD_LOOK: SleuthGetAllWindows(); return 0L; } return 0L; /* Scroll messages - scroll the window */ case WM_HSCROLL: case WM_VSCROLL: SleuthScroll( hwnd, msg, SHORT1FROMMP(mp1), SHORT2FROMMP(mp2), SHORT1FROMMP(mp2) ); return 0L; /* Key-down message - handle cursor keys, ignore others */ case WM_CHAR: switch( CHARMSG(&msg)->vkey ) { case VK_LEFT: case VK_RIGHT: return WinSendMsg( hwndHorzScroll, msg, mp1, mp2 ); case VK_UP: case VK_DOWN: case VK_PAGEUP: case VK_PAGEDOWN: return WinSendMsg( hwndVertScroll, msg, mp1, mp2 ); } return 0L; /* Paint message - repaint all or part of our window */ case WM_PAINT: SleuthPaint( hwnd ); return 0L; /* Size message - recalculate our scroll bars */ case WM_SIZE: SleuthSetScrollBars(); return 0L; } /* All other messages go to DefWindowProc */ return WinDefWindowProc( hwnd, msg, mp1, mp2 ); } Figure 6: MacPM WinInitialize HAB APIENTRY WinInitialize( fOptions ) USHORT fOptions; { /* Static initialization - less hassle this way? */ _pps1 = &_ps1; _hps1 = (HPS)&_pps1; /* Macintosh memory management initialization */ MaxApplZone(); MoreMasters(); MoreMasters(); MoreMasters(); /* Open resource file - TEMP */ OpenResFile( "\pMacPM.rsrc" ); return (HAB)1; } Figure 7: MacPM WinCreateMsgQueue HMQ APIENTRY WinCreateMsgQueue( hab, sSize ) HAB hab; SHORT sSize; { PMYWND pwnd; /* Generic Macintosh initialization (already did memory stuff) */ InitGraf( &thePort ); InitFonts(); FlushEvents( everyEvent, 0 ); SetEventMask( everyEvent ); InitWindows(); InitMenus(); TEInit(); InitDialogs( 0L ); InitCursor(); /* Initialize the SV_ values and register the predefined window classes */ MpmInitSysValues(); if( ! WinRegisterClass( hab, WC_BUTTON, MpmFnwpButton, CS_MOVENOTIFY | CS_PUBLIC, 0 ) ) return NULL; if( ! WinRegisterClass( hab, WC_DESKTOP, MpmFnwpDesktop, CS_PUBLIC, 0 ) ) return NULL; #ifdef FUTURE if( ! WinRegisterClass( hab, WC_DIALOG, MpmFnwpDialog, CS_MOVENOTIFY | CS_PUBLIC, 0 ) ) return NULL; #endif if( ! WinRegisterClass( hab, WC_ENTRYFIELD, MpmFnwpEntryField, CS_MOVENOTIFY | CS_PUBLIC, 0 ) ) return NULL; if( ! WinRegisterClass( hab, WC_FRAME, MpmFnwpFrame, CS_MOVENOTIFY | CS_PUBLIC, 0x20 /*??*/ ) ) return NULL; if( ! WinRegisterClass( hab, WC_LISTBOX, MpmFnwpListBox, CS_MOVENOTIFY | CS_PUBLIC, 0 ) ) return NULL; if( ! WinRegisterClass( hab, WC_MENU, MpmFnwpMenu, CS_MOVENOTIFY | CS_PUBLIC, 0 ) ) return NULL; if( ! WinRegisterClass( hab, WC_SCROLLBAR, MpmFnwpScrollBar, CS_MOVENOTIFY | CS_PUBLIC, 0 ) ) return NULL; if( ! WinRegisterClass( hab, WC_SIZEBORDER, MpmFnwpSizeBorder, CS_MOVENOTIFY | CS_PUBLIC, 0 ) ) return NULL; if( ! WinRegisterClass( hab, WC_STATIC, MpmFnwpStatic, CS_MOVENOTIFY | CS_PUBLIC, 0 ) ) return NULL; if( ! WinRegisterClass( hab, WC_TITLEBAR, MpmFnwpTitleBar, CS_MOVENOTIFY | CS_PUBLIC, 0 ) ) return NULL; /* Create the object and desktop windows */ _hwndObject = WinCreateWindow( NULL, WC_DESKTOP, _szNull, 0, 0, 0, 0, 0, NULL, NULL, 0, NULL, NULL ); if( ! _hwndObject ) return NULL; pwnd = PMYWNDOF(_hwndObject); pwnd->ucKind = WK_OBJECT; pwnd->pfnwp = MpmFnwpObject; _hwndDesktop = WinCreateWindow( NULL, WC_DESKTOP, _szNull, WS_DISABLED, 0, 0, 0, 0, NULL, NULL, 0, NULL, NULL ); if( ! _hwndDesktop ) return NULL; pwnd = PMYWNDOF(_hwndDesktop); pwnd->cx = _alSysVal[SV_CXSCREEN]; pwnd->cy = _alSysVal[SV_CYSCREEN]; pwnd->flStyle |= WS_VISIBLE; return (HMQ)1; } Figure 8: MacPM WinCreateWindow HWND APIENTRY WinCreateWindow( hwndParent, pszClass, pszName, flStyle, x, y, cx, cy, hwndOwner, hwndPrevSibling, id, pCtlData, pPresParams ) HWND hwndParent, hwndOwner, hwndPrevSibling; PSZ pszClass, pszName; ULONG flStyle; SHORT x, y, cx, cy; USHORT id; PVOID pCtlData, pPresParams; { HWND hwnd, hwndNextSibling, hwndMac; PMYWND pwnd; HMYCLASS hClass; PMYCLASS pClass; USHORT usLen; SHORT sProcID; Rect rect, rectAdj; WindowPeek pwin, pwinBehind; CHAR szTitle[256]; BOOL fGoAway, fFrameWindow; UCHAR ucKind; USHORT usHash; CHAR szClassBuf[10]; ULONG flFrameFlags; /* Figure out which WK_ code to use */ if( pszClass == WC_DESKTOP ) ucKind = WK_DESKTOP; else { if( ! hwndParent || hwndParent == HWND_DESKTOP ) hwndParent = _hwndDesktop; if( hwndParent == _hwndDesktop ) ucKind = WK_MAIN; else ucKind = WK_CHILD; } /* Fix up WC_ names, calculate hash, and find window class */ MpmFixName( &pszClass, szClassBuf, &usHash ); hClass = MpmFindClass( pszClass, usHash ); if( ! hClass ) return NULL; /* Grab frame flags if it's the window frame class */ fFrameWindow = MpmIsFrameClass( pszClass ); flFrameFlags = ( fFrameWindow && pCtlData ? *(PULONG)pCtlData : 0 ); /* Allocate window structure */ usLen = sizeof(MYWND) + (**hClass).class.cbWindowData; hwnd = (HWND)MpmNewHandleZ( usLen ); if( ! hwnd ) return NULL; /* Handle WK_ specific stuff */ switch( ucKind ) { /* Desktop window: just set window position */ case WK_DESKTOP: SetRect( &rect, 0, 0, cx, cy ); SetRect( &rectAdj, 0, 0, 0, 0 ); break; /* Main Mac window: figure out which style of Mac window to use and create it */ case WK_MAIN: fGoAway = FALSE; if( ! ( flStyle & FS_BORDER ) && ! ( flFrameFlags & FCF_DOCBITS ) ) { sProcID = plainDBox; /* shouldn't have border! */ rectAdj = _rectAdjPlainDBox; } else if( flFrameFlags & FCF_DOCBITS ) { sProcID = documentProc; rectAdj = _rectAdjDocumentProc; if( flFrameFlags & FCF_MAXBUTTON ) sProcID += 8; if( flFrameFlags & FCF_SYSMENU ) fGoAway = TRUE; } else if( flStyle & FS_DLGBORDER ) { sProcID = dBoxProc; rectAdj = _rectAdjDBoxProc; } else { sProcID = altDBoxProc; rectAdj = _rectAdjAltDBoxProc; } rect.left = rectAdj.left + x; rect.right = rectAdj.right + x + cx; rect.top = rectAdj.top + rect.bottom - cy; rect.bottom = rectAdj.bottom + screenBits.bounds.bottom - y; /* Should check rect for out of screen boundaries! */ strncpy( szTitle, pszName, 255 ); szTitle[255] = '\0'; if( hwndPrevSibling == HWND_TOP ) pwinBehind = (WindowPeek)(-1); else if( hwndPrevSibling == HWND_BOTTOM ) pwinBehind = (WindowPeek)NULL; else if( ISMAINWINDOW(hwndPrevSibling) ) pwinBehind = PWINOFHWND(hwndPrevSibling); else ERROR( "WinCreateWindow: Invalid hwndPrevSibling" ); pwin = (WindowPeek)NewWindow( NULL, &rect, CtoPstr(szTitle), FALSE, sProcID, pwinBehind, fGoAway, (LONG)hwnd ); if( ! pwin ) { DisposHandle( (Handle)hwnd ); return NULL; } PWINOFHWND(hwnd) = pwin; break; /* Child window: set up all the "kin" windows */ case WK_CHILD: if( hwndPrevSibling == HWND_TOP ) { MYWNDOF(hwnd).hwndNextSibling = hwndNextSibling = MYWNDOF(hwndParent).hwndTopChild; MYWNDOF(hwndNextSibling).hwndPrevSibling = hwnd; MYWNDOF(hwndParent).hwndTopChild = hwnd; } else if( hwndPrevSibling == HWND_BOTTOM ) { MYWNDOF(hwnd).hwndPrevSibling = hwndPrevSibling = MYWNDOF(hwndParent).hwndBottomChild; MYWNDOF(hwndPrevSibling).hwndNextSibling = hwnd; MYWNDOF(hwndParent).hwndBottomChild = hwnd; } else { if( ! MpmValidateWindow(hwndPrevSibling) ) return NULL; if( MYWNDOF(hwndPrevSibling).hwndParent != hwndParent ) return NULL; MYWNDOF(hwnd).hwndNextSibling = hwndNextSibling = MYWNDOF(hwndPrevSibling).hwndNextSibling; MYWNDOF(hwnd).hwndPrevSibling = hwndPrevSibling; MYWNDOF(hwndPrevSibling).hwndNextSibling = hwnd; if( hwndNextSibling ) MYWNDOF(hwndNextSibling).hwndPrevSibling = hwnd; else MYWNDOF(hwndParent).hwndBottomChild = hwnd; } if( ! MYWNDOF(hwndParent).hwndTopChild ) MYWNDOF(hwndParent).hwndTopChild = hwnd; if( ! MYWNDOF(hwndParent).hwndBottomChild ) MYWNDOF(hwndParent).hwndBottomChild = hwnd; for( hwndMac = hwndParent; ISCHILDWINDOW(hwndMac); hwndMac = MYWNDOF(hwndMac).hwndParent ); PWINOFHWND(hwnd) = PWINOFHWND(hwndMac); rectAdj = MYWNDOF(hwndMac).rectAdj; break; } /* Fill in the window structure fields */ pClass = *hClass; pwnd = PMYWNDOF(hwnd); pwnd->signature = WND_SIGNATURE; pwnd->ucKind = ucKind; pwnd->pfnwp = pClass->class.pfnWindowProc; pwnd->hclass = hClass; pwnd->flStyle = ( flStyle & ~WS_VISIBLE ) | ( pClass->class.flClassStyle & CLASSWINDOWBITS ); pwnd->hwndOwner = hwndOwner; pwnd->hwndParent = hwndParent; pwnd->id = id; pwnd->rectAdj = rectAdj; pwnd->flFrameFlags = flFrameFlags; /* Now the window is here for real, so send the WM_CREATE */ if( WinSendMsg( hwnd, WM_CREATE, MPFROMP(pCtlData), MPFROMP(&hwndParent) ) ) { WinDestroyWindow( hwnd ); return NULL; } /* Send the WM_ADJUSTWINDOWPOS if it's not the desktop window and it has a nonzero size */ if( cx && cy && ucKind != WK_DESKTOP ) WinSetWindowPos( hwnd, NULL, x, y, cx, cy, SWP_MOVE | SWP_SIZE ); /* Make the window visible if it's supposed to be visible */ if( flStyle & WS_VISIBLE ) WinShowWindow( hwnd, TRUE ); return hwnd; } Figure 9: MacPM WinCreateStdWindow HWND APIENTRY WinCreateStdWindow( hwndParent, flStyle, pCtlData, pszClassClient, pszTitle, flStyleClient, hmod, idResources, phwndClient ) HWND hwndParent; ULONG flStyle, flStyleClient; PVOID pCtlData; PSZ pszClassClient, pszTitle; HMODULE hmod; USHORT idResources; PHWND phwndClient; { HWND hwndFrame, hwndClient; SHORT x, y, cx, cy; ULONG flFrameFlags; /* Pick up frame flags, take care of special hwndParent values */ flFrameFlags = ( pCtlData ? *(PULONG)pCtlData : 0 ); if( ! hwndParent || hwndParent == HWND_DESKTOP ) hwndParent = _hwndDesktop; ASSERT( hwndParent == _hwndDesktop, "WinCreateStdWindow: Must be a top level window" ); ASSERT( ! hmod, "WinCreateStdWindow: hmod must be NULL" ); ASSERT( ! (flStyle & WS_CLIPCHILDREN), "WinCreateStdWindow: WS_CLIPCHILDREN not allowed" ); /* Assign default position if visible */ if( flStyle & WS_VISIBLE ) { x = 10; /* TEMP HACK */ y = screenBits.bounds.bottom - 200; cx = 250; cy = 150; } else x = y = cx = cy = 0; /* Create the frame window itself */ hwndFrame = WinCreateWindow( hwndParent, WC_FRAME, _szNull, flStyle & ~WS_VISIBLE, x, y, cx, cy, NULL, HWND_TOP, idResources, pCtlData, NULL ); if( ! hwndFrame ) return NULL; /* Create frame controls according to flFrameFlags */ MpmFrameUpdate( hwndFrame, flFrameFlags ); /* Create client window if requested */ if( pszClassClient && *pszClassClient ) { *phwndClient = hwndClient = WinCreateWindow( hwndFrame, pszClassClient, _szNull, flStyleClient, 0, 0, 0, 0, hwndFrame, HWND_BOTTOM, FID_CLIENT, NULL, NULL ); if( ! hwndClient ) goto exitDestroy; } /* Create menu and initialize title */ if( flFrameFlags & FCF_MENU ) if( ! MpmMenuLoad( hwndFrame, idResources ) ) goto exitDestroy; if( flFrameFlags & FCF_TITLEBAR ) WinSetWindowText( hwndFrame, pszTitle ); /* Make window visible if requested */ if( flStyle & WS_VISIBLE ) { WinSendMsg( hwndFrame, WM_FORMATFRAME, 0L, 0L ); WinShowWindow( hwndFrame, TRUE ); } return hwndFrame; exitDestroy: if( pszClassClient && *pszClassClient ) *phwndClient = NULL; WinDestroyWindow( hwndFrame ); return NULL; } Figure 10: MacPM Windowfunction for Scroll Bar Controls MRESULT EXPENTRY MpmFnwpScrollBar( hwnd, msg, mp1, mp2 ) HWND hwnd; USHORT msg; MPARAM mp1; MPARAM mp2; { USHORT id; ControlHandle hctl, hctl1; Rect rect; Rect* prect; SHORT sPart; POINTL ptl; SHORT cmd; Handle hcdef; LONG lPoint; Point point; static BOOL fTrackThumb = FALSE; static SHORT sInitValue; if( ! MpmValidateWindow(hwnd) ) return FALSE; id = MYWNDOF(hwnd).id; hctl = MYWNDOF(hwnd).hctl; switch( msg ) { /* Return the current position */ case SBM_QUERYPOS: return MRFROMSHORT( fTrackThumb ? sInitValue : (**hctl).contrlValue ); /* Return the scroll bar range */ case SBM_QUERYRANGE: return MRFROM2SHORT( (**hctl).contrlMin, (**hctl).contrlMax ); /* Set the scroll bar position and range */ case SBM_SETSCROLLBAR: (**hctl).contrlMin = SHORT1FROMMP(mp2); (**hctl).contrlMax = SHORT2FROMMP(mp2); /* fall through */ /* Set the scroll bar position only */ case SBM_SETPOS: if( MYWNDOF(hwnd).flStyle & WS_VISIBLE ) SetCtlValue( hctl, SHORT1FROMMP(mp1) ); else (**hctl).contrlValue = SHORT1FROMMP(mp1); fTrackThumb = FALSE; return MRFROMSHORT(1); /* Handle mouse button down: call TrackControl to track it */ case WM_BUTTON1DOWN: ptl.x = SHORT1FROMMP(mp1); ptl.y = SHORT2FROMMP(mp1); MpmMapMacOfPtl( hwnd, &point, &ptl ); sPart = FindControl( point, PWINOFHWND(hwnd), &hctl1 ); ASSERT( sPart && hctl1 == hctl, "MpmFnwpScrollBar: FindControl failed" ); _hwndTrack = hwnd; if( sPart == inThumb ) { fTrackThumb = TRUE; sInitValue = (**hctl).contrlValue; if( TrackControl( hctl, point, NULL ) ) MpmTrackScrollBarNotify( SB_SLIDERPOSITION ); fTrackThumb = FALSE; } else { TrackControl( hctl, point, (ProcPtr)MpmTrackScrollBar ); MpmTrackScrollBarNotify( SB_ENDSCROLL ); } return 0L; /* Handle scroll bar creation: call NewControl to create it */ case WM_CREATE: rect.left = rect.top = 0; if( MYWNDOF(hwnd).flStyle & SBS_VERT ) rect.right = _alSysVal[SV_CXVSCROLL], rect.bottom = 100; else rect.bottom = _alSysVal[SV_CYHSCROLL], rect.right = 100; hctl = NewControl( PWINOFHWND(hwnd), &rect, _szNull, MYWNDOF(hwnd).flStyle & WS_VISIBLE, 0, 0, 1, scrollBarProc, (long)hwnd ); if( ! hctl ) return MRFROMSHORT(1); MYWNDOF(hwnd).hctl = hctl; return 0L; /* Destroy scroll bar */ case WM_DESTROY: MYWNDOF(hwnd).hctl = NULL; DisposeControl( hctl ); return 0L; /* Handle window movement: use MoveControl to move the bar */ case WM_MOVE: MpmQueryMacRect( hwnd, &rect ); if( MYWNDOF(hwnd).flStyle & WS_VISIBLE ) MoveControl( hctl, rect.left, rect.top ); else { prect = &(**hctl).contrlRect; prect->right += rect.left - prect->left; prect->left = rect.left; prect->bottom += rect.top - prect->top; prect->top = rect.top; } return 0L; /* Show or hide the scroll bar */ case WM_SHOW: if( mp1 ) ShowControl( hctl ); else HideControl( hctl ); return 0L; /* Handle window resizing: use SizeControl to resize it */ case WM_SIZE: if( SHORT1FROMMP(mp2) && SHORT2FROMMP(mp2) ) { if( MYWNDOF(hwnd).flStyle & WS_VISIBLE ) SizeControl( hctl, SHORT1FROMMP(mp2), SHORT2FROMMP(mp2) ); else { prect = &(**hctl).contrlRect; prect->right = prect->left + SHORT1FROMMP(mp2); prect->bottom = prect->top + SHORT2FROMMP(mp2); } } return 0L; } return WinDefWindowProc( hwnd, msg, mp1, mp2 ); } Figure 11: Function to Load the Menu Bar from the Resource File LOCAL BOOL MpmMenuLoad( hwndFrame, idResources ) HWND hwndFrame; USHORT idResources; { MbarHandle hmbar; MbarPtr pmbar; SHORT sMenuCount; MenuHandle hmenu; hmbar = (MbarHandle)GetNewMBar( idResources ); if( ! hmbar ) return FALSE; sMenuCount = MENUCOUNTOFHMBAR( hmbar ); ASSERT( sMenuCount > 0, "MpmMenuLoad: null menu bar?" ); SetMenuBar( hmbar ); DisposHandle( (Handle)hmbar ); hmbar = (MbarHandle)MenuList; hmenu = (**hmbar).mlist[0].hmenu; ASSERT( hmenu, "MpmMenuLoad: no apple menu?" ); _sAppleCount = CountMItems( hmenu ); AddResMenu( hmenu, 'DRVR' ); DrawMenuBar(); return TRUE; } Figure 12: Function to Handle Mouse Events LOCAL BOOL MpmMsgMouse( pqmsg, pEvent, msg ) PQMSG pqmsg; EventRecord *pEvent; USHORT msg; { SHORT sArea; POINTL ptl; WindowPeek pwin; USHORT fid, usHitHi; HWND hwnd; sArea = FindWindow( pEvent->where, &pwin ); fid = usHitHi = 0; ptl = pqmsg->ptl; pqmsg->hwnd = hwnd = ( pwin ? HWNDOFPWIN(pwin) : _hwndDesktop ); switch( sArea ) { case inContent: WinMapWindowPoints( _hwndDesktop, hwnd, &ptl, 1 ); MpmMsgFindChild( pqmsg, &ptl ); hwnd = pqmsg->hwnd; break; case inDesk: break; case inDrag: fid = FID_TITLEBAR; break; case inGoAway: fid = FID_SYSMENU; break; case inGrow: fid = FID_SIZEBORDER; break; case inMenuBar: hwnd = _hwndMenu; if( ! hwnd ) return FALSE; break; case inSysWindow: SystemClick( pEvent, pwin ); return FALSE; case inZoomIn: case inZoomOut: usHitHi = sArea; fid = FID_MINMAX; break; default: return FALSE; } if( fid ) { hwnd = WinWindowFromID( hwnd, fid ); ASSERT( hwnd, "MpmMsgMouse: missing frame control" ); } if( MYWNDOF(hwnd).flStyle & WS_DISABLED ) return FALSE; pqmsg->hwnd = hwnd; pqmsg->msg = msg; pqmsg->mp1 = MPFROM2SHORT( ptl.x, ptl.y ); pqmsg->mp2 = MPFROM2SHORT( HT_NORMAL, usHitHi ); return TRUE; } Figure 13: Function to Get a PS Handle for hwnd HPS APIENTRY WinGetPS( hwnd ) HWND hwnd; { GrafPtr pgraf; RgnHandle hrgn; Rect rect; ASSERT( ! ( _ps1.flags & PSF_INUSE ), "WinGetPS: PS already in use" ); if( ! hwnd || hwnd == HWND_DESKTOP ) hwnd = _hwndDesktop; if( ! MpmValidateWindow(hwnd) ) return NULL; /* Clear the cache PS and mark it as in use */ memzero( &_ps1 ); _ps1.hwnd = hwnd; _ps1.flags |= PSF_INUSE; /* Copy the Mac window's GrafPort */ if( hwnd == _hwndDesktop ) GetWMgrPort( &pgraf ); else pgraf = &PWINOFHWND(hwnd)->port; _ps1.port = *pgraf; _ps1.port.visRgn = NewRgn(); CopyRgn( pgraf->visRgn, _ps1.port.visRgn ); _ps1.port.clipRgn = NewRgn(); CopyRgn( pgraf->clipRgn, _ps1.port.clipRgn ); /* Clip the visRgn down to this window's rectangle in case it's a child window */ if( ! ( MYWNDOF(hwnd).flStyle & WS_PARENTCLIP ) ) { hrgn = NewRgn(); MpmQueryMacRect( hwnd, &rect ); RectRgn( hrgn, &rect ); SectRgn( _ps1.port.visRgn, hrgn, _ps1.port.visRgn ); DisposeRgn( hrgn ); } /* Handle WS_CLIPCHILDREN and WS_CLIPSIBLINGS here? */ return _hps1; } Figure 14: Module Containing Mac Equivalents of PM GPI Functions /*-----------------------------------------------------------------*/ /* MpmGPI.c */ /* GPI functions */ /*-----------------------------------------------------------------*/ #include "MacPM.h" /*-----------------------------------------------------------------*/ /* Draw a character string at the current position */ /*-----------------------------------------------------------------*/ LONG APIENTRY GpiCharString( hps, lLen, pch ) HPS hps; LONG lLen; PCH pch; { if( ! MpmValidatePS(hps) ) return GPI_ERROR; thePort = PGRAFOFHPS(hps); DrawText( pch, 0, (SHORT)lLen ); return GPI_OK; } /*-----------------------------------------------------------------*/ /* Draw a character string at the specified position */ /*-----------------------------------------------------------------*/ LONG APIENTRY GpiCharStringAt( hps, pptl, lLen, pch ) HPS hps; PPOINTL pptl; LONG lLen; PCH pch; { if( ! GpiMove( hps, pptl ) ) return GPI_ERROR; return GpiCharString( hps, lLen, pch ); } /*-----------------------------------------------------------------*/ /* Erase a PS on the screen. */ /*-----------------------------------------------------------------*/ BOOL APIENTRY GpiErase( hps ) HPS hps; { if( ! MpmValidatePS(hps) ) return FALSE; thePort = PGRAFOFHPS(hps); EraseRgn( thePort->visRgn ); return TRUE; } /*-----------------------------------------------------------------*/ /* Set the drawing position to *pptl. */ /*-----------------------------------------------------------------*/ BOOL APIENTRY GpiMove( hps, pptl ) HPS hps; PPOINTL pptl; { PMYWND pwnd; if( ! MpmValidatePS(hps) ) return FALSE; thePort = PGRAFOFHPS(hps); pwnd = PMYWNDOF(HWNDOFHPS(hps)); MoveTo( (SHORT)pptl->x, (SHORT)( pwnd->cy - pptl->y ) ); return TRUE; } /*-----------------------------------------------------------------*/ /* Pick up the current font metrics and return it in *pfm. */ /* This is kind of hokey and only initializes some of the fields */ /* properly! */ /*-----------------------------------------------------------------*/ BOOL APIENTRY GpiQueryFontMetrics( hps, lLen, pfm ) HPS hps; LONG lLen; PFONTMETRICS pfm; { GrafPtr pgraf; FMInput fmi; FMOutPtr pfmo; if( ! MpmValidatePS(hps) ) return FALSE; pgraf = PGRAFOFHPS( hps ); fmi.family = pgraf->txFont; fmi.size = pgraf->txSize; fmi.face = pgraf->txFace; fmi.needBits = FALSE; fmi.device = 0; fmi.numer.h = 1; fmi.numer.v = 1; fmi.denom.h = 1; fmi.denom.v = 1; pfmo = FMSwapFont( &fmi ); ASSERT( pfmo, "GpiQueryFontMetrics: FMSwapFont failed?" ); memzero( pfm ); pfm->lAveCharWidth = /* wrong, but make it same as max */ pfm->lMaxCharInc = pfmo->widMax; pfm->lMaxBaselineExt = pfmo->ascent + pfmo->descent + pfmo->leading; pfm->lMaxDescender = pfmo->descent; /* more later... */ return TRUE; } /*-----------------------------------------------------------------*/ /* Make sure hps is a valid PS handle. */ /*-----------------------------------------------------------------*/ LOCAL BOOL MpmValidatePS( hps ) HPS hps; { if( hps != _hps1 ) { ERROR( "Invalid PS handle!" ); return FALSE; } return TRUE; } /*-----------------------------------------------------------------*/ ─────────────────────────────────────────────────────────────────────────── Development Tools for the Macintosh ─────────────────────────────────────────────────────────────────────────── For OS/2 and Windows, there's not much question of which compilers and libraries to use: Microsoft C and Macro Assembler (MASM), along with the Windows and OS/2 Software Development Kits (SDKs). This situation may change as other vendors provide OS/2 and Windows tools. On the Mac, the choice is less clear. There are a variety of good development environments for C and assembler, but the best are Lightspeed C and the Macintosh Programmer's Workshop (MPW). MPW provides the utmost in programmability and customizability; its main window is a command shell, where you can type in simple commands or edit and run complex scripts. You can open additional windows to edit C or assembler code, or to save and edit script files. Besides using MPW to code standalone Mac applications, it's also very easy to write "tools," specialized applications that run only under the MPW shell and extend its functionality. As great as MPW is, my personal choice is Lightspeed C. Though it lacks the programmable shell of MPW, Lightspeed C lives up to its name in speeding up the development process. Turnaround from a source change to running the code is blazingly fast, and the editor and compiler are closely integrated so the editor knows things like which source file defines a particular function. You can write inline assembly right in the middle of C code, making utility routines easy to code. Best of all is the source code debugger in the new 3.0 version. The hot item is its data window. Like the CodeView(R) Watch window, it's basically a list of expressions and their values, and it's updated each time the program stops (for example, at a breakpoint). Double-click on a structure or array in this window, and it opens up a structure or array window. This is almost identical to the new "??" command in the CodeView debugger, but you can open up as many of these windows as you like and keep them open while you run your program, watching your data as you go. In any of the data windows, you can type in new values for any of the watched expressions. Data windows and the source code window share screen space with the application fairly cleanly; on a Mac with two monitors, the data windows can appear on the second monitor. The expression evaluator, both for new values and for the expressions themselves, knows every bit of compiler information, and you can use preprocessor macros and symbols freely in your expressions. Using Lightspeed C with the source debugger requires at least two megabytes of RAM and MultiFinder. There are a few Mac utilities I wouldn't leave home without, whether using MPW or Lightspeed C. TMON is a machine-level debugger that is the perfect complement to Lightspeed's source debugger. It's easy to use, with a nice windowed interface, but lacks nothing in debugging power──a must for any serious Mac programmer. QuicKeys is a keyboard/mouse macro program. Although Apple now includes a keyboard macro program with the Mac System software, QuicKeys is still my favorite. None of my Mac software could do much with the extra keys on Apple's Extended Keyboard, but a few minutes with QuicKeys and I had them all working the way I wanted. I even have all the same Edit menu shortcuts (Ctrl-Ins and friends) as in Windows and PM, making life less confusing when I switch back and forth. QuicKeys is worth buying just to get a lesson in how to design a superb utility package. One last tidbit is The Programmer's Online Companion, a handy on-line reference to all the Mac Toolbox and operating system calls. If you ever use Inside Macintosh, you need this. Be sure to get the new version with the Mac II calls. Of course, one remaining problem is transferring files between the PC and the Mac. The best bet for this would be either Lap-Link Mac or MacLink(R). I've been using Lap-Link Mac, and it does a good job. It comes with a serial cable that will connect any Mac to any PC. The cable has both 9-pin and 25- pin connectors on the PC end, and 9-pin and mini-DIN connectors on the Mac end; it makes for a funny looking cable, but hooking the computers up is painless this way. Transfers are quick and reliable, and the program can convert between the Mac and PC text file formats. The only thing I don't like about Lap-Link Mac is the user interface. Everything is controlled from the PC side──the Mac program is a slave──and, while it's reasonably powerful and convenient, the user interface takes some getting used to. MacLink is probably worth looking into, though I haven't tried it out myself. Product Information XVT-the Extensible Virtual Toolkit Advanced Programming Institute Ltd. P.O. Box 17665 Boulder, CO 80308 (303) 443-4223 Currently available for Macintosh and Windows. OS/2, X Windows, and other versions planned. The "Technical Overview of XVT," by Marc Rochkind is available free from API and is itself an education in portability issues. Lightspeed C Symantec, Inc., Think Technologies Division 135 South Road Bedford, MA 01730 (617) 275-4800 Macintosh Programmer's Workshop Apple Programmer's and Developer's Association (APDA) 290 S.W. 43rd St. Renton, WA 98055 (206) 251-6548 TMON ICOM Simulations, Inc. 648 S. Wheeling Road Suite 10 Wheeling, IL 60090 (312) 520-4440 QuicKeys CE Software, Inc. P.O. Box 65580 West Des Moines, IA 50265 (515) 224-1995 The Programmer's Online Companion Addison-Wesley Publishing Company, Inc. Jacob Way Reading, MA 01867 (617) 944-3700 Using the OS/2 Environment to Develop DOS and OS/2 Applications ─────────────────────────────────────────────────────────────────────────── Also see the following related articles: An OS/2 Command Reference Configuring OS/2 Exploring the OS/2 SDK Exploring the OS/2 Programmer's Toolkit ─────────────────────────────────────────────────────────────────────────── Richard Hale Shaw☼ You've been thinking of switching to an OS/2 system for applications development but don't know where to start. You've bought an OS/2 system and aren't sure how to install and configure it for programming. Its multitasking, multithreaded capabilities intrigue you but seem formidable. Well, they are a bit formidable. OS/2 offers significant productivity gains whether you're developing software for either DOS or OS/2, but it demands that you rethink many of the programming assumptions you made when writing DOS applications. This article will help you get started. First, we'll investigate OS/2 as a program development environment: its hardware and software requirements, installing and configuring OS/2, and utilizing multiple sessions for program development. In subsequent articles we'll study the OS/2 application program interface (API), in particular the Video Input/Output (VIO), Keyboard (KBD), and Mouse (MOU) subsystems by writing useful programs that rely on them. We'll learn to write bound applications for both real and protected mode, and wrap up by putting together the key ideas into a real and useful OS/2 application. This series of articles assumes a working knowledge of C and DOS, but not OS/2. Our goal is to develop several programs and tools that will help to instill a working knowledge of the OS/2 API and some of the issues critical to writing OS/2 applications. You will need a C compiler, the Microsoft(R) OS/2 Programmer's Toolkit, and an OS/2 system. By the time we finish you'll be ready to consider the next level of programming──the Presentation Manager. What are the Benefits? It's well known that OS/2 operates in the protected mode of the Intel(R) 80286 processor──the 80386 also supports 286 protected mode──and, of course, you can use OS/2 to develop protected-mode programs. But there are also advantages to using OS/2 as a host development environment for applications that run in MS-DOS(R) 8086/8088 real mode. Probably the most immediate benefit of OS/2 is the ability to use multiple program development sessions. Each program can run as an independent process under OS/2, which gives the program its own screen group or session, and also endows each session with a logical keyboard and console interface. OS/2 ties these to the real keyboard and video screen when the process is running in the foreground. Processes running in the background continue uninterrupted unless awaiting keyboard input (which they can get when switched back into the foreground). As a result, most of the mundane tasks done while writing and developing a program, including tasks that tie up specific machine resources, may be carried out more efficiently in multiple program sessions. Compiling a program or grepping for program references, for instance, can run in a session that you switched into the background. This leaves you free to pursue other tasks, such as editing another part of the program or compiling a different program altogether. With the OS/2 multisession environment, you can dedicate a session to each facet of program development. It's possible to create separate sessions for the program editor, the compiler or assembler, for utilities, the debugger, and the program test environment. Or, you might create a session whose sole purpose is to act as a convenient, readily-available command prompt, for running utilities and copying disks (you can even relegate disk formatting to the background). You need not exit or terminate your program editor to start the compilation of a source code module. Nor must you terminate a debugging session in order to look up a source code reference with your editor. Since each OS/2 session contains its own independent environment, prompt, path, and screen configuration, you can customize a session and tailor it to the tasks which are to run in it. For example, you can set a compiler session's path and environment variables for library and source file searches, and optimize them to most quickly navigate the appropriate directories. You can create a program editor session which only allows the editor to know (via environment variables) about a specific set of files and directories, and which would differ from a session used for testing a new program. Creating several sessions to perform different tasks is easy under OS/2; in fact, it's addictive. Using the same tricks that were available under MS-DOS (a few simple tools and ANSI environment strings), you can configure the command prompt, screen colors, and screen modes for each session independently. Thus, you can readily identify each, even when rapidly switching from one session to another. For example, a compilation session might be set to a 43-line screen mode with a dark background and highlighted prompt. This would allow a quick perusal of compiler warning and error messages when switching between sessions, and it can make it easier to discern the progress of a compiler session when processing a large number of files. Or, the command-prompt session described above might be a stark black-on-white. Under OS/2 you can dedicate a separate session to each program that needs a unique environment (the concept of the "environment" takes on a meaning more akin to that in UNIX(R) operating environments). In fact, it's not unusual to run two different compilers at once. Running multiple editing sessions can further enhance your productivity. Most contemporary program editors boast the ability to display and edit multiple files in multiple windows. By creating more than one editing session, you can literally double or triple the number of files that can be displayed or edited at a time, and you can do so without cluttering the screen. To take this a step further, I've found it useful to actually edit, compile, and debug two entirely different programs simultaneously. Although it might seem counterproductive, this is helpful when one of the programs is exceptionally large (over 100Kb) and takes an inordinate amount of time to compile and link, in which case there is plenty of time to switch one compilation session into the background and edit or test a completely different program in the foreground. While foreground sessions get priority attention from the CPU under OS/2, you'll be amazed how quickly a well- behaved OS/2 application will run in the background. (The definition of a "well-behaved" OS/2 application will become evident over the course of the next few articles in this series.──Ed.) The DOS Session OS/2 can be configured to create a special real-mode session that supports a hybrid version of MS-DOS 3.x, known alternately as the Compatibility Box or the DOS session. This session is only active as a foreground process (that is, when displayed on the screen). It's frozen in the background whenever you switch to another session and OS/2 returns the processor to protected mode. To programs running in the DOS session, the operating environment appears to be DOS, although OS/2 keeps some control over the machine. Since the processor runs the DOS session in real mode, the "fast and loose" DOS operating environment is still available, making it possible for a DOS program running in the DOS session to abuse it and cause problems──with even greater ramifications than under normal DOS. Further, some tools behave acceptably under DOS but may not under OS/2. A number of program editors, for instance, poll the keyboard continuously in order to speedily process keyboard input. This is fine in a single-tasking environment like DOS. Under OS/2 these applications can bring the system to its knees; by directly polling the hardware from the DOS session, they force the CPU to assign them too many time slices. Thus, while they remain speedy, the lack of CPU attention given elsewhere can slow background processes (such as a program compile) to a crawl. A well-written OS/2 editor will create a thread to get keyboard input. Since OS/2 knows which screen group "owns" the thread, it will "block" the thread until keyboard input is available for it. Thus other processes will proceed normally (whether in the background or the foreground), getting their appropriate share of CPU time. While the DOS session facilitates the move to OS/2, product development is far easier when you have tools that run in, and take advantage of, OS/2's multitasking environment. For instance, if you have a UNIX-like grep command that only runs under DOS, you will only be able to run it in the DOS session, and it will be frozen whenever the DOS session is pushed into the background. If such a command were written for OS/2, it would not only continue to run in a background OS/2 session, but you could have it take advantage of threads──multitasking within the process itself──by assigning, for example, one thread to open and search a file, and another to write the results to the output. It will not only cooperate with other OS/2 processes competing for CPU attention and machine resources, but it will also be significantly more efficient than its single thread DOS counterpart. While the DOS session can provide almost a full 640Kb DOS environment, the limitations of trying to debug and run large programs under it are the same as those found under MS-DOS 3.x. It may be impossible to debug a program with Microsoft CodeView(R) debugger in the DOS session if the program is too large, or if too many modules are compiled for debugging. This is never an issue, however, when debugging large programs with the protected-mode version of CodeView, due to OS/2's virtual memory. Startup Requirements Now that you've decided to develop programs under OS/2, what do you really need in order to get started? And what fits in that special "it would sure be great──if I could afford it" category? Our goal is to become as proficient as possible at writing OS/2 applications. However, there really are two ways to get there, one sufficient and one preferred. It's not unlike the difference between flying coach and first class. The sufficient method includes everything necessary to successfully write OS/2 programs. Preferred (or first class) is definitely more expensive but it can make the difference between getting up and running, and getting up and running very quickly. Either way, to run and utilize OS/2 you need a machine with at least an Intel 80286 or 80386 CPU; 2.5Mb of RAM; 20Mb of disk space; a high-density disk drive (either a 1.44 megabyte 3.5" or a 1.2 megabyte 5.25"); and, of course, a video monitor and keyboard. The following hardware is preferred (and will immediately prove valuable): 3+ megabytes of RAM; 40+ megabytes of disk space; IBM(R) EGA, VGA, or 100 percent compatible display adapter (the EGA adapter should have 128Kb of RAM); and a mouse. When it comes to software, the choices are far more varied. To adequately develop programs under OS/2, you'll need the following tools: a compiler (C, Pascal, etc.) that provides OS/2 support; an assembler; a protected- mode linker; a protected-mode program editor; a protected-mode debugger; other utilities such as make, grep, etc.; OS/2 Version 1.0 (Version 1.1, which includes the Presentation Manager, will do, but it's not necessary to get started). As you're probably aware, most of the Microsoft languages already offer versions that will produce protected-mode executables. This includes Microsoft C Optimizing Compiler Version 5.1, a "bound" application capable of producing programs that run under both operating environments (that is, it runs in either real or protected mode). If you are an experienced C programmer, you'll have a number of assembly language subroutines. Microsoft Macro Assembler Version 5.1 (MASM) will fill the bill for an assembler (like C 5.1, it is a bound application). The new Microsoft incremental linker, LINK4, which can incrementally (and therefore more quickly) link object files to produce an executable for either OS/2 or DOS, is included with both Microsoft C 5.1 and MASM 5.1. The Microsoft Editor, a powerful protected-mode program editor, is also included with both MASM and Microsoft C. I should mention, however, that other excellent protected-mode editors, such as Lugaru's Epsilon(TM) and the ME Editor from Magma Systems are now available or beginning to appear. Toolkit or SDK? Microsoft offers two ways of getting the necessary tools for OS/2 program development. The Microsoft OS/2 Software Development Kit, known as SDK (see "Exploring the OS/2 SDK"), offers a comprehensive collection of virtually every tool needed, from OS/2 (Versions 1.0 and 1.1──the latter including Presentation Manager and LAN Manager) to the C compiler and assembler, to utilities, etc. This is the preferred package──but the cost of the SDK ($3,000) can be prohibitive to the individual programmer or developer. The alternative is to purchase each individual tool you need. MSJ readers who already own the most recent versions of Microsoft C and/or MASM have a head start, since many of the other required tools come with the products. These, plus OS/2 itself, are adequate for developing OS/2 programs. You will also need to purchase the Microsoft OS/2 Programmer's Toolkit (PTK). The PTK (see "Exploring the OS/2 Programmer's Toolkit"), which is also included in the SDK, contains many of the essential tools and example programs found in the SDK──most importantly the complete Programmer's Reference guide to all of the OS/2 API calls. If you don't expect to develop Presentation Manager or LAN Manager applications right away (a reasonable expectation if you are just getting started), you might consider purchasing the PTK as a less expensive (about $300) alternative to the SDK, in addition to the other required tools. Note that the PTK requires a high-level language compiler (such as C 5.1), an editor, and OS/2 itself. Installing OS/2 Once you're ready to install OS/2, you'll encounter a number of options, primarily hinging on whether you already have MS-DOS on the machine on which you're installing OS/2. These options let you: ■ Default to booting OS/2 (boot DOS from a floppy). ■ Default to booting DOS (boot OS/2 from a floppy). ■ Have the system prompt you for which operating system to run at boot time. (Dual-boot mode, available with early SDK versions of OS/2 and some other manufacturers' versions──for example, Zenith's; the operating system can be booted from the hard disk.) If you expect to rarely boot DOS again, the first option might be a viable one. The last option is usually the most convenient. Keep in mind that the DOS session will still be available to you when OS/2 is running; the option only refers to booting either MS-DOS 3.x or later or OS/2 from the hard disk. Not all versions of OS/2 provide a dual-boot mode option of installation, and dual-boot will not be supported in retail versions of OS/2 sold by IBM. For the sake of simplicity, I will assume you want to install OS/2 on your MS-DOS development machine, and still be able to run MS-DOS free and clear of OS/2-in other words, the dual-boot installation option. In actuality, it doesn't matter which installation method is chosen-whichever is handiest should be the guiding line. While there may be other steps involved, depending on the number and size of the hard disks in your system, installing OS/2 basically involves booting the OS/2 Installation Disk and answering a few questions. (It's infinitely less complex than installing C 5.1, with its plethora of options.) The installation program will prompt you for other disks as needed. And that's it. If you install OS/2 to include the dual-boot option, then the installation program reconfigures the boot sector of your drive. Once you reboot after successfully installing OS/2, the following message will appear on your screen: Boot: Enter=OS/2, ESC=DOS That's all that it takes to choose between OS/2 and DOS. If you chose to install OS/2 from either the hard disk or a floppy, then obviously OS/2 will come up directly. Exploring the OS/2 Files As you start exploring your system after installing OS/2, you'll find a number of changes. First of all, you'll find some additions to the subdirectory structure in the root of your boot drive, as shown in the diagram in Figure 1. Each directory has a specific use and meaning as indicated. It is possible that this directory structure may change somewhat in the future, but as it stands, it serves our purposes for discussion in this article. The installation program places a number of files in the root directory of your boot drive, or in one of the subdirectories mentioned above. While quite a few of these are OS/2 utilities (see "An OS/2 Command Reference"), a number are worth noting. First you'll find two hidden files, as in DOS environments: OS2BIO.COM and OS2DOS.COM. (Note the DOS hidden files, IBMBIO.COM and IBMDOS.COM, are untouched.) These will always be in the root directory if dual-boot mode was selected. Retail releases of OS/2, however, may have different names for these hidden system files. The next few changes are especially meaningful in light of what I said earlier about peaceful coexistence between DOS and OS/2. CONFIG.OS2 is similar to the CONFIG.SYS found under MS-DOS (see "Configuring OS/2"). OS/2 processes it at system startup. If you are not using the dual- boot mode, this file would be called CONFIG.SYS under OS/2. STARTUP.CMD is OS/2's counterpart to the MS-DOS AUTOEXEC.BAT. It is processed after CONFIG.SYS and lets you start other programs and processes and further modify the OS/2 environment. OS2INIT.CMD is the batch file that is run whenever you initiate a new protected-mode session, and its commands execute at the start of each session. AUTOEXEC.OS2, like AUTOEXEC.BAT its MS-DOS counterpart, runs upon initiation of the real-mode session and is used to configure the environment of the DOS session. This file retains its AUTOEXEC.BAT filename under OS/2 when the dual-boot mode is not used. Two of these files are OS/2 batch files. OS/2 requires that batch files have a CMD extension to distinguish them from their DOS counterparts. The following are among the remaining files added by the installation program, and require special note: CMD.EXE is the OS/2 command interpreter (akin to COMMAND.COM under MS-DOS). HARDERR.EXE is the OS/2 critical error handler. PMSHELL.EXE is the Presentation Manager Task Manager (for OS/2 Version 1.1). SHELL.EXE is the OS/2 Program Selector (for OS/2 Version 1.0). SWAPPER.DAT is the OS/2 virtual memory swap file. SWAPPER.EXE is the OS/2 virtual memory manager. Configuring OS/2 There are several ways to configure OS/2 via CONFIG.OS2 (see "Configuring OS/2"). You'll want to have loaded the appropriate device drivers (via DEVICE=), and you'll want paths to both the real-mode command processor (SHELL= as under MS-DOS) and to the protected-mode command processor. The latter usually appears as shown in Figure 2A. SHELL.EXE is the OS/2 Program Selector (provided with OS/2 Version 1.0), and CMD.EXE is the OS/2 command processor. Since one of SHELL.EXE's arguments is CMD.EXE, the former will use the latter for loading and running programs. In order to cover all bases, I should mention that SDKs shipped at the time of this writing (October, 1988) include installation programs for a beta version of Presentation Manager, which is included in OS/2 Version 1.1. The prerelease of the PM Shell is relatively functional, and if you install its Program Starter and Task Manager (both are PM's alternative to the Program Selector), your PROTSHELL entry will probably look like that shown in Figure 2B. In addition, the following are "musts" if you're going to use OS/2 for program development: ■ Use the OS/2 disk cache to gain a performance advantage. This is done by placing DISKCACHE=x into CONFIG.OS2, where x is the size in kilobytes of the disk cache. I recommend at least 256 (on a development machine with 2.5 meg of RAM). ■ To run the protected-mode version of CodeView, you will need IOPL=YES in CONFIG.OS2, which gives additional IO data privileges to programs like CodeView. ■ If you plan to use or create dynamic-link libraries, you should have LIBPATH=path, where path points to the location of DLLs (the installation program will take care of this for you in most versions). ■ If you want to conserve memory when the DOS session is unnecessary, set PROTECTONLY=YES in CONFIG.OS2. This prevents the DOS session from being allocated or run, even though you might have a SHELL= statement designating the real-mode command processor. As an alternative, you can keep the DOS session available but limit the memory allocated to it via RMSIZE=x, where x is the total memory up to 640Kb that can be allocated to it. ■ If you're running processes with a lot of threads, you can increase the number of threads available from OS/2 with THREADS=x, where x is the total number of threads available. Keep in mind that there is a slight memory overhead relative to the total number of threads allocated. RUN and START OS/2 lets you prestart programs via the RUN or START commands. RUN is a directive placed in CONFIG.OS2 that lets OS/2 initiate a program or process. For example, the line shown in Figure 2C will run the OS/2 print spooler when OS/2 is done processing CONFIG.OS2. Since CONFIG.OS2 lets you have more than one RUN statement in it, you can initiate a number of processes directly from CONFIG.OS2. One limitation to RUN, however, is that it is not capable of running CMD files-so that while you can run a program directly, you can't configure its environment. You get around this by running the CMD file as an argument to the OS/2 command processor, CMD.EXE. Thus, you could use a line like that in Figure 2D to start a process that needs a CMD file to initialize its environment. Alternatively, you could use the START command. START lets you initiate another session from the OS/2 command line. If you wanted to create a CMD file that initiated several processes (without having to initiate each one from the Program Selector), the CMD file might contain the lines shown in Figure 2E. If you are going to regularly start a series of programs, the preferred method is to place the START commands in STARTUP.CMD. Switching Programs Getting around in OS/2 is easy. You can press Ctrl-Esc at any time to bring up the Program Selector (the Task Manager under PM), which displays a configurable list of programs to initiate, and a list of sessions already active. You then select the session you wish to switch to, or the process you wish to start. Personally, I find it's easiest to switch between active sessions by pressing Alt-Esc. This lets you rapidly "hot-key" from session to session until you reach the one you want to work with. There is very little delay in this method even if you are hot-keying across the DOS session, regardless of the hardware you're using. This is fairly rapid on a 10 MHz 286 machine with 2.5Mb of memory and over a half-dozen sessions running, and it's especially fast when you consider the overhead that OS/2 imposes on the processor when switching from protected mode to real mode and back. Yet this is exactly what happens when you hot-key from a protected- mode session to the DOS session to another protected-mode session with Alt- Esc. Using OS/2 Once you have OS/2 up and running you can start exploring some of its advantages over DOS. As shown in "An OS/2 Command Reference," a number of carry-over commands from DOS have been enhanced to accept multiple arguments. In addition, some of the substitution facilities that were available only in BAT files under DOS are available on the command line under OS/2. If the environment has this entry: LIB=c:\MSC\OS2\LIB followed by the command CD %LIB% then it would change to the \MSC\OS2\LIB directory on the C: drive. Under DOS, this would only have worked in a BAT file. Another convenience is the DETACH command. DETACH lets you initiate a background process from the command line. For instance, DIR | SORT > dsort.txt initiates a process that runs DIR, pipes the result to SORT, and redirects the output to a file. So, DETACH DIR | SORT > dsort.txt places the entire process in a background session. DETACH always assumes that the process it runs does not require any keyboard input. After initiating the process, DETACH prints the process' ID for the task and returns to the process that started it──in this case, the OS/2 command line. Thus, it's easy to relegate complex tasks to the background and let the user proceed as usual. Another advantage becomes apparent whenever you need to perform an OS/2 command line operation while running a program. You can always Ctrl-Esc to the Program Selector to bring up another OS/2 command line. This is especially convenient when you need to format floppy disks or delete some files without leaving a program. The formatting, once started, will proceed smoothly in the background after you Alt-Esc back to the session you left earlier. If you find this convenient, you may want to make a habit of initiating an OS/2 command session when you begin work (or RUN one from CONFIG.OS2 before the Program Selector appears). Another OS/2 plus is that the command-line facilities have been enhanced in order to allow more complex expressions, as mentioned in "An OS/2 Command Reference." For instance, the command line in Figure 3 would conditionally build a sorted list of protected-mode utilities-conditionally, because the DIR command would not be issued if the drive and directory change were not successful. Users can run the entire process as a background task by placing a DETACH command in front of the entire command. Additionally, the foreground session would return on the original drive in the original directory since the drive and directory change would take place in the background session; remember, such changes are local to a session and do not affect other sessions. Finally, you'll find that OS/2 CMD files have also been enhanced over MS-DOS BAT files, especially with the new SETLOCAL and ENDLOCAL commands. SETLOCAL saves the current state of the logged-in disk drive and current working directory; ENDLOCAL restores these to the state saved by SETLOCAL. Thus you could write a CMD file like this: @echo off setlocal c: && cd\ha && ha endlocal This would log in drive C, change to the \HA directory, and run the program HA.EXE. When the program has completed execution, the original drive and directory will be restored to the session. These commands are similar to the PUSHDIR/POPDIR commands available in some UNIX shell environments. No Caffeine The best reason for not using OS/2 as a development environment is that it'll eliminate coffee breaks during long program compilations; you can always do something else! While its multitasking capabilities are extensive, OS/2 is an extremely productive, in fact fun, work environment. Coupled with the applications that are taking advantage of it, this fact will draw large numbers of users to OS/2, and it's a good reason for you to begin using OS/2 to develop programs now, if you haven't already started. In the next article of this series, we'll discuss setting up the C compiler and explore the issues surrounding the writing of your first protected-mode application: a multithreaded version of HELLO.C. (As this article was going to press, IBM and Microsoft announced the official release of OS/2 Version 1.1. The new installation of OS/2 Version 1.1 and Presentation Manager changes some of the setup features of earlier releases. For example, dual-boot mode is no longer an available option, the system files IBMBIO.COM and IBMDOS.COM have been renamed to OS2LDR and OS2KRNL respectivley, and the initial directory structure may be somewhat different than that described here.──Ed.) Figure 1: Standard OS/2 Directory Structure ┌───────────────────\(Root)────────┬──────────┐ │ │ │ SPOOL (other) ┌──────────┬──────────OS/2─────────┬──────────┐ │ │ │ │ │ BIN DEV LIB PBIN RBIN \OS2 for OS\2 and Presentation Manager files \OS2\BIN for bound utilities \OS2\DEV for storing device drivers \OS2\LIB for dynamic-link libraries \OS2\PBIN for protected-mode only utilities \OS2\RBIN for real-mode only utilities \SPOOL used by the OS/2 print spooler Figure 2: Examples of OS/2 CONFIG.SYS Entries 2A PROTSHELL=C:\SHELL.EXE C:\CMD.EXE /K C:\OS2INIT.CMD 2B PROTSHELL=C:\OS2\PMSHELL.EXE C:\OS2\PBIN\CMD.EXE /K C:\OS2INIT.CMD 2C RUN=c:\os2\pbin\spool.exe 2D RUN=c:\cmd.exe /c c:\os2\pbin\mestart.cmd 2E start "HyperACCESS" /c ha start "Paradox" /c pdoxos2 -multi start "OS/2 Command Line" Figure 3: Under OS/2 multiple commands can be executed from the command line. c: && cd \os2\pbin && dir *.exe *.cmd | sort > tools.lst ─────────────────────────────────────────────────────────────────────────── An OS/2 Command Reference ─────────────────────────────────────────────────────────────────────────── Most of the traditional MS-DOS commands have been retained in OS/2's protected-mode environment. However, several commands have been enhanced and a number of new ones have been added. The following commands have been enhanced over their DOS and real-mode counterparts to allow multiple arguments: ■ DEL ■ MD/MKDIR ■ DIR ■ RD/RMDIR ■ TYPE ■ VOL For instance you can delete all of the .C, .H, and .ASM files in a directory with: DEL *.c *.h *.asm (Obviously, I'm not recommending that you try out this particular sequence.) In addition, the MODE command now allows for the setting of time-out values and hardware handshaking details. The following are new commands, available in OS/2's protected mode: && The && is used to conditionally chain commands: dir x && del x This will delete x only if the first command was successful. & The & can unconditionally chain commands: dir x & del x Here, OS/2 will attempt both commands. The outcome of one does not depend on the other. || can conditionally execute commands, so that in: copy x y || copy a y The second command will only execute if the first command is unsuccessful. () Parentheses can group commands for chaining and execution: del x || (attrib -r x & del x) In this case, if the deletion is unsuccessful (because x's read/write attribute is on), OS/2 will attempt to clear the read/write attribute of x and, if successful, delete it. ^ The ^ will normalize special characters. Thus: ^> file.txt will be seen as "> file.txt". OS/2 will not see the > as a special character and will not attempt redirection. ANSI [ON | OFF] This command toggles support for ANSI escape sequences. It is provided in lieu of ANSI.SYS, the real-mode device driver. CMD [options] [arguments] Used to run the protected-mode command processor. Options include /c to ensure return to the primary command processor, and /k to retain the new command processor in memory after executing the specified commands. CREATEDD Creates a memory dump diskette. DETACH [program [arguments]] This command runs protected-mode programs and processes in the background. However, detached programs will not be able to get keyboard input and all screen output from them is lost. A process ID is returned when successfully initiated. DPATH path;... Similar to the APPEND command in real mode, DPATH creates a path applications can search for opening data files, via the environment variable, DPATH. Applications that use DPATH must search the environment to obtain its values. ENDLOCAL For batch file usage, this command restores the current working directory and environment to the one saved by a previous SETLOCAL command. EXTPROC This command defines an alternative command processor for use in processing a batch file. KEYB Specifies an alternative keyboard layout for users outside of the United States. SETLOCAL For batch file usage, this command preserves the current drive, directory, and environment for later restoration by ENDLOCAL during a batch file run. SPOOL Initiates the background print spooler. START "session name" [/c] [command [options]] Initiate a new protected-mode session. The session's name (specified as an argument) will appear in the Program Selector's menu. TRACE <on | off> [code[,...]] Toggles system tracing on or off. TRACEFMT Displays the contents of the system trace buffer in LIFO order. ─────────────────────────────────────────────────────────────────────────── Configuring OS/2 ─────────────────────────────────────────────────────────────────────────── As with CONFIG.SYS, under MS-DOS users can extensively configure OS/2 by placing commands in CONFIG.OS2. Several of these, such as break and fcbs, are virtually identical to their DOS counterparts and are still available in OS/2's real mode. Others──such as buffers, device, and shell──are also available in protected mode. Device is used to install OS/2 device drivers. In the context of OS/2's CONFIG.OS2 file, shell designates the real-mode command processor, as it did for MS-DOS' COMMAND.COM. In addition to these, however, there are a number of new commands available for configuring OS/2. DISKCACHE This command enables the diskcache and sets its size. By default the cache is not enabled. IOPL Gives protected-mode applications requesting them additional data IO privileges. IOPL=YES must be in your CONFIG.OS2 file to run the protected- mode version of CodeView, CVP. The default is YES. LIBPATH Similar in form to PATH and DPATH, LIBPATH is a CONFIG.OS2 command that specifies the location of dynamic-link libraries. The default LIBPATH is the root of the boot drive. MAXWAIT This command sets the maximum wait time, in seconds; this is the time that must elapse before the scheduler increases a process' priority when it hasn't been recognized. The default is 3 and can range from 1-255 seconds. MEMMAN This command toggles virtual-memory swapping. Turning swapping on will let OS/2 run more processes than will fit into memory, and automatically turn move on. Having move on will let OS/2 temporarily move data segments. However, performance tends to improve with swapping off, up to a point. The default is SWAP, MOVE if the boot drive is a hard disk, and noswap/nomove if the boot drive is a floppy. PAUSEONERROR Lets OS/2 display error messages while processing CONFIG.OS2. The default is YES. PRIORITY This command influences the manner in which a process gets time priority. OS/2 classifies priorities into three classes: time-critical, normal, and idle-time. There are 32 priority levels in each class, and priorities in the normal class can be adjusted. Setting PRIORITY=ABSOLUTE in CONFIG.OS2 stops the system from dynamically changing the priority of processes within the normal class. If PRIORITY=DYNAMIC, OS/2 will try to determine which process most needs CPU resources at any given time slice. If a process seems to have a lower priority than it should, OS/2 will increase the priority level of that process. The default is PRIORITY=DYNAMIC. PROTECTONLY Lets you run both protected-mode and real-mode applications if PROTECTONLY=NO. Although you can't run real-mode applications if PROTECTONLY is set to NO, this will free some memory used by the real-mode compatibility box (up to 640Kb). The default is NO. PROTSHELL This command designates the protected-mode command processor and program selector. The default is: PROTSHELL=SHELL.EXE CMD.EXE /K OS2INIT.CMD REM Used to place comments in CONFIG.OS2. RMSIZE Configures the amount of memory reserved for the real-mode environment. The default is based on the total memory available, usually the amount of memory installed below 1024Kb (either 512Kb or 640Kb). RUN This command lets protected-mode programs or processes start during system initialization. Thus with several RUN= commands in CONFIG.OS2, a number of programs can be started before the OS/2 Program Selector appears. Note that OS/2 processes all DEVICE= directives before any programs are run, and that batch files (.CMD) are not runnable from CONFIG.OS2. SWAPPATH The swap file is the temporary file OS/2 uses to implement its virtual memory scheme. This command specifies the location of the swap file (which should be placed on a hard disk). Note that the minimum swap file size is 640Kb, and that this command is ignored if swapping is disabled (see MEMMAN). THREADS A thread is the portion of an application or process that OS/2 can schedule. A process always contains at least one thread of execution, and can contain multiple threads. These act like small programs that perform particular tasks in each process. The command sets the maximum number of threads available at one time. Note that OS/2 uses about 14 threads. The system sets the default and each additional thread uses a small amount of memory. TIMESLICE A time slice is the interval of time OS/2 uses to schedule the threads of a process. The TIMESLICE command specifies the minimum and maximum amounts of time in milliseconds (thousands of a second) that OS/2 can dedicate to one process before checking on other processes. If only one value is specified, both values will be set to it. OS/2 sets the default. The minimum value must be greater than 31 and the maximum must be greater than or equal to the minimum. Setting TIMESLICE=500 will make the scheduler wait a half-second before checking other threads for a priority higher than the threads currently executing. TRACE OS/2 keeps a record of the actions it takes processing hardware interrupts and functions, which can be helpful during program development. System tracing, as this is called, can be toggled via TRACE=ON or TRACE=OFF in CONFIG.OS2. You can trace individual types of events by specifying their related event codes after the ON. You can issue TRACE=ON followed by TRACE=OFF X,Y where X and Y are event codes corresponding to events that you do not want traced. Tracing is OFF by default. The event codes must fall into the range 0-255. TRACEBUF This command sets, in kilobytes, the size of the trace buffer to be allocated for the TRACE command. In addition to the commands mentioned above, the following commands are also available, but are really only applicable outside of the U.S. CODEPAGE Selects a code page. COUNTRY Selects time, date, and currency conventions. DEVINFO Prepares a device for use with code page. Note that OS/2 ignores the FILES and LASTDRIVE commands found in DOS CONFIG.SYS files. Sample CONFIG.OS2 Files The following are two examples of CONFIG.OS2. The first is for OS/2 Version 1.0, while the second, for OS/2 Version 1.1 including the PM, is nearly identical to that produced by the installation program. Sample 1: REM CONFIG.OS2 FOR OS/2 v.1.0 buffers=50 shell=c:\command.com /P /E:500 protshell=c:\shell.exe cmd.exe /k c:\os2init.cmd libpath=c:\os2\dll device=c:\os2\dev\mousea03.sys device=c:\os2\dev\pointdd.sys device=c:\os2\dev\com01.sys run=c:\os2\pbin\spool.exe c:\os2\spool /o:lpt1 break=on files=25 IOPL=YES TRACE=ON TRACEBUF=8 run=c:\cmd.exe /c c:\cset diskcache=512 Sample 2: REM CONFIG.OS2 FOR OS/2 v.1.1 and PM buffers=30 shell=c:\os2\rbin\command.com /P /e:1024 c:\os2\rbin rem the following entry must be all on one line! protshell=c:\os2\pmshell.exe c:\os2\pbin\cmd.exe /k c:\os2init.cmd rmsize=640 protectonly=NO break=OFF fcbs=16,8 threads=64 iopl=YES libpath=c:\os2\dll set path=c:\os2;c:\os2\bin;c:\os2\pbin memman=SWAP,MOVE diskcache=64 maxwait=3 swappath=c: device=c:\os2\dev\mousea03.sys device=c:\os2\dev\pointdd.sys device=c:\os2\dev\pmdd.sys device=c:\os2\dev\com01.sys device=c:\os2\dev\ega.sys ─────────────────────────────────────────────────────────────────────────── Exploring the OS/2 SDK (Software Development Kit) ─────────────────────────────────────────────────────────────────────────── The OS/2 Software Development Kit (SDK) is the single most comprehensive resource for OS/2 development tools and materials. While it costs $3,000, it's so complete that you may seriously want to consider it, depending on what types of OS/2 applications you (or your company) intend to develop. In essence, the SDK puts all the tools together in one place. If you expect to develop Presentation Manager or LAN Manager applications rather quickly, the SDK contains everything you need in one purchase. On the other hand, if you won't be developing Presentation Manager or LAN Manager programs right away, you may want to consider purchasing the necessary components separately. Below is a list of the components included in the SDK. Note that in addition to the Presentation Manager, the LAN Manager, and the OS/2 Programmer's Toolkit, both MS Windows and the Windows SDK are included. The SDK includes these since the Windows programming interface is very similar to that provided by the Presentation Manager. The rule of thumb is: get to know the Windows programming interface. Once you've done so, programming Presentation Manager will be considerably easier. ■ MS OS/2 Version 1.0; MS OS/2 Version 1.1 (including Presentation Manager); MS Windows 2.03; MS OnLine; MS OS/2 Version 1.1 Toolkit. ■ Tools: Microsoft C Version 5.1; Microsoft Assembler Version 5.1; CodeView (real and protected mode); Utilities: Editor, Linker, Librarian, Bind (for creating dual-mode applications), Make, Grep, Incremental Linker. ■ QuickHelp for OS/2 ■ OS/2 Programmer's Toolkit (PTK) Version 1.0. ■ MS OS/2 LAN Manager: User's Guide; Programmer's Reference. ■ MS OS/2 Presentation Manager: Conversion Guide; Reference, Volume I; Reference, Volume II; Reference, Addendum; "Programming the OS/2 Presentation Manager" (and companion disk), by Charles Petzold. ■ MS Windows Version 2.03 Software Development Kit (SDK): Learning Guide; Tools, Style Guide, Extensions; Programmer's Reference Guide; Windows Applications Tools. ■ Inside OS/2 by Gordon Letwin. ■ A subscription to Microsoft System Journal. ─────────────────────────────────────────────────────────────────────────── Exploring the OS/2 Programmer's Toolkit ─────────────────────────────────────────────────────────────────────────── The Microsoft OS/2 Programmer's Toolkit contains many of the tools required to build complex protected-mode applications under OS/2. The Toolkit complements and completes the minimal program development tools necessary to writing programs under OS/2. Included are: ■ Setup Guide and User's Guide ■ Programmer's Learning Guide and Programming Tools introduces writing C programs under OS/2. It presupposes experience in both. There are also discussions of how to create and build dynamic-link libraries. ■ Programmer's Reference includes all of the OS/2 system calls, etc. ■ QuickHelp ■ Sample Program examples demonstrate: memory allocation; multithreaded programming: thread examples in assembler and C, passing arguments to threads, using critical sections in threads, using DosKill and DosExit, suspending threads; controlling the speaker; accessing machine configuration and machine mode; accessing country information; aliasing code/data segments; accessing the machine date and time; Dynamic-Link Libraries in C and assembler; use of Exitlist; accessing volume information; accessing environment information; hello.c for OS/2; accessing process information; keyboard input; using monitors, pipes, and queues; moving files; accessing file handle information; handling sessions; shared memory; signal handling; register VIO subsystem; simple terminal emulation; simple screen editing; multithreaded text searching; directory navigation; listing files; LIFE game; EGA graphics; mouse interface. ■ Application Development Utilities includes, in addition to the appropriate header files and libraries, utilities for: creating dual- mode applications; searching libraries; searching files; creating import libraries; dumping .EXE headers; running the system trace; binding messages; dumping shared memory and files; searching for files; setting the keyboard delay and repeat rates. ■ A Subscription to MS(R) OnLine and two hours of usage. ────────────────────────────────────────────────────────────────────────────

Volume 4 - Number 2

──────────────────────────────────────────────────────────────────────────── Exploring Vector Fonts with the OS/2 Graphics Programming Interface Charles Petzold Let's begin with a question: What graphics programming language stores fonts as lines and curves (rather than bitmaps) and thus allows fonts to be arbitrarily stretched, rotated, outlined, filled with different patterns, or even used as clipping areas? One answer is obviously PostScript(R), Adobe Systems' page composition language implemented on many high-end laser printers (beginning with the Apple(R) LaserWriter(R)) and Allied Corporation's Linotronic(R) phototypesetters. Over the past few years, PostScript has become the language of choice for computer manipulation of fonts and text. An equally valid answer is GPI--the Graphics Programming Interface (referred to herein as GPI) component of the OS/2 Presentation Manager. This article shows you how to work with vector fonts in GPI and demonstrates many PostScript-like techniques. As we'll see, GPI has facilities to do virtually everything with fonts that you can do with PostScript. However, GPI does have a deficiency that I will discuss at the end of this article. The Trouble with Text The display of text is always the most problematic part of a graphics programming system. Unlike lines and polygons (which are merely mathematical constructs), text is rooted in a long tradition of aesthetic typography. In any computer graphics system, the goal must always be to display text that is as pleasing and as easy to read as a well-printed book. Yet, most computer output devices (such as video displays and printers) are digital media. The subtly shaped and rounded characters that comprise traditional fonts must be broken down into discrete pixels for storage and then reassembled on the output device. This often causes distortions in the appearance of the text. One major advantage of using a computer for this job is versatility. We can use a wide variety of fonts in various sizes and characteristics and modify these fonts for display. The extent to which we can modify fonts depends on the way in which the fonts are stored in memory. Images and Vectors A font is generally stored in computer (or printer) memory in one of two very different ways. First, a font can be stored as an image or bitmap. Each character of the font is simply a rectangular array of bits. The 0 bits generally correspond to the background around the character and the 1 bits correspond to the character itself. Second, a font can be stored in a vector or outline format in which each character is defined as a series of lines and curves that enclose areas. The character is displayed by drawing the outline on the output device and filling in the enclosed areas. Image and vector fonts have distinct advantages and disadvantages. Image fonts are always created for specific font sizes and specific device resolutions. The size of a particular image font cannot easily be changed. (For example, enlarging an image font by doubling the rows and columns of pixels often emphasizes the jaggedness of the characters.) Also, image fonts cannot be rotated except possibly by 90 degree increments. Vector fonts are much more malleable. Because they are defined as a series of lines and curves, vector fonts can be stretched or compressed to any size and can be rotated to any angle. Vector fonts are not tied to a particular output device resolution. In general, however, image fonts are more legible than vector fonts. Various techniques are used to design image fonts so they fool the eye into thinking the characters are smoother than they actually are. Vector fonts--particularly when displayed on low-resolution devices and scaled to small font sizes--can be adjusted only by mathematical algorithms, which currently are less capable than human font designers. Another advantage of image fonts is performance since vector fonts usually require much more processing time to draw each character. Most conventional laser printers store fonts as images, either within the printer or in font cartridges. The printer is restricted to specific font sizes and the characters cannot be arbitrarily rotated. Much more versatile are the fonts stored in PostScript-based printers. These fonts are stored as vectors. PostScript fonts can be stretched or compressed to any size, they can be arbitrarily rotated, filled with various patterns, and used for clipping. The GPI Fonts GPI can, of course, take advantage of fonts that are stored in and supported by output devices such as laser printers. But it also includes its own support of both image and vector fonts. The image fonts are expected because they are particularly suited for low-resolution video displays and dot matrix printers. Image fonts are an important part of most graphics programming systems (such as Microsoft Windows GDI). The addition of vector fonts in GPI is a real treat. GPI can use these vector fonts with any output device. Thus, various font techniques that previously have been restricted to PostScript printers are now possible with other laser printers and even the video display. OS/2 Version 1.1 is shipped with three resource-only dynamic-link libraries with FON extensions. fIn addition, the video display (DISPLAY.DLL) and printer device drivers may also contain fonts designed specifically for the device. For example, the default System Proportional font is stored in DISPLAY.DLL. If you want to use any of the fonts in the FON files, you must install the fonts from the Presentation Manager Control Panel program. It is only necessary to install one font from each of the three files, and you only need do this once. Each font has a face name, which is shown in quotation marks in Figure 1. Each of the image fonts is available in several point sizes and for several output devices: the CGA, EGA, VGA (and 8514/A), and IBM(R) Proprinter. GPI can synthesize variations of these fonts, such as italic or boldfaced versions. Vector fonts, however, need not be designed for a particular output device and point size because they can be arbitrary scaled. You'll note that italic and boldface versions of the vector fonts are also included. The vector fonts in GPI are similar in style to the Courier, Helvetica(R), and Times(R) fonts included in most PostScript printers. A little exploration of the font files will reveal that vector fonts are encoded as a series of GPI drawing orders. When drawing text with these fonts, GPI translates the drawing orders into GPI functions, usually GpiPolyLine to draw straight lines and GpiPolyFilletSharp to draw curves. The VECTFONT Program The VECTFONT program demonstrates the use of GPI vector fonts. For purposes of clarity, I've divided the program into several modules. The files that comprise the basic shell of the program are VECTFONT, VECTFONT.LNK, VECTFONT.DEF, VECTFONT.H, and VECTFONT.C. (These files are not shown here, but can be downloaded from any of our bulletin boards--Ed.) Figure 2 shows the code fragment from VECTFONT.C that is responsible for creating a PS, and for sizing client windows. The VECTFONT Display menu lists 16 options. The first option (to display nothing) is the default. The other 15 options correspond to routines in the VF01.C through VF15.C files (described below). The VF00.C file (not shown here) contains some helper functions used by the routines in VF01.C through VF15.C. VECTFONT creates a micro presentation space during the WM_CREATE message using page units of PU_TWIPS. (Twips is a fabricated word standing for 20th of a point. A printer's point size is 1/72 inch, so page units correspond to 1/1440 inch.) VECTFONT then modifies the page viewport rectangle so that a page unit corresponds to 1 point, which is the default coordinate system in PostScript. Although VECTFONT displays output to the screen, vector fonts are obviously better suited for laser printers. As you will see, the appearance of the fonts--even at 24 point sizes--is not nearly as good as the image fonts. You will also notice that several of the demonstration routines in VECTFONT require a few seconds to run. For anything other than a demonstration program, you would probably want to use a second thread of execution to avoid holding up message processing. Figure 2: case WM_CREATE:\| hdc = WinOpenWindowDC (hwnd) ; // Create PS use Twips page units sizl.cx = 0 ; sizl.cy = 0 ; hps = GpiCreatePS (hab, hdc, &sizl, PU_TWIPS | GPIF_DEFAULT | GPIT_MICRO | GPIA_ASSOC) ; // Adjust Page Viewport for points GpiQueryPageViewport (hps, &rcl) ; rcl.xRight *= 20 ; rcl.yTop *= 20 ; GpiSetPageViewport (hps, &rcl) ; hwndMenu = WinWindowFromID ( WinQueryWindow (hwnd, QW_PARENT, FALSE), FID_MENU) ; return 0 ; case WM_SIZE: ptlClient.x = SHORT1FROMMP (mp2) ; // client width ptlClient.y = SHORT2FROMMP (mp2) ; // client height GpiConvert (hps, CVTC_DEVICE, CVTC_PAGE, 1L, &ptlClient); return 0 ; Selecting an Outline Font To use an outline font in a Presentation Manager program, you must first create a logical font and then select the font into your presentation space. The GpiCreateLogFont function creates a logical font and associates the font with a local ID (a number between 1L and 254L that you select). This function requires a pointer to a structure of type FATTRS (font attributes) that specifies the attributes of the font you want. To create a vector font, most of the fields in this structure can be set to zero. The most important fields are szFacename (which is set to the face name of the font, one of the names in the last column of Figure 1) and fsFontUse, which is set to the constant identifiers FATTR_FONTUSE_OUTLINE and FATTR_FONTUSE_TRANSFORMABLE, combined with the C bitwise OR operator. You may prefer using the CreateVectorFont function in VF00.C to create a vector font. This function requires only the presentation space handle, the local ID, and the face name: CreateVectorFont(hps, lcid, szFacename); After you create a logical font (using either GpiCreateLogFont or CreateVectorFont), you can select the font into the presentation space: GpiSetCharSet(hps, lcid); The lcid parameter is the local ID for the font. After the font is selected in the presentation space, you can alter the attributes of the font with various functions described below, obtain information about the font by calling GpiQueryFontMetrics, GpiQueryWidthTable, and GpiQueryTextBox; and use the font for text output with one of the text functions such as GpiCharStringAt. When you no longer need the font, you reselect the default font into the presentation space: GpiSetCharSet(hps, LCID_DEFAULT); and then delete the local ID associated with the font: GpiDeleteSetId(hps, lcid); In VECTFONT I always use the identifier LCID_MYFONT for the local ID. This is defined in VECTFONT.H as 1L. Scaling to a Point Size When you call GpiSetCharSet to select a vector font into the presentation space, the initial width and height of the font are based on the GPI character box, which defines a character width and height in page units. The default character box is based on the size of the default system font. You can change the character box size by calling GpiSetCharBox. An important point to note is that to get a correctly proportioned vector font, you must change the character box size. Do not use the default. Generally you set the height of the character box to the desired height of the font. If you want a vector font to have a normal width, set the width of the character box equal to the height. For a skinnier font, set the width of the character box less than the height; for a fatter font, set the width greater than the height. If you're working in page units of PU_PELS, you must also adjust the character box dimensions to account for differences in horizontal and vertical resolution of the output device. For this reason, it's much easier to work with vector fonts if you use one of the metric page units (PU_LOENGLISH, PU_HIENGLISH, PU_LOMETRIC, PU_HIMETRIC, or PU_TWIPS). With these page units, horizontal and vertical page units are the same. For example, suppose you're using page units of PU_TWIPS. This means that one page unit is equal to 1/20 of a point or 1/1440 inch. After selecting a vector font into the presentation space, you want to scale the font to 24 points. You first define a structure of type SIZEF: SIZEF sizfx; The two fields of this structure, named cx and cy, are interpreted as 32-bit FIXED numbers; that is, the high 16 bits are interpreted as an integer, and the low 16 bits are interpreted as a fraction. If you want to scale the vector font to a 24-point height, you can use the MAKEFIXED macro to set to the fields of the structure like this: sizfx.cx = MAKEFIXED(24 * 20, 0); sizfx.cy = MAKEFIXED(24 * 20, 0); Multiplying by 20 is necessary to convert the point size you want to twips. Then call GpiSetCharBox: GpiSetCharBox(hps, &sizfx); After setting the character box, any character or text dimensions you obtain from GpiQueryFontMetrics, GpiQueryTextBox, and GpiQueryWidthTable will reflect the new font size. The ScaleVectorFont routine in VF00.C can help in scaling a vector font to a desired point size. The function will work with any page units, even PU_PELS. The second and third parameters to ScaleVectorFont specify a point size in units of 0.1 points. (For example, use 240 for a 24-point size.) If you want a normally proportioned vector font, set the third parameter to ScaleVectorFont equal to the second parameter. The VF01.C file in Figure 3 shows how the functions discussed so far can be used to display all the available vector fonts in 24-point size. You can run the function in VF01.C by selecting the 24 Point Fonts option from the VECTFONT Display menu. The source code and its results are shown in Figure 3. Figure 3: /*---------------------------------------- VF01.C -- Display 24-point vector fonts ----------------------------------------*/ #define INCL_GPI #include <os2.h> #include <string.h> #include "vectfont.h" VOID Display_24Point (HPS hps, LONG cxClient, LONG cyClient) { static CHAR *szFacename[] = { "Courier", "Courier Italic", "Courier Bold", "Courier Bold Italic", "Tms Rmn", "Tms Rmn Italic", "Tms Rmn Bold", "Tms Rmn Bold Italic", "Helv", "Helv Italic", "Helv Bold", "Helv Bold Italic" } ; static INT iNumFonts = sizeof szFacename / sizeof szFacename[0] ; FONTMETRICS fm ; INT iFont ; POINTL ptl ; ptl.x = cxClient / 8 ; ptl.y = cyClient ; for (iFont = 0 ; iFont < iNumFonts ; iFont++) { // Create font, select it and scale CreateVectorFont (hps, LCID_MYFONT, szFacename[iFont]) ; GpiSetCharSet (hps, LCID_MYFONT) ; ScaleVectorFont (hps, 240, 240) ; // Get font metrics for scaled font GpiQueryFontMetrics (hps, (LONG) sizeof (FONTMETRICS), &fm) ; ptl.y -= fm.lMaxBaselineExt ; // Display the font facename GpiCharStringAt (hps, &ptl, (LONG) strlen (szFacename[iFont]), szFacename[iFont]) ; GpiCharString (hps, 10L, " - abcdefg") ; GpiSetCharSet (hps, LCID_DEFAULT) ; // Clean up GpiDeleteSetId (hps, LCID_MYFONT) ; } } Arbitrary Stretching Besides scaling the vector font to a specific point size, you can also scale vector fonts to fit within an arbitrary rectangle. For example, you may want to scale a short text string to fill the client window. The function shown in VF02.C (see Figure 4) shows how this is done. You can run this function by selecting Stretched Font from VECTFONT's menu. The function in VF02.C displays the word "Hello!" in the Tms Rmn Italic font stretched to the size of the client window. The ScaleFontToBox function in VF00.C helps out with this job. This function first calls GpiQueryTextBox to obtain the coordinates of a parallelogram that encompasses the text string. The character box is then scaled based on the size of this text box and the rectangle to which the font must be stretched. The QueryStartPointInTextBox function in VF00.C determines the starting point of the text string (that is, the point at the baseline of the left side of the first character) within this rectangle. This point is used in the GpiCharStringAt function to display the text. Figure 4: /*---------------------------------------------------------- VF02.C -- Display vector font stretched to client window ----------------------------------------------------------*/ #define INCL_GPI #include <os2.h> #include "vectfont.h" VOID Display_Stretch (HPS hps, LONG cxClient, LONG cyClient) { static CHAR szText[] = "Hello!" ; static LONG cbText = sizeof szText - 1 ; POINTL ptl ; // Create font, select, and scale CreateVectorFont (hps, LCID_MYFONT, "Tms Rmn Italic") ; GpiSetCharSet (hps, LCID_MYFONT) ; ScaleFontToBox (hps, cbText, szText, cxClient, cyClient) ; QueryStartPointInTextBox (hps, cbText, szText, &ptl) ; GpiCharStringAt (hps, &ptl, cbText, szText) ; // Display text GpiSetCharSet (hps, LCID_DEFAULT) ; // Clean up GpiDeleteSetId (hps, LCID_MYFONT) ; } Mirror Images The character box, besides scaling the font to a particular size, can also flip the characters around the horizontal or vertical axis. If the character box height (the cy field of the SIZEF structure) is negative, the characters will be flipped around the baseline and displayed upside down. If the character box width (the cx field) is negative, the individual characters will be flipped around the vertical axis. Moreover, GpiCharStringAt will draw a character string from right to left. It's as though the whole character string is flipped around the vertical line at the left side of the first character. The function in VF03.C (see Figure 5) displays the same string four times, using all possible combinations of negative and positive character box dimensions. You can run this function by selecting Mirrored Font from the VECTFONT menu. Figure 5: /*------------------------------------------------------ VF03.C -- Display four strings in mirror reflections ------------------------------------------------------*/ #define INCL_GPI #include <os2.h> #include "vectfont.h" VOID Display_Mirror (HPS hps, LONG cxClient, LONG cyClient) { static CHAR szText[] = "Mirror" ; static LONG cbText = sizeof szText - 1 ; INT i ; POINTL ptl ; SIZEF sizfx ; // Create font, select and scale CreateVectorFont (hps, LCID_MYFONT, "Tms Rmn Italic") ; GpiSetCharSet (hps, LCID_MYFONT) ; ScaleFontToBox (hps, cbText, szText, cxClient / 2, cyClient / 2) ; ptl.x = cxClient / 2 ; // Center of client window ptl.y = cyClient / 2 ; for (i = 0 ; i < 4 ; i++) { GpiQueryCharBox (hps, &sizfx) ; if (i == 1 || i == 3) sizfx.cx *= -1 ; // Negate char box dimensions if (i == 2) sizfx.cy *= -1 ; GpiSetCharBox (hps, &sizfx) ; GpiCharStringAt (hps, &ptl, cbText, szText) ; } GpiSetCharSet (hps, LCID_DEFAULT) ; // Clean up GpiDeleteSetId (hps, LCID_MYFONT) ; } Transformations Unlike image fonts, vector fonts can be scaled to any size and sheared and rotated to any angle. The matrix transforms described in my article "OS/2 Graphics Programming Interface: An Introduction to Coordinate Spaces," MSJ (Vol. 3, No. 4), affect vector fonts in the same way they affect the display of other graphics primitives. In addition, GPI also supports several functions specifically for performing transforms on vector fonts. We've already seen how the GpiSetCharBox function allows font characters to be scaled. The GpiSetCharAngle rotates the font characters and the GpiSetCharShear performs x shear. Character Angle and Rotation By default, the baseline of the vector font characters is parallel to the x axis in world coordinates. You can change this by calling GpiSetCharAngle. This rotates the vector font's characters. The character angle is specified using a GRADIENTL structure, which has two fields named x and y of type LONG. Imagine a line connecting the point (0,0) to the point (x,y) in world coordinates. The baseline of each character is parallel to this line. The direction of the text is the same as the direction from (0,0) to (x,y). You can also think of this in trigonometric terms. The baseline of the text is parallel to a line at angle a measured counterclockwise from the x axis, where: a = arctan (y/x) and y and x are the two fields of the GRADIENTL structure. The absolute magnitudes of y and x are not important. What's important are the relative magnitudes and signs. The signs of x and y determine the direction of the text string as indicated in the following table: x y Direction + + To upper right - + To upper left - - To lower left + - To lower right The function in VF04.C (see Figure 6) uses the GpiSetCharAngle function to display eight text strings at 45 degree increments around the center of the client window. Each string displays the fields of the GRADIENTL structure that is used in the GpiSetCharAngle call. In this example, the text strings begin with a blank character so as not to make a mess of overlapping characters in the center of the client window. The character angle does not affect the interpretation of the starting position of the string specified in the GpiCharStringAt function. If you move your head so that a particular string is seen as running left to right, the starting position still refers to the point at the baseline of the left side of the first character. You can make the characters in a text string follow a curved path by individually calculating the starting position angle of each character and displaying the characters one at a time. This is what is done in VF05.C (see Figure 7) to display "Hello, World!" around the perimeter of a circle. The text string is scaled based on the circumference of a circle that is positioned in the center of the window and has a diameter half the width or height (whichever is less) of the window. The GpiQueryWidthTable function is used to obtain the width of individual characters and then space them around the circle. Figure 6: /*------------------------------------------ VF04.C -- Display eight character angles ------------------------------------------*/ #define INCL_GPI #include <os2.h> #include <stdio.h> #include "vectfont.h" VOID Display_CharAngle (HPS hps, LONG cxClient, LONG cyClient) { static GRADIENTL agradl[8] = { 100, 0, 100, 100, 0, 100, -100, 100, -100, 0, -100, -100, 0, -100, 100, -100 } ; CHAR szBuffer[40] ; INT iIndex ; POINTL ptl ; // Create Helvetica font CreateVectorFont (hps, LCID_MYFONT, "Helv") ; GpiSetCharSet (hps, LCID_MYFONT) ; ScaleVectorFont (hps, 200, 200) ; ptl.x = cxClient / 2 ; // Center of client window ptl.y = cyClient / 2 ; for (iIndex = 0 ; iIndex < 8 ; iIndex++) { GpiSetCharAngle (hps, agradl + iIndex) ; // Char angle GpiCharStringAt (hps, &ptl, (LONG) sprintf (szBuffer, " Character Angle (%ld,%ld)", agradl[iIndex].x, agradl[iIndex].y), szBuffer) ; } GpiSetCharSet (hps, LCID_DEFAULT) ; // Clean up GpiDeleteSetId (hps, LCID_MYFONT) ; } Character Shear It is easy to confuse the character angle with the character shear. Let's look at the difference. The character angle refers to the orientation of the baseline. As you can see in Figures 6 and 7, text displayed with various character angles is rotated but otherwise not distorted in any way. The character shear affects the appearance of the characters themselves apart from any rotation. Character shear by itself bends characters to the left or right, but the bottom of each character remains parallel to the x axis. You can use character shear to create oblique (sometimes mistakenly called italic) versions of a font. To set character shear you call the GpiSetCharShear function. This function requires a pointer to a structure of type POINTL, which has two fields named x and y. Imagine a line drawn from (0,0) to the point (x,y) in world coordinates. The left and right sides of each character are parallel to this line. The function shown in in VF06.C (see Figure 8) displays seven text strings using different character shears. You can run this function by selecting Character Shear from the VECTFONT menu. Each string displays the x and y values used in the POINTL structure to set the character shear. The character shear is governed by the relative magnitudes and signs of the x and y fields of the POINTL structure. If the signs of both fields are the same, then the characters tilt to the right; if they are different, the characters tilt to the left. The character shear does not flip the characters upside down. For example, character shear using the point (100,100) has the same effect as (-100,-100). The angle of the left and right sides of the characters from the y axis is sometimes called the shear angle. In theory, the shear angle can range to just above -180 degrees (infinite left shear) to just under +180 degrees (infinite right shear) and is equal to a = arctan (x/y) where x and y are the two fields of the POINTL structure. When you set a nondefault character shear, the GpiQueryTextBox function returns an array of points that define a parallelogram rather than a rectangle. However, the top and bottom sides of this text box are the same width as for a nonsheared text string, and the distance between the top and bottom sides also remains the same. You can use character shear to draw an oblique shadow of a text string. The function in VF07.C (see Figure 9) colors the background of the client window blue and displays the text string Shadow twice. The first call to GpiCharStringAt displays the shadow. This is drawn in dark blue using a positive character shear. The second call to GpiCharStringAt draws the characters upright in red with a slightly smaller character box height. You can run this function by selecting Font with Shadow from the VECTFONT menu. Figure 7: /*-------------------------------------------- VF05.C -- Display "Hello, World" in circle --------------------------------------------*/ #define INCL_GPI #include <os2.h> #include <math.h> #include <stdlib.h> #include "vectfont.h" VOID Display_Rotate (HPS hps, LONG cxClient, LONG cyClient) { static CHAR szText[] = "Hello, world! " ; static LONG cbText = sizeof szText - 1L ; static LONG alWidthTable[256] ; double ang, angCharWidth, angChar ; FONTMETRICS fm ; GRADIENTL gradl ; INT iChar ; LONG lCircum, lRadius, lTotWidth, lCharRadius, cyChar ; POINTL ptl ; // Create the font and get font metrics CreateVectorFont (hps, LCID_MYFONT, "Tms Rmn") ; GpiSetCharSet (hps, LCID_MYFONT) ; GpiQueryFontMetrics (hps, (LONG) sizeof fm, &fm) ; // Find circle dimensions and scale font lRadius = min (cxClient / 4, cyClient / 4) ; lCircum = (LONG) (2 * PI * lRadius) ; cyChar = fm.lMaxBaselineExt * lRadius / fm.lMaxAscender ; ScaleFontToBox (hps, cbText, szText, lCircum, cyChar) ; // Obtain width table and total width GpiQueryWidthTable (hps, 0L, 256L, alWidthTable) ; for (lTotWidth = 0, iChar = 0 ; iChar < (INT) cbText ; iChar ++) lTotWidth += alWidthTable [szText [iChar]] ; ang = PI / 2 ; // Initial angle for first character for (iChar = 0 ; iChar < (INT) cbText ; iChar++) { // Set character angle angCharWidth = 2 * PI * alWidthTable [szText [iChar]] / lTotWidth ; gradl.x = (LONG) (lRadius * cos (ang - angCharWidth / 2 - PI / 2)) ; gradl.y = (LONG) (lRadius * sin (ang - angCharWidth / 2 - PI / 2)) ; GpiSetCharAngle (hps, &gradl) ; // Find position for character and display it angChar = atan2 ((double) alWidthTable [szText [iChar]] / 2, (double) lRadius) ; lCharRadius = (LONG) (lRadius / cos (angChar)) ; angChar += ang - angCharWidth / 2 ; ptl.x = (LONG) (cxClient / 2 + lCharRadius * cos (angChar)) ; ptl.y = (LONG) (cyClient / 2 + lCharRadius * sin (angChar)) ; GpiCharStringAt (hps, &ptl, 1L, szText + iChar) ; ang -= angCharWidth ; } GpiSetCharSet (hps, LCID_DEFAULT) ; // Clean up GpiDeleteSetId (hps, LCID_MYFONT) ; } Figure 8: /*---------------------------------------------------------- VF06.C -- Display seven different character shear angles ----------------------------------------------------------*/ #define INCL_GPI #include <os2.h> #include <stdio.h> #include "vectfont.h" VOID Display_CharShear (HPS hps, LONG cxClient, LONG cyClient) { static POINTL aptlShear[7] = { -100, 41, -100, 100, -41, 100, 0, 100, 41, 100, 100, 100, 100, 41 } ; CHAR szBuffer[40] ; FONTMETRICS fm ; INT iIndex ; POINTL ptl ; // Create and scale Helvetica font CreateVectorFont (hps, LCID_MYFONT, "Helv") ; GpiSetCharSet (hps, LCID_MYFONT) ; ScaleVectorFont (hps, 480, 480) ; // Get font metrics for scaled font GpiQueryFontMetrics (hps, (LONG) sizeof (FONTMETRICS), &fm) ; ptl.x = cxClient / 8 ; ptl.y = cyClient ; for (iIndex = 0 ; iIndex < 7 ; iIndex++) { GpiSetCharShear (hps, aptlShear + iIndex) ; // Char shear ptl.y -= fm.lMaxBaselineExt ; GpiCharStringAt (hps, &ptl, (LONG) sprintf (szBuffer, "Character Shear (%ld,%ld)", aptlShear[iIndex].x, aptlShear[iIndex].y), szBuffer) ; } GpiSetCharSet (hps, LCID_DEFAULT) ; // Clean up GpiDeleteSetId (hps, LCID_MYFONT) ; } Figure 9: /*-------------------------------------------------- VF07.C -- Display characters with sheared shadow --------------------------------------------------*/ #define INCL_GPI #include <os2.h> #include "vectfont.h" VOID Display_Shadow (HPS hps, LONG cxClient, LONG cyClient) { static CHAR szText[] = "Shadow" ; static LONG cbText = sizeof szText - 1 ; POINTL ptl, ptlShear ; SIZEF sizfx ; CreateVectorFont (hps, LCID_MYFONT, "Tms Rmn Italic") ; GpiSetCharSet (hps, LCID_MYFONT) ; ScaleFontToBox (hps, cbText, szText, 3 * cxClient / 4, cyClient) ; QueryStartPointInTextBox (hps, cbText, szText, &ptl) ; ColorClient (hps, cxClient, cyClient, CLR_BLUE) ; GpiSavePS (hps) ; ptlShear.x = 200 ; // Set char shear ptlShear.y = 100 ; GpiSetCharShear (hps, &ptlShear) ; GpiQueryCharBox (hps, &sizfx) ; sizfx.cy += sizfx.cy / 4 ; // Set char box GpiSetCharBox (hps, &sizfx) ; GpiSetColor (hps, CLR_DARKBLUE) ; GpiCharStringAt (hps, &ptl, cbText, szText) ; // Display shadow GpiRestorePS (hps, -1L) ; GpiSetColor (hps, CLR_RED) ; GpiCharStringAt (hps, &ptl, cbText, szText) ; // Display text GpiSetCharSet (hps, LCID_DEFAULT) ; // Clean up GpiDeleteSetId (hps, LCID_MYFONT) ; } A Primer on Paths To explore more capabilities of vector fonts, it's necessary to become familiar with the GPI path, which is similar to a PostScript path. In GPI, you create a path by calling line drawing functions between calls to the GpiBeginPath and GpiEndPath functions: GpiBeginPath(hps, idPath); <... call line drawing functions ... > GpiEndPath (hps) ; This is called a path bracket. In OS/2 1.1, idPath must be set equal to 1L. The functions that are valid within a path bracket are listed in the documentation of the Presentation Manager functions. The functions you call within the path bracket do not draw anything. Instead, the lines that make up the path are retained by the system. Often the lines you draw in a path will enclose areas, but they don't have to. After the GpiEndPath call, you can do one of three things with the path you've created: ■ Call GpiStrokePath to draw the lines that comprise the path. These lines are drawn using the geometric line width, joins, and ends (discussed shortly). ■ Call GpiFillPath to fill enclosed areas defined by the path. Any open areas are automatically closed. The area is filled with the current pattern. ■ Call GpiSetClipPath to make the enclosed areas of the path a clipping area. Any open areas are automatically closed. Subsequent GPI calls will only display output within the enclosed area defined by the path. Each of these three functions causes the path to be deleted. Prior to calling any of these three functions, you can call GpiModifyPath, which I'll describe toward the end of this article. Normally, GpiCharStringAt and the other text output functions are not valid in a path bracket. The exception is when a vector font is selected in the presentation space. When called from within a path bracket, GpiCharStringAt does not draw the text string. Instead, the outlines of the characters become part of the path. Paths opens up a whole collection of PostScript-like stylistic techniques that you can use with vector fonts. Hollow Characters Let's begin by calling GpiCharStringAt in a path bracket and then use GpiStrokePath to draw the lines of the path. GpiStrokePath has the following syntax: GpiStrokePath(hps, idPath, 0L); In the initial version of the Presentation Manager, the last parameter of the function must be set to 0L. When used to stroke a path created by calling GpiCharStringAt, only the outline is drawn and not the interiors. This creates hollow characters. The function in VF08.C (see Figure 10) uses this technique to display the outline of the characters in the text string Hollow. You can run this function by selecting Hollow Font from the VECTFONT menu. You may want to display characters in one color with an outline of another color. In this case, you must call GpiCharStringAt twice, first to draw the interior in a specific color and second, in a path bracket followed by a GpiStrokePath call to draw the outline. The function in VF09.C (see Figure 11) does something like this to draw text with a drop shadow. The shadow is drawn first using a normal GpiCharStringAt call. Another GpiCharStringAt function draws the text again in the current window background color at a 1/6-inch offset to the first string. This is surrounded by an outline created by a third GpiCharStringAt call in a path bracket followed by GpiStrokePath. Quite similar to this is the creation of characters that look like solid blocks, as shown in the function in VF10.C ( see Figure12). Eighteen character strings are drawn at 1-point offsets using CLR_DARKGREEN. This is capped by the character string drawn again in CLR_GREEN and the border in CLR_DARKGREEN. Figure 10: /*----------------------- VF08.C -- Hollow font -----------------------*/ #define INCL_GPI #include <os2.h> #include "vectfont.h" VOID Display_Hollow (HPS hps, LONG cxClient, LONG cyClient) { static CHAR szText[] = "Hollow" ; static LONG cbText = sizeof szText - 1 ; POINTL ptl ; CreateVectorFont (hps, LCID_MYFONT, "Tms Rmn Italic") ; GpiSetCharSet (hps, LCID_MYFONT) ; ScaleFontToBox (hps, cbText, szText, cxClient, cyClient) ; QueryStartPointInTextBox (hps, cbText, szText, &ptl) ; GpiBeginPath (hps, ID_PATH) ; GpiCharStringAt (hps, &ptl, cbText, szText) ; // Text in path GpiEndPath (hps) ; GpiStrokePath (hps, ID_PATH, 0L) ; // Stroke path GpiSetCharSet (hps, LCID_DEFAULT) ; // Clean up GpiDeleteSetId (hps, LCID_MYFONT) ; Figure 11: *--------------------------------- VF09.C -- Font with Drop Shadow ---------------------------------*/ #define INCL_GPI #include <os2.h> #include "vectfont.h" VOID Display_DropShadow (HPS hps, LONG cxClient, LONG cyClient) { static CHAR szText[] = "Hello!" ; static LONG cbText = sizeof szText - 1 ; POINTL ptl ; CreateVectorFont (hps, LCID_MYFONT, "Tms Rmn Italic") ; GpiSetCharSet (hps, LCID_MYFONT) ; ScaleFontToBox (hps, cbText, szText, cxClient, cyClient) ; QueryStartPointInTextBox (hps, cbText, szText, &ptl) ; GpiCharStringAt (hps, &ptl, cbText, szText) ; // Shadow ptl.x -= 12 ; // 1/6 inch ptl.y += 12 ; GpiSetColor (hps, CLR_BACKGROUND) ; GpiCharStringAt (hps, &ptl, cbText, szText) ; // Text string GpiBeginPath (hps, ID_PATH) ; GpiCharStringAt (hps, &ptl, cbText, szText) ; // Outline GpiEndPath (hps) ; GpiSetColor (hps, CLR_NEUTRAL) ; GpiStrokePath (hps, ID_PATH, 0L) ; GpiSetCharSet (hps, LCID_DEFAULT) ; // Clean up GpiDeleteSetId (hps, LCID_MYFONT) ; } Figure 12: /*---------------------------- VF10.C -- Solid block font ----------------------------*/ #define INCL_GPI #include <os2.h> #include "vectfont.h" VOID Display_Block (HPS hps, LONG cxClient, LONG cyClient) { static CHAR szText[] = " Block " ; static LONG cbText = sizeof szText - 1 ; INT i ; POINTL ptl ; CreateVectorFont (hps, LCID_MYFONT, "Tms Rmn Italic") ; GpiSetCharSet (hps, LCID_MYFONT) ; ScaleFontToBox (hps, cbText, szText, cxClient, cyClient) ; QueryStartPointInTextBox (hps, cbText, szText, &ptl) ; ColorClient (hps, cxClient, cyClient, CLR_WHITE) ; GpiSetColor (hps, CLR_DARKGREEN) ; for (i = 0 ; i < 18 ; i++) { GpiCharStringAt (hps, &ptl, cbText, szText) ; // Block ptl.x -= 1 ; ptl.y -= 1 ; } GpiSetColor (hps, CLR_GREEN) ; GpiCharStringAt (hps, &ptl, cbText, szText) ; // Text string GpiBeginPath (hps, ID_PATH) ; GpiCharStringAt (hps, &ptl, cbText, szText) ; // Outline GpiEndPath (hps) ; GpiSetColor (hps, CLR_DARKGREEN) ; GpiStrokePath (hps, ID_PATH, 0L) ; GpiSetCharSet (hps, LCID_DEFAULT) ; // Clean up GpiDeleteSetId (hps, LCID_MYFONT) ; } Filling the Path When first introducing paths, I mentioned that you can do one of three things with a path: stroke it, fill it, or use it for clipping. Let's move on to the second; filling the path. To fill a path, you call: GpiFillPath(hps, idPath, lOption); This fills the path with the current pattern. Any open areas of the path are automatically closed before being filled. The lOption parameter can be either FPATH_ALTERNATE or FPATH_WINDING to fill the path using alternate or winding modes. For vector fonts, FPATH_WINDING causes the interiors of some letters (such as O) to be filled. You'll probably want to use FPATH_ALTERNATE instead. If the current area filling pattern is PATSYM_SOLID, the code GpiBeginPath(hps, idPath); GpiCharStringAt(hps, &ptl, cch, szText); GpiEndPath(hps); GpiFillPath(hps,idPath, FPATH_ALTERNATE); does roughly the same thing with a vector font as does a GpiCharStringAt by itself. When using GpiFillPath you will want to set a pattern other than PATSYM_SOLID. (A bug in OS/2 1.1 causes the current pattern to be reset to PATSYM_SOLID during a path bracket in which GpiCharStringAt is called. You can get around this bug by calling GpiSetPattern after you end the path.) The function in VF11.C (see Figure 13) uses GpiFillPath to display the text string Fade eight times filled with the eight GPI shading patterns (PATSYM_DENSE1 through PATSYM_DENSE8) and then finishes by calling GpiCharStringAt outside of a path bracket. You can run this function by selecting Fading Font from the VECTFONT menu. Figure 13: /*------------------------------------------------------ VF11.C -- Fading font with various pattern densities ------------------------------------------------------*/ #define INCL_GPI #include <os2.h> #include "vectfont.h" VOID Display_Fade (HPS hps, LONG cxClient, LONG cyClient) { static CHAR szText[] = "Fade" ; static LONG cbText = sizeof szText - 1 ; LONG lPattern ; POINTL ptl ; CreateVectorFont (hps, LCID_MYFONT, "Tms Rmn Italic") ; GpiSetCharSet (hps, LCID_MYFONT) ; ScaleFontToBox (hps, cbText, szText, cxClient, cyClient) ; QueryStartPointInTextBox (hps, cbText, szText, &ptl) ; GpiSetBackMix (hps, BM_OVERPAINT) ; for (lPattern = 8 ; lPattern >= 1 ; lPattern--) { GpiBeginPath (hps, ID_PATH) ; GpiCharStringAt (hps, &ptl, cbText, szText) ; // Text out GpiEndPath (hps) ; GpiSetPattern (hps, lPattern) ; GpiFillPath (hps, ID_PATH, FPATH_ALTERNATE) ; // Fill path ptl.x += 2 ; ptl.y -= 2 ; } GpiSetPattern (hps, PATSYM_SOLID) ; GpiSetBackMix (hps, BM_LEAVEALONE) ; GpiCharStringAt (hps, &ptl, cbText, szText) ; // Solid GpiSetCharSet (hps, LCID_DEFAULT) ; // Clean up GpiDeleteSetId (hps, LCID_MYFONT) ; } Geometrically Thick Lines At first, it may seem as though there is no difference between drawing a line normally, like this: GpiMove(hps, &ptlBeg); GpiLine(hps, &ptlEnd); and calling these same two functions within a path bracket and then stroking the path, like this: GpiBeginPath(hps, idPath); GpiMove(hps, &ptlBeg); GpiLine(hps, &ptlEnd); GpiEndPath(hps); GpiStrokePath(hps, idPath, 0); There are, in fact, some very significant differences. First, the line drawn with the normal GpiLine function has what is called a cosmetic line width. The default width of the line is based on the resolution of the output device. It is a device-dependent width for a normal line. The width of the line does not change when you use matrix transforms to set different scaling factors in the coordinate space. Although GPI provides a function called GpiSetLineWidth to change the cosmetic line width, this function is not implemented in MS (R) OS/2 Version 1.1. But a line drawn by stroking a path has a geometric line width. This is a line width (in world coordinates) that you set with the GpiSetLineWidthGeom function. Because this line width is specified in world coordinates, it is affected by any scaling that you set using matrix transforms. Second, a line drawn with GpiLine can have different line types that you set with the GpiSetLineType function. These line types are various combinations of dots and dashes. The line is drawn with the current line color and the current line mix. But a line drawn by stroking a path does not use the line type. The line is instead treated as an area that follows the path of the line but which has a geometric width. This area is filled with the pattern that you set with GpiSetPattern, and is colored with the current pattern foreground and background color, and the current pattern foreground and background mix. Third, a line drawn by stroking the path can have various types of line joins and ends. By calling GpiSetLineJoin you can specify that lines meet with a rounded, square, or miter join. GpiSetLineEnd lets you specify rounded, square, or flat ends to the lines. The function in the VF12.C file (see Figure 14) demonstrates the use of geometrically thick lines filled with patterns to give the letters a kind of neon look. You can run this function by selecting Neon Effect from the VECTFONT menu. The function strokes the path using various geometric line widths filled with a PATSYM_HALFTONE pattern and several colors. The outline of the font is white, but it is surrounded with a halo of red. A better effect could be achieved on devices capable of more than 16 colors. In this case, you can use a solid pattern but color each stroke with a different shade of red. Figure 14: /*----------------------------------------------------- VF12.C -- Neon font using geometricall thick lines -----------------------------------------------------*/ #define INCL_GPI #include <os2.h> #include "vectfont.h" VOID Display_Neon (HPS hps, LONG cxClient, LONG cyClient) { static CHAR szText[] = " Neon " ; static LONG cbText = sizeof szText - 1 ; static LONG lForeColor[] = { CLR_DARKRED, CLR_DARKRED, CLR_RED, CLR_RED, CLR_WHITE, CLR_WHITE }; static LONG lBackColor[] = { CLR_BLACK, CLR_DARKRED, CLR_DARKRED, CLR_RED, CLR_RED, CLR_WHITE }; static LONG lWidth[] = { 34, 28, 22, 16, 10, 4 } ; INT iIndex ; POINTL ptl ; CreateVectorFont (hps, LCID_MYFONT, "Tms Rmn Italic") ; GpiSetCharSet (hps, LCID_MYFONT) ; ScaleFontToBox (hps, cbText, szText, cxClient, cyClient) ; QueryStartPointInTextBox (hps, cbText, szText, &ptl) ; ColorClient (hps, cxClient, cyClient, CLR_BLACK) ; for (iIndex = 0 ; iIndex < 6 ; iIndex++) { GpiBeginPath (hps, ID_PATH) ; GpiCharStringAt (hps, &ptl, cbText, szText) ; // Text out GpiEndPath (hps) ; GpiSetColor (hps, lForeColor[iIndex]) ; GpiSetBackColor (hps, lBackColor[iIndex]) ; GpiSetBackMix (hps, BM_OVERPAINT) ; GpiSetPattern (hps, PATSYM_HALFTONE) ; GpiSetLineWidthGeom (hps, lWidth[iIndex]) ; GpiStrokePath (hps, ID_PATH, 0L) ; // Stroke path } GpiSetCharSet (hps, LCID_DEFAULT) ; // Clean up GpiDeleteSetId (hps, LCID_MYFONT) ; } Clipping to the Text Characters The third option after creating a path is to call GpiSetClipPath: GpiSetClipPath(hps, idPath, lOption); The lOption parameter can be either SCP_RESET (which equals 0L, so it's the default) or SCP_AND. The SCP_RESET option causes the clipping path to be reset so that no clipping occurs. The SCP_AND option sets the new clipping path to the intersection of the old clipping path and the path you've just defined in a path bracket. Any open areas in the path are automatically closed. You can combine the SCP_AND option with either SCP_ALTERNATE (the default) or SCP_WINDING. As with GpiFillPath, you'll probably want to use alternate mode when working with paths created from vector fonts. The function in VF13.C (see Figure 15) calls GpiCharStringAt with the text string WOW within a path bracket. This is followed by a call to GpiSetClipPath. The clipping path is now the interior of the letters. The function draws a series of colored lines emanating from the center of the client window. The function in VF14.C (not shown here) uses a similar technique but draws a series of areas defined by splines. Figure 15: /*-------------------------- VF13.C -- Clipped Spokes --------------------------*/ #define INCL_GPI #include <os2.h> #include <math.h> #include "vectfont.h" VOID Display_Spokes (HPS hps, LONG cxClient, LONG cyClient) { static CHAR szText[] = "WOW" ; static LONG cbText = sizeof szText - 1 ; static LONG lColors[] = { CLR_BLUE, CLR_GREEN, CLR_CYAN, CLR_RED, CLR_PINK, CLR_YELLOW, CLR_WHITE } ; double dMaxRadius ; INT i, iNumColors = sizeof lColors / sizeof lColors[0] ; POINTL ptl ; CreateVectorFont (hps, LCID_MYFONT, "Tms Rmn") ; GpiSetCharSet (hps, LCID_MYFONT) ; ScaleFontToBox (hps, cbText, szText, cxClient, cyClient) ; QueryStartPointInTextBox (hps, cbText, szText, &ptl) ; ColorClient (hps, cxClient, cyClient, CLR_BLACK) ; GpiBeginPath (hps, ID_PATH) ; GpiCharStringAt (hps, &ptl, cbText, szText) ; // Text string GpiEndPath (hps) ; GpiSetClipPath (hps, ID_PATH, SCP_AND | SCP_ALTERNATE) ; dMaxRadius = sqrt (pow (cxClient / 2.0, 2.0) + pow (cyClient / 2.0, 2.0)) ; // Draw spokes for (i = 0 ; i < 360 ; i++) { GpiSetColor (hps, lColors[i % iNumColors]) ; ptl.x = cxClient / 2 ; ptl.y = cyClient / 2 ; GpiMove (hps, &ptl) ; ptl.x += (LONG) (dMaxRadius * cos (i * 6.28 / 360)) ; ptl.y += (LONG) (dMaxRadius * sin (i * 6.28 / 360)) ; GpiLine (hps, &ptl) ; } GpiSetCharSet (hps, LCID_DEFAULT) ; // Clean up GpiDeleteSetId (hps, LCID_MYFONT) ; } Modifying the Path Between the call to GpiEndPath to end the path and the call to GpiStrokePath, GpiFillPath, or GpiSetClipPath, you can call GpiModifyPath. This function uses the current geometric line width, join, and end to convert every line in the path to a new line that encloses an area around the old line. For example, suppose that the path contained a single straight line. After GpiModifyPath the path would contain a closed line in the shape of a hot dog. The width of this hot dog is the geometric line width. The ends of the hot dog could be round, square, or flat, depending on the current line end attribute. Following the creation of a path, these two functions in succession: GpiModifyPath(hps, ID_PATH, MPATH_STROKE); GpiFillPath(hps, ID_PATH, FPATH_WINDING) ; are usually equivalent to: GpiStrokePath(hps, ID_PATH, 0L); GpiModifyPath and GpiStrokePath are the only two functions that use the geometric line width, joins, and ends. In theory, you can call GpiStrokePath after GpiModifyPath, like this: GpiModifyPath(hps, ID_PATH, MPATH_STROKE); GpiStrokePath(hps, ID_PATH, 0L); This should do something, and it should be rather interesting, but GPI usually reports that it can't create the path because it's too complex. Instead, let's look at GpiModifyPath followed by GpiSetClipPath. The function in VF15.C (see Figure 16) is almost the same as the one in VF13.C (see Figure 15) except that it sets the geometric line width to 6 (1/12 inch) and calls GpiModifyPath before calling GpiSetClipPath. Note that the colored lines are clipped not to the interior of the characters but to their original outlines. By the use of GpiModifyPath, the outlines of the characters have themselves been made into a path that is 1/12 inch wide. This is the path that is used for clipping. Figure 16: /*-------------------------- VF15.C -- Clipped Spokes --------------------------*/ #define INCL_GPI #include <os2.h> #include <math.h> #include "vectfont.h" VOID Display_ModSpokes (HPS hps, LONG cxClient, LONG cyClient) { static CHAR szText[] = "WOW" ; static LONG cbText = sizeof szText - 1 ; static LONG lColors[] = { CLR_BLUE, CLR_GREEN, CLR_CYAN, CLR_RED, CLR_PINK, CLR_YELLOW, CLR_WHITE } ; double dMaxRadius ; INT i, iNumColors = sizeof lColors / sizeof lColors[0] ; POINTL ptl ; CreateVectorFont (hps, LCID_MYFONT, "Tms Rmn") ; GpiSetCharSet (hps, LCID_MYFONT) ; ScaleFontToBox (hps, cbText, szText, cxClient, cyClient) ; QueryStartPointInTextBox (hps, cbText, szText, &ptl) ; ColorClient (hps, cxClient, cyClient, CLR_BLACK) ; GpiBeginPath (hps, ID_PATH) ; GpiCharStringAt (hps, &ptl, cbText, szText) ; // Text string GpiEndPath (hps) ; GpiSetLineWidthGeom (hps, 6L) ; // 1/12 inch GpiModifyPath (hps, ID_PATH, MPATH_STROKE) ; GpiSetClipPath (hps, ID_PATH, SCP_AND | SCP_ALTERNATE) ; dMaxRadius = sqrt (pow (cxClient / 2.0, 2.0) + pow (cyClient / 2.0, 2.0)) ; // Draw spokes for (i = 0 ; i < 360 ; i++) { GpiSetColor (hps, lColors[i % iNumColors]) ; ptl.x = cxClient / 2 ; ptl.y = cyClient / 2 ; GpiMove (hps, &ptl) ; ptl.x += (LONG) (dMaxRadius * cos (i * 6.28 / 360)) ; ptl.y += (LONG) (dMaxRadius * sin (i * 6.28 / 360)) ; GpiLine (hps, &ptl) ; } GpiSetCharSet (hps, LCID_DEFAULT) ; // Clean up GpiDeleteSetId (hps, LCID_MYFONT) ; } Is It Enough? I think it's clear that the facilities provided by GPI for working with vector fonts equal--and sometimes exceed--those in PostScript. The GPI interface is very powerful and very versatile. Is that enough? No, it's not. The implementation of vector fonts in GPI has a structural flaw that still leaves PostScript the king. Take a close and careful look at Figure 3 and the display of the Helv font. You'll notice that the two legs of the H are different in width by one pixel when they should be the same width. This is undoubtedly caused by a rounding error. It's obviously more noticeable on a low-resolution video display than it would be on a 300 dpi laser printer, but even on a laser printer such errors will affect the legibility of the text. Errors such as this do not occur with PostScript fonts. PostScript fonts are true algorithms that are able to recognize and correct any anomalies in the rendition of the characters. In contrast, GPI fonts (which are encoded as a simple series of polylines and polyfillets) are drawn blindly without any feedback or correction. So, while we can rejoice in what we have in GPI, there is still the need for some improvement. Basic as a Professional Programming Language (An Interview with Ethan Winer) C and Macro Assembler (MASM) are the languages of choice for most programmers working in the personal computing environment. Occasionally one hears of a programmer using BASIC, but generally what one hears from professional programmers is that BASIC is a toy language--nice for learning, but hardly a language for serious program development. That attitude is beginning to change. In fact, BASIC has grown up quite a bit over the past year. The Microsoft(R) QuickBASIC Version 4 user interface provides an effective working environment for both the beginner and the advanced user, and the BASIC language itself, via the Microsoft BASIC Compiler Version 6.0, now has the professional language features programmers need. One believer in the ability of BASIC to handle the needs of programmers is Ethan Winer, founder of Crescent Software--a firm that specializes in software development in BASIC--as well as the developer of QuickPak Professional, a library of advanced routines designed especially for the BASIC compiler environment. Winer has also written numerous articles on BASIC for many industry publications. Recently we talked to Winer, who shared with us the reasons BASIC is his language of choice, his reasons for developing QuickPak Professional, and some insights into the technical issues involved in BASIC programming. MSJ: Why has BASIC become the language of choice for you and Crescent Software? EW: Unlike C and Pascal, Microsoft BASIC has always included a wealth of built-in commands and features. It can produce sophisticated graphics on an assortment of monitors, open and manipulate files and devices, play music, and access memory and hardware ports directly. The release of Microsoft QuickBASIC 4 and the BASIC 6.0 compiler, has added even more commands and features. BASIC has always been the easiest of the high-level languages to use, and the latest version is as powerful and capable as either C or Pascal. Furthermore, Microsoft has made clear they intend to keep improving both their BASIC 6.0 Professional Compiler and Microsoft QuickBASIC. Of the features that raise BASIC to the level of other professional languages, which do you find particularly useful? The overwhelming advantage of BASIC is that it's extremely easy to learn and use; the commands and functions have sensible names and their purpose is usually obvious. Unlike C or assembly language, it is almost impossible to overwrite the operating system or corrupt memory unless you really try, but this doesn't limit the control a program has over the system resources. For example, the PEEK and POKE commands let you read or write to any memory address, and INP and OUT will directly access hardware ports. Another important advantage of BASIC is that it handles type conversion automatically. That is, you can freely multiply an integer times a double precision variable, and BASIC will automatically handle the math. String handling is equally powerful; there are functions to assign or extract characters anywhere in a string. Graphics is yet another powerful feature. BASIC has built-in commands to draw circles, boxes, filled boxes, and so forth, on all of the popular monitor types. Why did Crescent Software focus on developing a library of programmer's tools for BASIC? There is no disputing the fact that programming languages have come a long way in the past few years. Yet no matter how complete or well designed a language is, programmers will always find they need some additional capability or feature. Third party libraries--or toolboxes--have traditionally filled that gap. A language like C requires external library support, due to its inherent limitations. For example, "pure" C can neither clear the screen nor locate the cursor, so C programmers must use toolbox routines, often viewing them as a necessary fact of life. Likewise, Pascal has its own limitations, and toolboxes are now equally common for that language as well. But perhaps the most important reason to use a third-party library is to reduce the effort needed to achieve a completed program. No doubt knowledgeable programmers could design a pull-down menu system or a full-featured text editor with mouse support, but that requires an enormous amount of time, time better spent designing the rest of the program. Furthermore, end users are used to snappy screen displays, pop-up windows, and all the other hallmarks of a sophisticated user interface. A good toolbox product can provide "canned" solutions to such programming problems, as well as enhance and extend a language. Did the library simply emerge from a collection of utilities you built up over time, or did you set out specifically to create the library? Designing a set of tools requires much more effort than, say, merely writing an interface to the various DOS and BIOS services. We would often start designing a program only to discover that one or more special support routines were also needed. In many cases our needs led to the development of a custom function or subroutine that turned out to be useful in a broader sense. One example is a set of functions that returns the minimum and maximum of two values. These are used extensively in many of the BASIC programs provided in QuickPak Professional and they eliminate what would otherwise be many IF/THEN statements. The MinInt assembler function returns the minimum of two integer values, eliminating such code as: IF X > MaxAllowed THEN Value = MaxAllowed ELSE Value = X END IF Instead a single statement does the same thing, but much more elegantly: Value = MinInt(X, MaxAllowed) We made many other custom functions to assist the QEdit editor that also resulted in meaningful additions to the package. In designing QuickPak Professional we had several very clear goals in mind. We wanted to provide ready-made solutions to common programming problems. These fall into two general categories: routines that are difficult for most programmers to create themselves, and services BASIC just cannot do directly. An example of the first category would be routines written in assembly language, perhaps to search or sort an array very quickly. Other difficult programs would be a text editor, a spreadsheet, and a delay timer with microsecond resolution. The second category is comprised of DOS and BIOS interrupts, because BASIC cannot easily access them. You would need these to list the files on a disk, change their date and time, or read and write a disk's volume label. Second, we wanted these services to be very easy to use. For example, all of the routines that search and process strings would be in both case sensitive and insensitive versions. It was also important to limit the number of passed parameters to the absolute minimum, and implement the routines as functions where appropriate. Using functions rather than called subroutines is the most natural way to extend BASIC. Third, in some cases BASIC's error handling abilities are not powerful enough. C or Pascal let you try opening a file and then check whether an error occurred, but BASIC requires a special error handling subroutine set up in advance. If an error occurs, you end up in the error handler--often with no idea of how you got there or where in the program to return to. So some means to test a disk drive or printer or even bypass BASIC's file handling altogether would be a useful addition. From the looks of all the source code and tutorials you've provided, it would appear that you have very strong convictions about teaching BASIC. Yes. What programmers often need is not just more language features, but an understanding of how to use those capabilities already present. The single most useful tool a programmer can acquire is knowledge. Most accomplished programmers will tell you the best way to become proficient is by studying other people's programs. Indeed, many programmers are self-taught, learning solely from books and articles in popular computer magazines. By using someone else's program, studying the source code and perhaps even modifying it, a much deeper understanding results. We want people to understand how these routines work, and be able to learn from them. This means not only providing all of the BASIC and assembly source, but also writing a series of tutorials explaining the underlying concepts. The manuals that come with Microsoft QuickBASIC 4 are excellent as far as they go, but they gloss over a number of important topics; for example passing TYPE variables and arrays to subprograms and functions. Instead, the examples that use a TYPE array skirt the issue by declaring the array as SHARED. Likewise, the manual makes no mention of saving and loading arrays and EGA screen images to disk. Programmers who would like to become more proficient in BASIC must understand these concepts. QuickPak Professional includes much of this information, along with tutorials on accessing files, storing string and numeric data in a program's code segment, and a comparison of the various ways procedures are designed. One of the nice things about BASIC is that it relieves the programmer of the need to know about the nuts and bolts of the operating system. Unfortunately this also results in some limitations. Can you provide an example where you've removed some of these limitations while keeping BASIC's ease of use? BASIC has many built-in commands, but it is admittedly lacking in the ability to access DOS and BIOS interrupts. Microsoft QuickBASIC 2 added a form of the CALL INTERRUPT feature, though only as an add-on available by linking with a special object module. Further, CALL INTERRUPT is clumsy to implement, and its use requires a knowledge of DOS services that many BASIC programmers do not possess. Rather than simply provide a "watered down" replacement for CALL INTERRUPT, we felt it was important that the BASIC programmer not have to understand how DOS and BIOS services are accessed. Let me give you an example. Any DOS function that accepts a file name expects that name to be in an ASCIIZ format; this is how C strings are stored. Unlike C, BASIC strings do not contain a zero byte to mark the end; instead, a string descriptor is maintained for each string or string array element. A string descriptor is a 4-byte table containing the length of the string and the address of the actual data. Using CALL INTERRUPT required BASIC programmers to add the zero byte manually whenever a file name has to be passed to a DOS file service. Since one of our highest priorities was ease of use, we instead chose to have the file name copied to a temporary holding area; then the routine adds the terminating zero byte before calling DOS. An important DOS service most programs need is the ability to get a list of file names from disk. No application worth its salt will force the operator to remember the names of files to be loaded. Instead, a menu should list all the files present, with some sort of "point and shoot" method provided for selecting one. DOS provides no direct way to get a complete list of file names in one operation. Even if it did, the BASIC program would need to know beforehand the number of names present so sufficient string memory could be reserved for them. The solution we devised was to create a function that returns the number of file names matching a given search specification. Once this is known, a BASIC string array may be reserved for them, and a second subroutine retrieves all of the names from DOS at once. This brings up an interesting point. Microsoft BASIC provides two ways to create a subroutine: subprograms accessed with the CALL keyword, and functions invoked from a BASIC expression. You seem to rely heavily on functions. Functions are indeed useful and a major and welcome addition. One can develop a BASIC function in either BASIC or assembly language. The instructions for implementing an assembler function in QuickBASIC can be found in the Mixed-Language Programming Guide that comes with Microsoft Macro Assembler Version 5 (MASM). One major advantage of a function is the elimination of a passed parameter, in situations where it is appropriate. If the routine that returns the number of matching files were set up as a program it would be called like this: CALL FCount("*.*", Count) PRINT Count; "files were found" But designing the same routine as a function makes it both easier to use and understand: PRINT FCount("*.*");_ "files were found" The output of a BASIC function may be used directly within a PRINT statement or, in the case of setting aside sufficient string elements, as part of the DIM command: DIM Array$(FCount("*.*")) Using a function like this is the most natural way to extend the BASIC language, and since one less parameter is needed, a function will be faster than an equivalent called subroutine. Parameters in all Microsoft languages are passed on the stack, and it is no secret that stack operations can be very slow. This is why functions are especially good when used without parameters. For instance, we have included a set of functions to return the status of the various keyboard settings. The usual method to determine the status of, say, the Alt key is to use BASIC's DEF SEG, PEEK, and AND commands: DEF SEG = 0 AltKey = PEEK(&H417) AND 8 IF AltKey THEN PRINT_ "the Alt key is depressed" By using a dedicated function, you can replace all that code with a single statement like: IF AltKey THEN PRINT_ "the Alt key is depressed" Better still, without passed parameters only five instructions in assembler code are necessary to implement such a function. Assembly language surfaces again. It would appear that a knowledge of MASM might be necessary even for the BASIC programmer. What do you think? Many programmers who use high-level languages would like to learn assembly language but are intimidated by what they believe will be a long and painful process. Learning assembly language is actually not that painful, and even a casual understanding from the perspective of a BASIC programmer is useful. Of course, you need not understand assembly language to use an assembler routine. Are there any special things to know about assembly language functions? The most obvious thing that comes to mind is that, before an assembly language function can be used in Microsoft BASIC, the program that uses the function must declare it. Having to declare procedures is new to BASIC, but in this case it is not unreasonable. Or else--in the Alt key example--Microsoft QuickBASIC, would assume that the Alt key is simply an integer variable. By declaring it ahead of time, Microsoft QuickBASIC knows that the Alt key is a function to be called, and the value returned in AX holds the result. And using assembly language speeds things up here and there. That's an equally important reason for using assembly language. We wanted to speed up those operations that typically are very slow in any high-level language. Again, assembly language is the key to great performance, and about two-thirds of the routines in QuickPak Professional are in assembler. The majority of MS-DOS(R) language compilers, for example, do screen printing through the operating system in order to be compatible with as many machines as possible. Whether this is done via DOS or the BIOS, the results are too slow. Microsoft QuickBASIC 4 partly addresses this by writing directly to video memory, but there is still much room for improvement. Each character must be examined to see if it is a special control character and for each character it takes extra time just to see if the screen needs to be scrolled. We devised several quick printing routines to accommodate multiple video pages and to display new text without destroying the colors already on the screen. One particularly useful routine displays a portion of a string array, containing it within a specified area of the screen. This greatly simplifies the creation of, for example, a browse facility, where you can scroll up and down through the entire array on the screen. Again, our intent was to have these routines do as much as possible and save the programmer unnecessary work. Assembler routines would also give the BASIC programmer substantially faster array processing. This is exactly where assembly language really gives BASIC a much needed boost, in the area of processing arrays. We have, for example, provided a set of functions for each of Microsoft BASIC's numeric array types to return the largest or smallest values. Using a dedicated assembler routine is typically six or seven times faster than an equivalent function in BASIC. An even greater improvement can be had when saving an entire string array to disk and then reloading it later. One of the slowest operations BASIC performs is reading data from a sequential file; it must examine every single byte to see if it is either a carriage return that marks the end of a string, or the CHR$(26) byte that marks the end of a file. Creating a custom assembler routine to capture the entire file in one operation saves an enormous amount of time. The fastest way to save a numeric array in BASIC is to use BSAVE, as opposed to making multiple PRINT statements to a file. The major problem with the latter is the overhead required to convert the values from the internal format used by the PC into the equivalent ASCII digits. Further, BSAVE takes up less disk space. In the case of integers, only 2 bytes are used to store the number in memory, regardless of its value. Contrast that with up to five digits (six if the number is negative) to store the number as ASCII digits. Worse, each number in the file would also need either a carriage return/line feed pair, or a delimiting comma. BSAVE instead captures a "snapshot" of any area of memory, and sends it to disk in one operation. The complementary command is BLOAD, which retrieves a previously BSAVE'd file. What about string arrays? Unlike numeric arrays, BASIC string arrays do not occupy contiguous memory addresses. Instead, the descriptor tables are contiguous, and the actual data could be anywhere in near memory. Therefore, before BSAVE can be used on a string array, the data from all of the elements must be gathered up into a single block. A pair of dedicated routines processes all of the string elements in one operation, and copies them to an integer array and back again. An additional routine lets the programmer retrieve an individual string element if needed. Numeric arrays in Microsoft QuickBASIC are not restricted to near memory. Just being able to copy a string array out to far memory is a useful feature. And if far memory becomes congested, it's easy to save the arrays to disk to open additional space. Further, because we delimit the end of each string with a carriage return/line feed pair, the file is directly readable by any application. You've provided BASIC users with two "new" data types. What are Very Long Integers and Bit Arrays? While I was working on QuickPak Professional, several interesting concepts and routines resulted, including the development of two "new" variable types. One important and long-overdue feature introduced with Microsoft QuickBASIC 4 is support for long (4-byte) integers. Where appropriate, long integers have a decided advantage over floating point numbers since you can process them very quickly, without any rounding errors. Accountants like that. Considered as pennies, their range of +/-$21,474,836.47 is generally adequate--except for serious financial work. As a solution, we devised a set of routines for adding, subtracting, multiplying, and dividing what we call Very Long Integers. These variables use 8 bytes apiece and let you manipulate extremely large numbers without rounding errors. You assign a Very Long by aliasing it into a conventional double precision variable, and BASIC isn't any the wiser. In the opposite direction, we also created a pair of routines to manipulate Bit Arrays. The smallest variable that BASIC provides is a 16-bit integer. When a program needs the range of values offered by integers, using them makes a lot of sense. But often all that is needed are simple true or false variables, and an integer array can waste an enormous amount of memory. A 1000-element bit array occupies only 125 bytes, compared to 2000 bytes for a conventional integer array. There is a routine you supply that lets a BASIC program write to two monitors at the same time. How did you accomplish that? It isn't difficult to create a routine that changes the active monitor, and the very act of switching monitors always clears the screen in the process. Because there is nothing to prevent a program from writing directly to screen memory for an inactive monitor, our solution was both effective and easy to implement. All of the video routines in QuickPak Professional automatically determine the type of monitor installed when called. This is necessary for two reasons. First, the video segment is different for color and monochrome monitors, so the correct segment must be known before a routine can write directly to screen memory. Second, and equally important, CGA monitors create "snow" unless the reading and writing is synchronized to the horizontal retrace. In the routine that writes to any monitor, though, the caller must instead indicate the type of monitor to write to with a code; 1 means monochrome, 2 is a CGA, and 3 means an EGA or VGA. Even though a PC can support two monitors at once, they must be different--that is, both cannot be color, or both monochrome. Thus the programmer would simply specify the monitor type that is not the current one. Of course there is also a function that reports the currently active type of monitor. You've also given BASIC programmers a way to dump screen images regardless of the screen mode they may be in. What can you tell us about that? We also wanted to address graphics. A major limitation in the GRAPHICS.EXE program that comes with DOS is that it works only in the CGA screen modes. In addition, GRAPHICS works only with printers that are compatible with the Epson(R)/IBM(R) command set. Since BASIC supports the EGA, VGA, and Hercules(R) standards, it was important to provide a routine that could print a graphics image from any of these modes. We realized that to be truly useful a screen print routine must know not only the Epson printer codes (which all modern printers can emulate), but also the HP(R) LaserJet(R) codes. Since a LaserJet can print in several sizes, the caller should be able to specify which resolution to use. The only remaining problem was what to do with the colors. A stunning three-dimensional pie chart displayed in sixteen colors is useless as a printout; all you'd get is a big black circle. Clearly, the best solution was to substitute a different hatching pattern for each of the possible screen colors. But what complicates things considerably is the different ways that graphics screen memory is organized. In text modes, each successive character on the screen is contained in successive bytes in display memory. But in CGA graphics modes the screen memory is organized using a method known as interlacing. In an interlaced system, every other row is in a different block of memory, which makes accessing the memory much more difficult. Hercules graphics also interlaces, except that it uses every fourth row. EGA and VGA adapters use yet another system, where each color is in an entirely different segment. This complicated both reading screen memory and translating the colors into hatching patterns. One of your accomplishments was the creation of a word processor, QEdit, written in BASIC. BASIC's variable length strings must have been very useful there. Languages like C or Pascal that permit only fixed length strings take much more programming to get the same results. The program either wastes a substantial amount of memory for text lines that are shorter than the maximum, or it must maintain a table of pointers to keep track of where in memory each string begins. As characters and lines are inserted or deleted, the pointers must be constantly updated. The fact that BASIC supports variable length strings reduced our effort considerably. BASIC does all of this very quickly and automatically, and wordwrapping in QEdit will keep up with even the fastest typist. [See code samples at end of article for an example of a wordwrap function in BASIC, and an example of how to code a function to obtain screen colors] Variable length strings need to be dynamically allocated. This must have caused some significant problems. One of our biggest difficulties was creating the string array processing routines, which required additional research. In particular, we had to get a fair amount of information about Microsoft BASIC's internal workings. As you mentioned, since BASIC permits strings of nearly any length, they are allocated dynamically as necessary. This complicates memory management considerably and, not surprisingly, Microsoft considers many of those details to be proprietary. This became evident immediately when we started writing the routines to sort a string array. I noted earlier that BASIC strings use a descriptor table to tell each string's length and memory location. At first glance, it seems that any two strings can be exchanged by just swapping their descriptor tables. Descriptors are well documented in the Microsoft QuickBASIC manuals, but unfortunately that isn't the entire story. What is not mentioned is how string data is tied. It took us several days to figure it out by trial and error. Once we could exchange strings for sorting purposes, it was a simple matter to insert or delete elements in an array. On a standard IBM PC, inserting a single string at the beginning of a 2000 element array takes more than two seconds using Microsoft QuickBASIC. Contrast that to less than a tenth of a second for the equivalent routine written in assembler. Some of the string functions in QuickPak must have caused similar problems. But again it seems that by providing "functions" you've made life easier. String functions were indeed yet another difficult feature to implement--and again, because of the lack of information. Besides the advantage of eliminating a passed parameter, a function that can directly return a string saves the programmer from having to set aside space beforehand. One of the routines in QuickPak Professional obtains the current directory for a specified drive. Since a directory name can be as long as 64 characters, such a routine would first need a string of that length allocated. When the routine is finished, the extra characters at the end must be removed manually. Because DOS uses a zero byte to mark the end of any strings it returns, the program must use BASIC's INSTR function to find that byte; then it keeps only those characters that precede it. Dir$ = SPACE$(64) 'set aside 64 bytes Drive$ = "C" 'specify which drive CALL GetDir(Drive$, Dir$) 'GetDir loads Dir$ Zero = INSTR(Dir$, 0) 'find the zero byte Dir$ = LEFT$(Dir$, Zero--1) 'keep what's before the 0 When GetDir is designed as a string function, it is considerably easier to use: Drive$ = "C" Dir$ = GetDir$(Drive$) or Dir$ = GetDir$("a") As a function, GetDir locates the zero byte, and then returns a string that is only as long as actually needed. Let's go back and talk about QEdit again. You seem to have emulated the Microsoft QuickBASIC 4 edit commands in QEdit. With all of the editors already available, why did you create another? Though there certainly are many editor programs available commercially, I don't know of any that come with free source code, and none meant to be added to another program. People often ask us to develop new routines, but by far the one they request most is a text editor that can be customized and added to their own programs. Of course, "editor" means different things to different people, and every programmer has his or her own idea of how one should work. Because our customers are familiar with the Microsoft QuickBASIC 4 edit commands, it made the most sense to emulate those. But since we provide the editor's source code, it is easy to customize the various keystrokes that are recognized. But deciding on the editing commands was just the beginning. To be truly useful, a text editor must be able to accept text both as individual lines and in the "one line per paragraph" method of many word processors. We also wanted the operator to be able to change margins, size the window, and scroll the text using a mouse--without the calling program having to do any additional work. Of course, keystrokes must be processed quickly enough to be totally transparent to the user. QEdit contains a number of important features such as full mouse support, block operations, and on-line help. But perhaps the most important feature is the ability to remove features that aren't needed. After all, if someone wants a bare bones editor with margins and wordwrap, there should be a way to exclude the code for block operations and mouse support. We therefore placed those portions of QEdit into separate sections, where users can easily delete or remark them out. One additional feature we wanted for our own use was block operations on columns, as well as for words and lines. Most editors work only in sentence mode, so that marking a block downward captures the entire length of the line. In a word processor this is often sufficient. But when programming, the ability to mark a long section and instantly change the indent level is very useful. A column mode also simplifies copying or moving a table of numbers. Apparently QEdit was designed for use as a pop-up utility. In reality, though QEdit is not meant as a standalone word processor, it certainly could be. It was, however, designed so that users can add it as a "pop-up" to a main BASIC program, thus showing that even sophisticated programs can be written in BASIC. What sort of problems did you encounter? Because of the number of features in QEdit, it uses four separate help screens, accessed with the F1 key. The help text is in the code segment of the program, and a special assembler routine retrieves it. This saves nearly 2Kb of string memory. Also, since users can remove the mouse and block operations, a variable is modified in each of those sections so the help code will know which screens are appropriate. Besides storing the help text outside of string memory, we used the same technique for the cut-and-paste clipboard. Otherwise, with a fairly large document loaded, BASIC could run out of memory when capturing the block. Which brings up an important point. As difficult as it was to capture a column that may span several screens, deleting or pasting it somewhere else was even tougher. Moreover, to prevent the text block from stealing string space, the clipboard is kept in an integer array. We had already written a string manager to copy all or part of a string array to an integer array and back, but capturing and pasting a column required two additional custom assembler routines. One routine copies elements from a string array to an integer array, but starting at a given offset and for a specified number of characters. If a line ends short of the column width, it pads the remainder with blanks. The second custom routine retrieves the string portions one by one from the integer array, so the wordwrap routine can insert the clipboard contents back into the text. We also needed a routine to close a portion of a screen that had been saved earlier. Because QEdit is a pop-up, it saves the underlying screen automatically. When the screen size changes, it complicates matters. So you also had to develop routines for sizing windows on the screen. How was this handled? The user can at any time place the mouse cursor at the upper left or bottom right corner, and move the corner to a new location. Of course, sizeable windows are nothing new, but we wanted the window to actually change, rather than simply show where it would be upon release of the mouse button. With some programs, changing the window size produces an enormous amount of flicker. In QEdit, we employed a custom routine to close only that portion of the window actually needed to reduce the window size. We developed many other related routines both for QEdit and QuickPak Professional in general. For example, one quickly scans a string array for the number of active lines. Another contains the mouse cursor within a specified area on the screen. Another determines the current video mode so a program can accommodate 25, 43, or 50 lines automatically. As you can see, an enormous amount of detail is required to implement a full-featured word processor, regardless of the language used. Microsoft QuickBASIC and the Microsoft BASIC 6.0 compiler are ideal for any application that requires extensive string manipulation, but they are especially appropriate for a word processor. You have certainly made a strong case for BASIC as a "real" language. I truly believe in BASIC as a serious applications language. It has made computer programming--real programming--accessible to millions of people. It is unfortunate that many otherwise informed programmers view BASIC 6.0 and Microsoft QuickBASIC as unsuitable for "real" applications simply because of the BASIC interpreter that comes with DOS. Besides exceptional performance, the Microsoft BASIC products also support recursion, structured code and data, and many other features needed for a modern, professional language. Sample Code: DEFINT A-Z DECLARE SUB WordWrap (X$, Wide) CLS A$ = "BASIC strings use a descriptor table ot tell each string's" B$ = "length and memory location. At first glance, it seems that" C$ = "any two strings can be exchanged by just swapping their" D$ = "descriptor tables. Once we could exchange strings for" E$ = "sorting purposes, it was a simple matter to insert or" F$ = "delete elements in an array. On a standard IBM PC, inserting" G$ = "a single string at the beginning of a 2000 element array" H$ = "takes more thatn two seconds using Microsoft QuickBASIC." W$ = A$ + B$ + C$ + D$ + E$ + F$ + G$ + H$ PRINT W$ PRINT Wide = 60 'the maximum width of the display WordWrap W$, Wide SUB WordWrap (X$, Wide%) Length% = LEN(X$) 'remember the length Pointer% = 1 'start at the beginning of the string 'scan a block of sixty characters backwards, looking for a blank 'stop at the first blank, or if we reached the end of the string DO FOR X% = Pointer% + Wide% TO Pointer% STEP -1 IF MID$(X$, X%, 1) = " " OR X% = Length% + 1 THEN 'LOCATE , LeftMargin 'optional to tab in the left edge PRINT MID$(X$, Pointer%, X% - Pointer%); 'LPRINT [TAB(LeftMargin)]; MID$(X$, Pointer%, X% - Pointer%) Pointer% = X% + 1 WHILE MID$(X$, Pointer%, 1) = " " Pointer% = Pointer% + 1 'swallow extra blanks to next word WEND IF POS(0) > 1 THEN PRINT 'if cursor didn't wrap next line EXIT FOR 'done with this block END IF NEXT LOOP WHILE Pointer% < Length% 'loop until done END SUB Sample 2: DEFINT A-Z DECLARE SUB GetColor (FG, BG) 'gets BASIC's current colors DECLARE SUB SplitColor (XColor, FG, BG) '.ASM - splits a color into FG ' and BG DECLARE FUNCTION OneColor% (FG, BG) '.ASM - combines FG/BG into one ' color CLS INPUT "Enter a foreground color value (0 to 31): ", FG INPUT "Enter a background color value (0 to 7) : ", BG COLOR FG, BG PRINT : PRINT "BASIC's current color settings are: "; GetColor FG, BG PRINT FG; "and"; BG PRINT "That combines to the single byte value of"; OneColor%(FG, BG) PRINT "Broken back out results in"; SplitColor OneColor%(FG, BG), NewFG, NewBG PRINT NewFG; "and"; NewBG COLOR 7, 0 'restore defaults before ending 'This function obtains BASIC's current colors by first saving the 'character and color in the upper left corner of the screen. Next, 'a blank space is printed there, and SCREEN is used to see what color 'was used. Finally, the original screen contents are restored. ' SUB GetColor (FG%, BG%) STATIC V% = CSRLIN 'save the current cursor location H% = POS(0) SaveChar% = SCREEN(1, 1) 'save the current character SaveColor% = SCREEN(1, 1, 1) 'and its color SplitColor SaveColor%, SaveFG%, SaveBG% LOCATE 1, 1 'print with BASIC's current color PRINT " "; CHR$(29); 'back up the cursor to 1,1 CurColor% = SCREEN(1, 1, 1) 'read the current color COLOR SaveFG%, SaveBG% 'restore the original color at 1,1 PRINT CHR$(SaveChar%); 'and the character LOCATE V%, H% 'put the cursor back where it was SplitColor CurColor%, FG%, BG% 'split the color into separate 'FG and BG COLOR FG%, BG% 'restore BASIC's current value for it END SUB Organizing Data in Your C Program with Structures, Unions, and Typedefs Greg Comeau Many of you have no doubt used structures, unions, and typedefs in your C programming careers. In general, structures are easy to use, and they help in the programming paradigms involved with the correct usage of data in programs. They also keep code readable and maintainable. Unions and typedefs can perform some of these duties as well, but their usage tends to become more complicated and cryptic. Since the mechanism of struct and union is pretty well established, I will not spend time describing their basic syntax (the mechanism by which the grammar of the C language dictates the way they may be written) or semantics (the way the compiler treats them in a meaningful fashion). Instead I will describe some of the idiosyncrasies that crop up with structures, unions, and typedefs, providing a resource to use to avoid coding bugs, recognize portability problems, and know more about the C language. Recent Additions The initial specification of C appeared publicly in the classic text The C Programming Language by Brian W. Kernighan and Dennis M. Ritchie (herein referred to as K&R). In that specification, structure assignments, structure arguments, and functions that returned structures were not allowed. Present day compilers, however, allow these activities to occur, and these features have been formalized by the American National Standards Institute's draft proposal for C (ANSI C) which, I understand, will have no more public review periods and will at last become a bona fide standard sometime around March of 1989. Another feature that has made its way into that draft is initialization of automatic structures. However, this is not available under all compilers, especially those that typically come with the UNIX(R) or XENIX(R) Compiler Development Set. For example, the structure definition in Figure 1a will not compile under those compilers unless the static storage class is added as an attribute as is shown in Figure 1b. This places an extra burden on the programmer and is due to the compiler following the older language specification rather than some technical constraint in compiler technology. This restriction may also force you to fill up more of your programs' static storage space area than you'd like. If space is at a premium, this may present problems. The alternative is to code explicit assignments for each member somewhere in the function. In terms of efficiency, this probably isn't going to make much of a difference since this is what the compiler would do with the initialization of the automatic. However, it would make your code more cluttered. Automatic unions and arrays used to have this problem too. They have been allowed to be initialized without constraints by the ANSI C proposal. Since this has become common practice and has been mandated by ANSI, I would think twice about any vendor that does not yet support this. Figure 1: A 1 main() 2 { 3 struct { 4 int a; 5 } s = { 1 }; 6 } B 1 main() 2 { 3 static struct { 4 int a; 5 } s = { 1 }; 6 } C 1 main() 2 { 3 struct { 4 int a; 5 } s1, s2; 6 7 if (s1 == s2) /* syntax error */ 8 printf("s1 == s2\n"); 9 else 10 printf("s1 != s2\n"); 11 } Comparing Structures If you were to try to compile the example code from Figure 1c, you'd find that it produces a syntax error. This is because you cannot compare structures or unions by name the way you'd perform a structure assignment (for example, struct1 = struct2;). The only way to accomplish the comparison is by checking each member of the structure individually. This may sound like a silly restriction to place on a structure (like the inability to easily assign and initialize used to be) and it is--to an extent. However, there is a twofold reason for this. First, if the operation were allowed, it would be expected that it should work for the inequivalency operator (!=), for the relational operators (<, >, <=, >=), and perhaps even for some others. This would not be 100 percent desirable since it would tend to put the compiler through quite a few gyrations. You may very well think "So what?" about this point in time. But I haven't told you the entire story yet. What happens is that a data aggregate like a structure could possibly have extraneous memory associated with it (refer to the sizeof section below), and generating proper code would be rather extravagant, especially to support the relational operators. Furthermore, cross compilation, or rather cross assembly, of such code could produce difficult to track bugs. Member Access Most C programmers I know have no problem understanding C's two member access operators: the . and the ->. The dot allows for the retrieval of a given member within a structure or union, and the arrow allows retrieval from a member referenced by a pointer to a structure or union. However, there seems to be much confusion about the use of other C operators with the member access operators. The ones that seem to cause the most confusion are the & (address of) and the * (indirection) operators. For the novice, something like &*p->x might as well be hieroglyphics. Actually, if you type in the sample listing in Figure 2, many of you, novice and experienced alike, may be surprised to see exactly what this construct and derivations of the construct do. The use of the member access operators is mandated strictly by the operator precedence chart. This chart (one can be found on page 137 of the Microsoft(R) C Compiler (referred to herein as MSC) Version 5.1 Language Reference) makes it clear that both the dot and arrow operators have the highest precedence allowed (along with ( ) and [ ]). Please remember this. As stated in "A Guide to Understanding Even the Most Complex C Declarations," MSJ (Vol. 3, No. 5), precedence is also an important declaration consideration. Knowing what the highest precedence operators are is one of the key facts C programmers should know (meaning have memorized) since many things in the language depend upon it. Also, from a coding viewpoint, it usually makes reading, writing, and maintenance a snap. I cannot emphasize this point strongly enough. Figure 2 should now make more sense. I'll assume that most of you are fine until at least line 13. If we break down each of the subsequent statements, we should get the parenthesized results found in Figure 3 (you can use it as a guide). Therefore, since pfoo->x means to access a member x of a structure, which is pointed to by pfoo, line 12 says to get the address of the member x, whose structure is being accessed indirectly via pfoo. Note that this does not mean to take the address of pfoo and treat it as a pointer to a structure that contains a member x. Line 14 instructs the compiler to get the contents of the member x of the structure that pfoo points to. This works out fine since x is an int *. However, this does not mean to treat the contents of the pfoo variable as a struct pointer to obtain x. If it meant that, it would be equivalent to pfoo->x, and this obviously isn't the case. Line 16 is a bit interesting to me since it is a case of operators negating each other. For instance, if we were to have a declaration such as int x, we could say *&x. To take this one step at a time, this construct first takes the address of x, which implicitly gets cast into an int *, and then obtains the contents of the value of that address. Well, isn't that simply the same as having just used x itself in the first place? This same form of destructive interference happens on line 16 since it's going to get the contents of the address of pfoo->x (and we know it's going to interpret it in this way because of operator precedence). Finally, although line 17 is constructed somewhat differently from line 16, the end results are the same. However, the reason is admittedly cryptic and requires memorization since it is not immediately intuitive. For instance, in the code in Figure 4, one would expect &*pvar in line 10 to map into &9999 since *pvar is 9999 (which is an error, of course, since you can't take the address of a constant). Instead, if we look at this as though we were reading it as the address of the contents of pvar, then since the contents of pvar is var, its address is &var. This same logic applies to line 17 in Figure 2. I've included lines 15, 18, and 19 in case you decide to investigate the example any further. It might be worth proving to yourself that you understand the above by assuming that the declaration of the member x was changed to char *x. For practice, given this situation, would you be able to change the printf format specifications in Figure 2 to something more appropriate (such as %s or %c)? Figure 2: 1 struct s { 2 int y; 3 int *x; 4 }; 5 6 struct s foo; 7 struct s *pfoo = &foo; 8 9 main() 10 { 11 printf("%d\n", foo.x); 12 printf("%d\n", pfoo->x); 13 printf("%d\n", &pfoo->x); 14 printf("%d\n", *pfoo->x); 15 printf("%d\n", &foo.x); 16 printf("%d\n", *&pfoo->x); 17 printf("%d\n", &*pfoo->x); 18 printf("%d\n", sizeof(int)); 19 printf("%d\n", &foo); 20 } Figure 3: 1 &(pfoo->x) 2 *(pfoo->x) 3 &(foo.x) 4 &(*(pfoo->x)) or simply pfoo->x 5 *(&(pfoo->x)) or simply pfoo->x Figure 4: 1 main() 2 { 3 int var = 9999; 4 int *pvar = &var; 5 6 printf("%d\n", pvar); 7 printf("%d\n", *pvar); 8 printf("%d\n", &pvar); 9 printf("%d\n", *&pvar); 10 printf("%d\n", &*pvar); 11 printf("\n"); 12 printf("%d\n", var); 13 printf("%d\n", &var); 14 printf("%d\n", *&var); 15 printf("%d\n", &*&var); 16 printf("%d\n", *&*&var); 17 } The sizeof Structure Another rather cryptic and nasty sort of thing to be aware of when using structures is their size. Basically, what you see is not necessarily what you get. But why should you care? Let me explain by using another code sample (see Figure 5). I ran it on an 80386 computer and got the results shown. The sizeof(char) and the sizeof(int) print 1 and 4 since this is the size of a byte and a natural word on a 386 (under UNIX and the DOS large model). Note that things seem to be going along smoothly while printing the storage space requirements for chara and charab, but all of a sudden the space for huh seems to be out of whack. This is not a bug in the compiler or in the program. There are parts of C that have been allowed to be classified as undefined behavior and this is one of those areas. In this circumstance, huh takes up 8 bytes instead of 5 because intb uses 4 bytes and it must be aligned on a word boundary. Therefore, since chara only uses one byte, the compiler will insert a hole of 3 unnamed and inaccessible bytes after chara to ensure that intb ends up on the right boundary alignment. Some of you may be saying, "Sure that's fine, but why not shift things around so that you don't waste any space?" That doesn't quite work either as can be seen when I printed out the size of huh2. It turns out to be the same size that huh was. Think about it--could this be a coincidence because of an inefficient or lazy compiler or is there some substance to it? The answer must be the latter since an array of type huh2 must be able to be put in storage in such a way that all huh2 intb variables would be word aligned. Going one step further, this means that every structure also has an alignment requirement, a fact that is not readily apparent. Note that I'm not talking about structure tags here since they do not use any execution time memory; however, they will conform to these requirements and produce the correct sizes and offsets if interrogated. In fairness, it is worth mentioning that not all CPUs have this type of alignment requirement. Some enforce alignment only to even addresses, others by multiples of 2, 4, 8, . . . , bytes. And others, even though they would like you to use the suggested alignment, have a lax but less efficient alignment (that is, none). Microsoft C (MSC) Version 5.1 supports this mechanism if you find that there are too many wasted holes in your structures and space is at a premium. You can enable this by using the /Zp command-line switch to CL or by using the #pragma pack statement within your code. These two techniques are explained in the Microsoft C Optimizing Compiler Version 5.1 User's Guide in section 3.3.15. Figure 5: 1 #define offsetof(type, identifier) (&(((type *)0)-\ >identifier)) 2 3 main() 4 { 5 struct chara { 6 char chara; 7 }; 8 9 struct charab { 10 char chara; 11 char charb; 12 }; 13 14 struct huh { 15 char chara; 16 int intb; 17 }; 18 19 struct huh2 { 20 int intb; 21 char chara; 22 }; 23 24 printf("%d\n", sizeof(char)); 25 printf("%d\n", sizeof(int)); 26 27 printf("%d\n", sizeof(struct chara)); 28 printf("%d\n", sizeof(struct charab)); 29 printf("%d\n", sizeof(struct huh)); 30 printf("%d\n", sizeof(struct huh2)); 31 32 printf("%d\n", offsetof(struct charab, charb)); 33 printf("%d\n", offsetof(struct huh, intb)); 34 printf("%d\n", offsetof(struct huh2, intb)); 35 } The output for this program on an 80386 computer is: 1 4 1 2 8 8 1 4 0 Reading/Writing Structures Very often it is necessary to write a structure to a disk file or RAM or perhaps through a pipe or other interprocess communications facility available under such operating systems as OS/2, XENIX, or UNIX. Because the program "distributing" the structure may not have been compiled with the same packing options or may not even be running on the same machine that is responsible for processing any of the data within the structure, it is advised that you should always create a dummy stub program that will dump the values of the structure before assuming that the data within it must be correct. Remember--alignment restrictions may create holes; holes change the physical layout of the structure. You cannot make assumptions without seeing the source code and/or compile options used. Getting the offsetof It is generally considered bad programming style to include hard-wired constants within your code. And because of potential structure holes, it is neither wise nor portable to use hard-wired constants to represent structure member offsets. For instance, returning to Figure 5, because of the inability to track it down easily, it would not be reasonable for you to keep track of how many bytes into huh that intb was located. Instead you should use the offsetof macro. The offsetof macro is available with most recent compiler releases and can be found in <stddef.h>. It takes two arguments: a type representing a structure and a member of that structure. The result is the offset of the member in bytes from the beginning of the structure. I have included one possible implementation of the macro that will calculate a member's offset; it should work on most machines (see the #define offsetof in Figure 5). The basic idea behind the way the macro works is to pretend the structure begins at address zero [hence the (type*)0] so that any reference to any member from address zero must give its true relative offset in bytes. You are encouraged to type in Figure 5 to experiment with the structures, especially lines 32-34. Forcing Type Alignment Up to this point I have been emphasizing the use of structures. Another popular C construct along these lines is the union construct. It is similar to a struct in its syntactical nomenclature but different in its semantics. Unfortunately, its function within C programs is often misunderstood. Some of you are probably curious about my statement that unions are misunderstood, so let me define what a union is and what a union isn't (or rather, what it isn't supposed to be). A union is a variable that has the ability to hold one, and only one of many different types of named objects (that is, objects with overlapping storage) at a given point in time regardless of the types of those objects. Just think about that for a moment. Ramifications of this definition are as follows: ■ The purpose of a union is to allow for the reuse of a memory location or variable. ■ The memory spaces for all members of the union begin at the same address. ■ The sizeof (a union) = = sizeof (the union's largest member). ■ The value of only one of the union's members may be stored within the union. ■ As a style issue, it is desirable that all the union's members are related to a specific piece of program logic using it. It should now be clear that unions can suffice as a method of forcing type alignment and for redefining types. Redefinition of types is described in the next section, and type alignment is described as follows. Very often in systems programming an operating system resource or request call, a device driver, or a specific hardware device (perhaps using DMA) may require that data transferred to or from it have specific alignment requirements. Luckily C, the de facto systems programming language, can satisfy this requirement via unions. For example, let's make a relatively safe assumption--that most of the time an operating system or device is to be word aligned (which taken one step further on most machines usually means that there should be an even address boundary). Let's also assume that the info is to be passed into a hardware device expecting 6 bytes of information. Simply coding something like char info[6]; may or may not necessarily work since C doesn't guarantee that info will begin on an even address--therefore you've got a 50 percent chance of getting it right and even worse odds for tracking down the bug when code is changed 6 months down the road. If you haven't guessed already, the way to ensure that info can be word aligned is through something like: union device_data { int dummy; /* alignment */ char info[6]; }; Since dummy is word aligned (why?) and the address of each member of a union begins at the same location, info must also be word aligned! Of course, your code never needs to be concerned about dummy again since it has served its purpose. All very elegant, no? Redefinitions: Abuse of Unions? Because C does not enforce one facet of unions, namely that there may only be one object in use at a time, this is left up to the programmer. In other words, you are only supposed to take from a union what you put into it; therefore, if you assign to a particular member, you are only supposed to retrieve from that same member until you assign to another member. The problem here is that C leaves this up to you, at least at a syntactical level. For instance, given the declarations: union example { int i; double d; } ex; int j; probably no C compiler will prohibit you from coding: ex.d = 999.999; j = i; even though you'd be guaranteed that j wouldn't hold anything valid, unless perhaps you were interested in getting a random number. However, if you are careful, this side effect can be turned to occasional advantage, what I'd like to term a nonportable nicety--something I don't recommend that you use, but if you do, I hope that you document the fact and clearly understand what it is you are doing. For example, I was working on a consulting project last year that required use of the Intel(R) RMX operating system. The operating system was running on an 80386, which is a segmented architecture machine. At one point in the project, we needed to obtain some dynamic memory. For some rather obtuse reason, it was decided that none of the standard C run-time routines such as malloc would be appropriate for what we wanted to do. Instead we issued an RMX system call in order to obtain the memory. The problem we were faced with then was that the call returned an entity called a token, which, every time we called it, turned out to be the beginning of a segment. However, the token only contained a 2-byte segment number and not the 2-byte zero offset as well, and we needed to use the token as a character pointer. The way we resolved the problem was by using code similar to that listed in Figure 6a. However, given the reasons that have already been mentioned, this technique is not guaranteed to work under all C compilers. Furthermore, we're making the assumption that pointer = = int is valid and to make matters worse, our ordering of the segment and offset variables are also system-dependent since byte or word ordering is hardware-dependent. As you can see, all in all it is not a very healthy affair. Knowing this, many of you probably are agreeing but also noticing how easily the conversion routine functioned. Before this looks too enticing, let's look at the preferred method of redefining types: the cast operator. Figure 6b has a version of toktoptr that uses casts. This may look rather ugly and the cast code is also making some nonportable assumptions. The best thing I can say is that you must recognize that using unions for redefinitions instead of as reusable storage may work, but the method is simply not valid and is totally nonportable even among compilers on the same machine. The chances are also high that it will produce incorrect code with many of the optimizing compilers currently on the market. The cast, although also system dependent in many ways, is generally the less problematic method, and besides it's "legal" C. The reason I am explaining this is not to teach you a new trick but to make you aware of a bad coding practice and to prepare you for the unexpected if it ever pops up while you are maintaining a piece of code. Also, if you do decide to use this technique, you will know about the problems that can crop up because of it. Figure 6: 1 #define getmemory() 20 /* e.g. simulate RSX call */ 2 3 char * 4 toktoptr(token) 5 short token; 6 { 7 union { 8 struct { 9 short offset; 10 short segment; 11 } segoff; 12 char *ptr; 13 } convert; 14 15 convert.segoff.segment = token; /* get segment number */ 16 convert.segoff.offset = 0; /* offset is zero within the segment */ 17 18 return (convert.ptr); /* assume segment & offset overlay ptr perfectly */ 19 } 20 21 main() 22 { 23 int token = getmemory(); /* RSX memory call */ 24 char *p = toktoptr(token); 25 26 printf("%d\n", p); 27 } Figure 6b: 1 #define getmemory() 20 /* e.g. simulate RSX call */ 2 3 char * 4 toktoptr(token) 5 short token; 6 { 7 return ((char *)(((long)token) << (sizeof(short) * 8))); 8 } 9 10 main() 11 { 12 int token = getmemory(); /* RSX memory call */ 13 char *p = toktoptr(token); 14 15 printf("%d\n", p); 16 } Unions in General A more general use of unions is for building coded records. Since a union is capable of declaring a valid identifier, it may appear within a structure (and vice versa). An example of this occurs in Figure 7, which illustrates a case in which a certain record is obtained, perhaps through a communications line, and needs to be processed. Each record has a code recordtype signifying what shape it is to take on so that it can be directed to the appropriate case of the switch statement that knows about it and the names of its members. This is a very elegant and easily maintainable feature of C. I was able to create a data structure that did not have to use any unnecessary data space nor did I need to resort to playing any games with pointers to multiple structures to be able to use the correct type structure. Note that contrary to what was discussed in the preceding section, you can reuse the first int of codedrecord's members without harm. This is an extension as a special case. That is, if any initial segment of overlapping structures within a union is the same, a write to one of the objects can be followed by a read from another object. For instance, if the logistics behind the construction of type1 and type2 data structures were similar, it would be valid to store a value in a and then use c with absolutely no ill effects because a and c are of the same type and located at the same offsets. As a style issue it may even be better to take a, c, and e out of union and make it a single entity in genericrecord. However, this would depend entirely on the logical connection between these identifiers as well as the logic behind the code that uses them. One last point about unions: though the draft proposal of ANSI C now allows for union initialization to occur in C code, the initialization must be represented as a constant expression and the initialization expression can only initialize the first member of the union. This is something to be aware of if your compiler does a conversion to a different type and quietly performs the assignment, perhaps by truncation. You may have to swap the order of the variables in the union in some cases to ensure proper initialization. Figure 7: 1 #include <stdio.h> 2 3 union codedrecords { 4 struct { 5 int a; 6 float b; 7 } type1; 8 struct { 9 int c; 10 long d; 11 } type2; 12 struct { 13 int e; 14 short f; 15 } type3; 16 }; 17 18 struct genericrecord { 19 int recordtype; 20 union codedrecords cr; 21 }; 22 23 struct genericrecord gr; 24 25 main() 26 { 27 /* ... */ 28 29 switch(genericrecord.recordtype) { 30 case TYPE1: 31 /* code that uses genericrecord.cr.type1.? */ 32 break; 33 34 case TYPE2: 35 /* code that uses genericrecord.cr.type2.? */ 36 break; 37 38 case TYPE3: 39 /* code that uses genericrecord.cr.type3.? */ 40 break; 41 42 default: 43 fprintf(stderr, "Invalid Record Type!!!"); 44 exit (1); 45 break; 46 } 47 48 /* ... */ 49 } The typedef Before going into some explanations and examples of typedefs and structures, let me get two common misconceptions about typedefs and one source of confusion out of the way. First, even though the typedef keyword is syntactically categorized as a storage class specifier, it is a misnomer and does not allocate any execution time storage. It is only categorized as such for notational convenience. Second and more pertinent to our discussion is that typedefs do not create or define a new type, as the keyword may imply. What they do is allow the programmer to create a new name for a base or derived type that already exists. In other words, a typedef allows you to create a synonym for the type. Furthermore, the synonym is placed into the general name space (an area used to categorize identifiers) of the compiler's symbol table. The general name space holds function and most variable and enumeration names as well. The end result is that typedef names are practically no different from any others. That is to say, they serve to lock into a name, much like a struct or union tag does. In this way, typedef names can be used to classify identifiers declared with them. Finally, an additional source of confusion with typedef is that you are not allowed to place any storage class specifiers within the type name being created during a typedef statement. This is because you are not allowed to have more than one storage class specifier within any of your declarations. Some would say this is a blessing in disguise since hiding storage class information within a typedef could result in code that is harder to maintain and write than it should be. In other words, it wouldn't be directly obvious from the declaration of a variable based on the typedef what all its attributes are. It's a thin line since I'm not sure what an object-oriented programmer would say about this abstraction. While on this subject, one more thing to remember is that while you may not have storage class specifiers associated with the typedef name, you can use the const and volatile type qualifiers. However, if you use incremental typedefs, only one appearance of a given qualifier is allowed to be applied to the previous typedef declarator. I could be wrong, but I suspect that many compilers would have a problem issuing a proper diagnostic error about this. Allocating Structures There are two common techniques for allocating a structure. They involve the use of #define and typedef. Both can be used to achieve similar goals; however, their syntax, and amazingly their semantics, are very different. If possible the allocation should be performed via typedef. Let's see why. Since K&R never elaborated upon the use of typedef, and since many of the earlier non-AT&T(R) C compilers usually didn't support it (possibly for that reason), until the past few years typedefs were either ignored or misunderstood. The usual method for allocating a structure was to use the #define directive. For instance, before the void keyword became popular on all compilers, it was usually suggested to include #define void int in all programs, usually via a programmer supplied include file, say mydefs.h, that functioned similarly to the macros currently found in stddef.h. However, even though this worked without a hitch (try compiling the code listed in Figure 8), it will create problems with only slightly more complicated type specifications. Do not get off on the wrong track by using old code as an example. The problems appear because #define is concerned only about textual substitutions of strings or tokens of strings. And this is fine since that is the duty of the preprocessor. However, it doesn't know or care about C syntax. The preprocessor is only responsible for accomplishing the text substitutions; as long as it is fed a valid C program, it must emit a valid C program. On the other hand, since the compiler does classify typedef names, it can and will enforce type checking of expressions involving typedefs. Figure 8: A 1 #define v int /* don't use void in this example since it's a keyword */ 2 3 main() 4 { 5 int x; 6 v y; 7 8 x = y; 9 } B 1 typedef int v; /* don't use void in this example since it's a keyword */ 2 3 main() 4 { 5 int x; 6 v y; 7 8 x = y; 9 } If we use some of the examples presented by K&R, whose interpretation is more or less left as an exercise for the reader (even in the new revised second edition), we can investigate why #define will fail. Using their declaration: typedef char * String; the equivalent #define would be: #define MAXLINES (5) #define String char * If we also use their sample invocations of String: String p, lineptr[MAXLINES], alloc(int); a first glance might not indicate any problems. However, if we follow through the text substitutions, the line gets passed (at least as a transparent step) to the C compiler as: char * p, lineptr[5], alloc(int); certainly not what was desired. What happened is that only p was declared as a char *. And lineptr and alloc were declared as an array of char and a function returning char, respectively. We were trying to obtain: char *p, *lineptr[5], *alloc(int); Besides the #define problem, this also points out a pitfall of using a multideclarator declaration. You were warned of this in "A Guide to Understanding Even the Most Complex C Declarations." As another simple case in which #define could not possibly work, consider: typedef int * array20[20]; There's just no way to coerce it to produce: array20 samplearray; Finally, relating all this back to structs and unions, there are also the type checking capabilities of the compiler that are a concern to us. For instance, using K&R's Treenode example, an equivalent define (ignoring Treeptr for our purposes) would be: #define Treenode struct {\ char *word;\ int count;\ } Invocations of this might appear as follows: Treenode tn1, tn2; Treenode tn3, *ptn; However, although tn1 and tn2 are assignment and member compatible, they have nothing to do with other declarations that might use Treenode even if they are pointers such as ptn. Therefore: tn1 = tn2; tn1.count = tn2.count; are fine, but: tn1 = tn3; ptn = &tn1; are undoubtedly syntax errors. To make matters worse, if we use the style constraint of one declarator per declaration: Treenode tn1; Treenode tn2; Treenode tn3; Treenode *ptn; none of these have the ability to have anything to do with the others--if we perform the preprocessor substitution, we get four unnamed yet distinct structure tags. The compiler doesn't care that they might all look the same. Every invocation of Treenode will create a new structure. If instead we (properly) use a typedef: typedef struct { char * word; int count; } Treenode; all the executable statements above are valid. The compiler only creates one reference to the struct in the symbol table and after that everything falls into place including type checking. All is now syntactically sound. Since a typedef is C code, all the Treenode invocations will have the same type. Proper Perspective The basics of structures and unions are well defined and most programmers are capable of using them with reasonably good results. However using them to their full capacity and in a portable way requires a bit more knowledge than commonly available. This also applies to typedef, which has often been either neglected or misunderstood (typedef becomes very important under the OS/2 and Presentation Manager programming environments--Ed.). Since a structure is the main data construct in C (as well as in many other languages), now that you have these facts under your belt, you should have a better understanding of the C language. You can use it to your advantage in a wise, efficient, and portable manner. Skipping over any detail of a language specification is a mistake. Certainly some things are useless and awkward, but it's disappointing to think that many programmers avoid parts of languages simply because they are somewhat complicated. Though C is many times a very terse language and often cryptic, an understanding of its more subtle and complex points opens up C's unique power. My own experience with C, as well as that of other programmers, clearly shows that continuing to use it while not understanding it is not helpful. This usually results in slower development and mistakes that will be costly. If you stick with it and put in that extra energy to understand its advanced syntax and especially the underlying philosophy, you will be able to tap into its most powerful and advanced features. Whitewater's Actor : An Introduction to Object-Oriented Programming Concepts Zack Urlocker Object-oriented programming techniques are not new, but they are becoming more popular as programmers tackle increasingly complex projects. Object-oriented programming can help simplify the development of elaborate programs by breaking them down into logical objects that manage their own behavior and hide internal complexity. Windowing applications in particular are easier to develop and maintain if object-oriented programming techniques are used. Although object-oriented programming is best done in a pure object-oriented language, such as Actor(R) or Smalltalk, it can also be used in other languages. This article provides an overview of object-oriented programming, demonstrating how it can simplify the development of Windows programs. If you haven't done much programming in Microsoft(R) Windows, some knowledge about object-oriented programming can help you understand how Windows works. Most of the sample code is taken from PC-Project, a critical path project management program that I wrote in Actor. PC-Project lets you model a real-world project by creating milestones and tasks, assigning times, and determining the critical path of the project. The critical path shows which activities, if delayed, will cause a delay in the overall completion of the project. PC-Project also lets you allocate resources and costs to tasks and create a Gantt time-line chart of the project. PC-Project is available with complete source code from various Bulletin Board systems or directly from the author. Figure 1 shows the PC-Project application running under Windows. What Is Object-Oriented ? In traditional procedural languages like C or Pascal, the programmer defines data structures and writes functions and procedures to operate on the data. Although normally a correspondence exists between which functions operate on which types of data, most procedural languages offer no formal support for this correspondence; it is entirely the programmer's responsibility to manage such an abstraction. In an object-oriented lan-guage, both data and operations that work with that data are combined into a single logical unit known as an object. Dividing a program into objects encompassing both data and operations makes the program more closely represent the logical design that is being implemented. As a result, object-oriented programs are generally easier to understand and maintain than procedural programs. Object-oriented program-ming encourages the creation of abstract data types; that is, the implementation of an object is referred to abstractly and is encapsulated by high-level operations. Objects have a clear division between public protocol and private imple-mentation. For example, we might have a stack object that defines a public protocol based on the push and pop operations. The stack may be implemented as an array with variables that maintains the first and last positions, but this representation would be considered private. By adhering to the public protocol, we can change the implementation of stacks, say, to linked lists, without having to rewrite any of our code. Figure 2 illustrates a stack object and the separation of public protocol and private implementation. Programming in an object-oriented language involves creating objects and sending them commands or messages to do things. For example, we can create a window with the caption Sample and then show it on the screen. In Actor, this is done using the messages shown below: /* create it */ W := defaultNew( Window, "Sample"); show(W, 1); /* display it */ close(W); /* close it */ In this case Window is a predefined Actor class or type of object already in the system. Therefore dozens of lines of code are eliminated that would otherwise have to be written in C to accomplish the same thing without affecting performance. Window objects have private data that manage their location, size, caption, parent, handle, and so on. Window objects know how to create themselves, position themselves on the screen, resize, close, and so on, as part of their public protocol. Thus, the statement close(W); is a message being sent to the W window to close itself. Although it may seem that messages are the same as function calls in other languages, they are not. The receiver of a message, which is the first parameter after the parenthesis, determines how to respond. Different classes of objects can respond to the same message in different ways, a language characteristic known as polymorphism. For example, if W were a member of the ProjWindow class that received a close message, it would first check to make sure that the current project was saved. If W were a member of the GanttWindow class, it would inform its parent window that it was closing. In fact, W doesn't even need to be a window at all. Other objects, such as files or communication channels, could respond to a close message. Polymorphism is achieved in Actor by defining a method, with the corresponding message name, for the class. A method definition is similar to a function definition in other languages. Polymorphism allows you to write more general, reusable code, since you don't have to worry about what types of objects you are dealing with, as long as they follow the same public protocol. You can let the objects themselves manage the details of implementation. Inheritance Objects are organized hierarchically in classes. Most object-oriented languages include classes for things like arrays, files, strings, stacks, and queues. In Actor there are also predefined classes for dealing with such Windows entities as text windows, dialog boxes, and scroll bars. We can create new subclasses that inherit all the character-istics of an existing class. For example, in PC-Project the ProjWindow class descends from the Window class and adds to it functionality related to project management. Because of this inheritance capability, classes are much more powerful than data types in other lan-guages. The advantage is that all the generic windowing capabilities, like resizing, displaying, and dragging work properly without your having to write or test a single line of code. Using inheritance you focus on those parts of the program that are application-specific instead of constantly "reinventing the window," so to speak. Inheritance encourages the development of small, reusable classes that become building blocks for more sophisticated classes. This approach results in less code to maintain and test and more rapid development from prototype to final application. In PC-Project I used inheritance to group the characteristics that are common to the dialog boxes used for editing activities and for the activities themselves. Figure 3 describes the classes in PC-Project. Figure 4 shows how they are related in the class tree. How does object-oriented programming work with procedural languages? Purists maintain that object-oriented programming is possible only in a late-bound language that has a class facility and inheritance. However, object-oriented design techniques are applicable in many languages. For example, both Ada and Modula-2 include facilities that allow the creation of abstract data types. You can also add object-oriented extensions to C by using a C++ preprocessor. Any program can be designed, if not implemented, as logical objects that encompass data and operations with a clear separation of public protocol and private implementation. Many programmers are pleasantly surprised to find that object-oriented languages encourage them to use techniques they have been faking for years in other languages. Through the rest of this article I encourage you to consider how object-oriented techniques could be used in your current language. Figure 3: ProjWindow window that can display a PERT diagram GanttWindow window that can display a Gantt chart ActivDialog formal class of dialog box for activities MStoneDialog dialog box for editing Milestones TaskDialog dialog box for editing Tasks PERTDialog dialog box for editing PERTTasks Network generic network of nodes with a start and end Node generic node capable of connecting itself Project network that knows the critical path method Activity node with an earlyStart and lateFinish Milestone an activity that uses no time or resources Task an activity that has time, resources, and cost PERTTask Task where the time is estimated by PERT Resource used by a task; has a name and cost Windows Windows, like object-oriented languages, operates on a message-passing paradigm. Windows is an event-driven system, meaning that programs respond to events that the user or other programs initiate. These events correspond to actions like pressing a key, clicking the mouse, or selecting a menu item. Whenever an event occurs, Windows sends a message to notify the program. More specifically, when the user presses a key, for example, Windows sends a WM_KEYDOWN message with the virtual key code of the key that was pressed. The "WM" is mnemonic for "Windows message." Other Windows messages are WM_COMMAND (indicating the user selected a menu command), WM_LBUTTONDOWN (the user clicked the left mouse button), WM_VSCROLL (the user clicked in the vertical scroll bar), and WM_PAINT (Win-dows wants the window to redraw itself). Windows messages are always sent with two parameters to convey additional information. These are known as the word parameter, or wParam, and the long parameter, or lParam. The wParam contains a 16-bit word value; the lParam sometimes contains a 32-bit long pointer to other data. If an application has multiple windows, as PC-Project does, the Windows messages are sent to the appropriate window. For example, if you press the F1 function key while the Gantt window has the focus, a message is sent to that window; the main window is not informed. Making windows responsible only for their own events simplifies application development. Actor Objects The relationship between Actor objects and Windows entities is similar to the relationship between file variables and files in most high-level languages. For example, in Pascal, you can declare a variable of type File. To use the variable, however, you must assign it the name of an actual disk file. The file variable is an abstraction of the physical file on disk. In the same way, Actor objects belonging to classes like Window and Dialog are abstractions of underlying areas of memory managed by Windows. Although both Actor and Windows send messages, the messages are processed separately. Unlike Windows messages, Actor messages are not queued at all and are therefore very efficient. The main function in a Windows application, called WinMain, normally includes a very short loop that translates and dispatches Windows messages. The application must also define a WndProc function that processes the messages. From an object-oriented perspective, it is the window itself that responds to the messages. After all, a window is not just a data structure, it is both the data and the functionality. Thus, Actor manages the WinMain and WndProc functions, the Windows message queue, and other low-level details. Actor automatically translates Windows messages into equivalent Actor messages, enabling you to process all messages in the same way. The Actor classes Window and WindowsObject define many high-level messages that hide the generic details of Windows programming and allow the programmer to concentrate on application specific behavior. Figure 5 shows the flow of messages between Windows and Actor objects. The WM_PAINT method defined in the Actor class Window, automatically locks down an area of memory known as a display context used for redrawing. It then calls the Windows function BeginPaint, sends an Actor paint message, calls the EndPaint function, and lastly frees the memory used for drawing. The WM_PAINT method defined for class Window is shown in Figure 6. Since this method is inherited by all descendants of the class Window, they only need to define a higher-level paint method that knows how to redraw the contents of the window. The Actor paint message will be sent whenever Windows sends a WM_PAINT message. In PC-Project, the ProjWindow class defines a paint method to redraw a network or PERT diagram of the project. The paint method, shown in Figure 7, includes no calls to Windows functions to manage the display context, since this job will be handled by the WM_PAINT method of the Window class. The paint method loops through all the nodes in the project, and if a node is visible, it sends a draw message to the node. To determine where to draw a node, a pos message is sent to the node to get its logical display position. For example, the first node in a project is at the logical display position (0,0). This position is converted into the Windows coordinate (10,30) by sending a displayToWindow message to the window, with the logical display position as an argument. Notice that while it is the node's responsibility to manage its logical location, it is the window's responsibility to determine if the node is visible and convert the logical display location into its own Windows coordinates. Again, the goal in object-oriented programming is to let the objects manage their own behavior as much as possible. Figure 6: /* Trap MS-Window's message to paint self, a Window. Sends a paint(self) message with the display context. self is a Window or ancestor of class Window. wParam and lParam arguments are ignored. hDC, hPS, lpPS are local variables. hDC : handle to display context for drawing hPS : handle to paint struct lpPS : long pointer to locked down paint struct */ Def WM_PAINT(self, wParam, lParam | hDC, hPS, lpPS) { hPS := asHandle(paintStruct); /* get hPS */ lpPS := globalLock(hPS); /* lock down mem */ hDC := Call BeginPaint(hWnd, lpPS); /* get hDC */ paint(self, hDC); /* send paint msg */ Call EndPaint(hWnd, lpPS); /* done painting */ globalUnlock(hPS); /* free memory */ ^0; /* ok, return 0 */ } Figure 7: /* Respond to MS-Windows messages to paint the window as a PERT (network) diagram. Draw each visible node in its proper position. Display the name and any other info required. Then draw the lines to connect the outputs of the node. self is the ProjWindow that receives the message. hDC, a handle to a display context, is sent as an arg. wPoint, x, y are local variables. aNode is the temporary loop variable for the do message. */ Def paint(self, hDC | wPoint, x, y) { do(nodes(project), {using(aNode) wPoint := displayToWindow(self, pos(aNode)); x := x(wPoint); /* horiz windows posn */ y := y(wPoint); /* vert windows posn */ if visible(self, aNode) draw(aNode, self, x, y, hDC); drawTextInfo(self, aNode, x, y, hDC); endIf; /* always draw connections since they may be visible */ drawConnections(self, aNode,x,y, getOutputs(aNode),hDC); }); } Windows Messages In addition to Windows sending messages, like the WM_PAINT message described above, window objects can send messages to other windows or even to themselves. For example, in PC-Project if the user presses the up arrow, the window will scroll up as necessary. This is done by trapping the WM_KEYDOWN Windows message and sending a WM_VSCROLL Windows message to the window. When a scroll message is sent, the wParam argument indicates the scroll direction, defined by a constant such as SB_LINEUP or SB_LINEDOWN. (The "SB" is mnemonic for "scroll bar.") In the case of a scroll message, the lParam argument is ignored, so by convention we send a long zero, 0L. To send a scroll message to a window using C, we call the sendMessage function with a handle to the window that will receive the message, the Windows message constant, and the wParam and lParam arguments, thus: sendMessage(W.hWnd, WM_VSCROLL, SB_LINEUP, 0L); /* C */ Since Actor automatically translates Windows messages into Actor messages of the same name, there is no need to call the sendMessage function (though you could if you wanted to). Instead the scroll message can be sent directly to the W window: WM_VSCROLL(W, SB_LINEUP, 0L); /* Actor */ Actor will then call the sendMessage function with appropriate arguments. Note that in Actor the internal representation of the window and its handle are hidden; the window object is responsible for managing its data and responding to all messages. Of course, you could define a higher-level method, perhaps called scrollUp, that would hide the details of the WM_VSCROLL message and SB_LINEUP constant. Once you understand how Windows sends messages in response to user events, a basic principle of object-oriented programming should become clear: objects are like event-driven data structures. This similarity makes programming for a windowing environment with an object-oriented language very natural. A Keyboard Interface Messages can easily be used to create a keyboard interface. Although PC-Project uses the mouse extensively, I wanted to make sure that the user could use the keyboard if he/she preferred to. Windows provides automatic support for keyboard menu commands, but what about scrolling and selecting activities? Normally these are done by clicking the mouse in a scroll bar or clicking on an activity in the project window. I was able to trap all key presses and simulate mouse actions in the main window of PC-Project by defining a WM_KEYDOWN method for the ProjWindow class. For example, if the user presses the up arrow, a WM_VSCROLL message is sent. If he/she presses the F2 function key or Enter, a WM_LBUTTONDOWN message is sent. After years of conditioning with Lotus(R) 1-2-3(R), many PC users intuitively press the slash (/) key to enter commands. Even Microsoft Excel, a model Windows program, employs this method as an alternative to the Windows user interface. Similarly, programs such as Microsoft Word use the Esc key to bring up the command menu. I wanted PC-Project to accommodate all these different user interfaces. But how? Windows allows us to define accelerator keys that trigger WM_COMMAND messages rather than WM_KEYDOWN messages. The accelerator table is written as part of a resource script file and is separate from the application's source code. I defined the slash and Esc keys as accelerators that would send a WM_COMMAND Windows message with the wParam argument as the constant PW_COMMAND_MODE. The WM_COMMAND message is trapped so that when wParam has the value PW_COMMAND_MODE, an Actor commandMode message is sent. If the keys are defined as accelerators, rather than trapped as in the WM_KEYDOWN method, they work in all windows of the application and there is no need to remove the keystroke from the input buffer. But the question remained, how to respond to the commandMode message in order to activate the menu bar without selecting any item? To answer this question, I used the SPY.EXE utility of the Microsoft Windows Software Development Kit to see what messages Microsoft Excel sends when the slash key is pressed. It sends a WM_SYSCOMMAND message with the constant F100H. The source code in Figure 8 shows how the resources are defined as well as how the WM_COMMAND and commandMode messages are trapped. You can use these techniques to add a keyboard user interface to your own Windows programs in Actor or C. The WM_COMMAND method is written in essentially the same style as would be used in C, by employing a lengthy case statement that determines what action to take. Although this approach works, it is not very object-oriented. A more typical approach in Actor would be to write a method called command that eliminates the case statement by using a lookup table known as a dictionary. Dictionary is a predefined Actor class that allows array-like access to elements of a collection using arbitrary keys. For example, we would create a dictionary called actions that used as its key the values of the wParam argument and as values the literal message to be sent. The pound symbol (#) is used to specify a literal symbol name. The dictionary would be initialized as shown in Figure 9. The command method becomes much shorter; we simply look up the message in the actions dictionary and perform it. Compare this method, as shown in Figure 10, to the one shown in Figure 8. Writing the command method in this way makes it much easier for descendant classes to modify their behavior without having to redefine the entire method. They only have to add or change elements in the actions table. This approach leads to much better code reusability than is possible with the procedural approach. Figure 8: /* initialize the actions dictionary */ actions := new(Dictionary, 10); actions[PW_FILE_OPEN] := #fileOpenAs; actions[PW_FILE_NEW] := #fileNew; ■ ■ <other cases omitted> ■ actions[PW_HELP] := #help; actions[PW_COMMAND_MODE] := #commandMode; Figure 9: PROJECT.RC ; Accelerators are used to enhance the keyboard interface ; note: cursor keys are not defined as accelerators and are ; trapped in the WM_KEYDOWN for ProjWindow ; #define VK_SLASH 191 ; For Lotus-like commands PC-Project ACCELERATORS BEGIN VK_SLASH, PW_COMMAND_MODE, VIRTKEY VK_ESC, PW_COMMAND_MODE, VIRTKEY VK_F1, PW_HELP, VIRTKEY "^o", PW_FILE_OPEN "^n", PW_FILE_NEW END How to Trap WM_COMMAND and commandMode Messages /* Handle menu events and accelerator keys identically. self refers to the ProjWindow that receives the message. */ Def WM_COMMAND(self, wParam, lParam) { select case wParam == PW_FILE_OPEN /* ^O or menu */ fileOpenAs(self); endCase case wParam == PW_FILE_NEW /* ^N or menu */ fileNew(self); endCase . . <other cases omitted... > . case wParam == PW_HELP /* F1 or menu */ help(self); endCase case wParam == PW_COMMAND_MODE /* slash or Esc */ commandMode(self); endCase endSelect; } /* Enter "command mode" in response to a slash key accelerator. This simulates Lotus 1-2-3 style commands by sending an Alt key sysCommand message. */ Def commandMode(self) { WM_SYSCOMMAND(self, 0xF100, 0L); } Figure 10: Def command(self, wParam, lParam) { perform(self, actions[wParam]); } Dialog Boxes Dialog boxes represent a more advanced challenge. In PC-Project I needed several types of dialog boxes to allow data to be edited. For example, when the user clicks the mouse button in the project window, I want to bring up a dialog box that lets the user edit the selected activity's name, description, starting date, and so on. This is complicated slightly by the fact that different types of activities have different data and thus require different dialog boxes. For example, tasks have time and cost, whereas milestones do not. Because all the activity classes--that is, Milestones, Tasks, and PERTTasks (tasks with an estimated time)--are logically similar, they descend from a single class called Activity. The Activity class is known as a formal class, since we will never create objects of that class, only objects of the descendant classes. By making good use of inheritance, we can minimize the amount of code that needs to be written. A general editInfo method can be written for the Activity class that will be inherited by Milestone, Task, and PERTTask. When the user clicks on an activity in the project window, we determine which activity is selected and then send it an editInfo message. However, the editInfo method must be able to run the appropriate dialog box for each type of activity. How do we know which type of dialog box to run? We simply send a message to the activity. Descendants of the Activity class that use the editInfo method should define a method called dialogClass that returns the type of dialog to use when editing. The dialogClass method is considered part of the public protocol for activities. The source code to trap the mouse click and edit an activity in Figure 11 shows the WM_LBUTTONDOWN method for the ProjWindow class. In the same way that the Milestone class descends from the formal class Activity, the MStoneDialog class descends from the formal class ActivDialog, which in turn descends from the Actor class Dialog. In this section I will use higher-level Actor methods that hide some of the details of programming dialog boxes. Dialogs are created by sending the dialog objects a runModal message. The runModal message requires as arguments the resource ID (a constant) and a parent window. The layout of the dialog and its fields, known as edit controls, are defined in the resource script file. Before a dialog actually runs, Actor sends an initDialog message. We can trap this message to load the edit controls with initial values. Since edit controls are objects, they manage the details of handling keyboard input, tabbing, and so on. By making use of inheritance I have the ActivDialog class initialize the edit controls that it knows about and the MStoneDialog class initialize the additional edit controls that it adds. To initialize an edit control with a value, you send a setItemText message to the dialog and specify as arguments a constant indicating the edit control and the value to be used. For example, the message setItemText(self, NAME, getName(activity)); will load the name of the activity being edited into the edit control named NAME. I wrote several access methods, such as getName, getEarlyStart, and setValues that provide a safe way to access an activity's private data. Therefore, if I change the representation of a Milestone, I don't need to change code in other classes. This helps maintain a logical division in the program. The dialog box is responsible only for editing and does not need to be concerned with whether changes to the data require a recalculation of the critical path; that is the responsibility of the setValues method of the activity. The command method defined for the ActivDialog is used to trap user events related to the dialog. In this case, there are only two events that we're interested in--clicking on OK or clicking on Cancel--any other event is ignored. If the user clicks on OK, an update message is sent to the dialog box. Although the command method is defined in the ActivDialog class, it is inherited by the MStoneDialog, TaskDialog, and PERTDialog classes, which define their own update method. The update method sends several other messages, including setValues, which informs the activity that was being edited of its new values so that it can take appropriate action. The code required for the dialog box classes is shown in Figure 12. The other dialog classes, like TaskDialog or PERTDialog, work in a similar fashion, but they require less code because of inheritance. They merely have to initialize any additional edit controls in the initDialog message and define their own setValues method; everything else is inherited. Figure 11: /* Respond to a left button mouse click message. The lParam is the point in the window where clicked. Convert the window location into a "logical" display point and then send a displayLookup message to the project. This message will return the activity at the location, which is considered to be logically true, or logically false if there is no activity at that location. If there is an activity, edit it; otherwise just beep. The variable self refers to the ProjWindow that receives the message. The variables dPoint and activity are local. */ Def WM_LBUTTONDOWN(self, wParam, lParam | dPoint, activity) { dPoint := windowToDisplay(self, lParam); /* convert */ activity :=displayLookup(project, dPoint); /* find it */ if activity /* logically true if found */ editInfo(activity); /* user clicked on an activity */ else /* false if nothing found */ beep(); /* user clicked on dead space */ endif; } The editInfo Method for Class Activity and Descendants /* Display and edit an activity's information. Descendants that use this method should have a dialogClass() method that returns the type of dialog to be used. self refers to the activity that received the message. dlg and retValue are local variables. ThePort is the main window used as the dialog' s parent. */ Def editInfo(self | dlg, retValue) { showWaitCurs(); /* show an hour glass */ dlg := new(dialogClass(self)); /* we know what kind */ setEditItem(dlg, self); /* what to edit? self! */ retValue := run(dlg, ThePort); /* run the dialog */ showOldCurs(); /* change cursor back */ ^retValue; /* return the run value */ } The dialogClass Method for Class Milestone /* Return the appropriate Actor dialog class for editing. */ Def dialogClass(self) { ^MStoneDialog; } Figure 12: /* This is a formal class to define behavior common to the various activity dialog boxes in PC-Project. Descendants should define the res() method to return the resource ID to be used, initDialog() to initialize additional fields, and update() to update values in the activity. ActivDialog descends from class Dialog and inherits all of its methods and variables. */ /* Set the object being edited. */ Def setEditItem(self, anEditItem) { activity := anEditItem; } /* Run the dialog with the appropriate resource. */ Def run(self, parent | retValue) { ^runModal(self, res(self), parent); } /* Initialize all of the fields in the dialog. Descendants may wish to initialize additional fields or override this method. */ Def initDialog(self, wParam, lp) { setText(self, makeCaption(activity)); setItemText(self, NAME, getName(activity)); setItemText(self, DESC, getDesc(activity)); setItemText(self, UES, getUserEarlyStart(activity)); setItemText(self, ULF, getUserLateFinish(activity)); setItemText(self, ES, asString(getEarlyStart(activity))); setItemText(self, EF, asString(getEarlyFinish(activity))); setItemText(self, LS, asString(getLateStart(activity))); setItemText(self, LF, asString(getLateFinish(activity))); setItemText(self, SLACK, asString(getSlack(activity))); } /* Handle the Ok and Cancel buttons. If Ok was clicked, then update the activity. This command method is used by descendants. They will define their own update method. */ Def command(self, wParam, lParam) { select case wParam == IDOK update(self); end(self, IDOK); endCase case wParam == IDCANCEL end(self, IDCANCEL); endCase default ^1; /* ignore it */ endSelect; ^0; } MStoneDialog class [in black bar] /* The MstoneDialog class descends from class ActivDialog and inherits all of its methods and variables. */ /* Return the resource ID used with this dialog box. */ Def res(self) { ^MSTONE_BOX; } /* Initialize additional fields in the dialog. Uses the ancestor's initDialog first. */ Def initDialog(self, wParam, lParam) { initDialog(self:ActivDialog, wParam, lParam); setItemText(self, INPUT, getInputNames(activity)); setItemText(self, OUTPUT, getOutputNames(activity)); } /* Update the activity after Ok was pressed. Inform the network if the name changes, check the connections and set the values. */ Def update(self) { setName(activity, getItemText(self, NAME)); addNode(getNetwork(activity), activity); checkConnection(activity, getItemText(self, INPUT), getItemText(self, OUTPUT)); setValues(self); } /* Set the values of the activity. checkDate displays an error message if the date is illegal. */ Def setValues(self | ues, ulf) { ues := checkDate(getItemText(self, UES)); ulf := checkDate(getItemText(self, ULF)); setValues(activity, getItemText(self, NAME), getItemText(self, DESC), ues, ulf); } Worth the Effort I hope that this "under-the-hood" discussion of PC-Project has helped illustrate the concepts of object-oriented programming and how Windows works. Sometimes it seems like a lot of work to program for Windows, but the end result, a program with a graphical user interface, consistent commands, and device independence makes it worthwhile. Although it is difficult to make use of polymorphism and inheritance in procedural languages, I encourage you to use object-oriented design techniques no matter what language you are using. By designing objects that encompass both data and their operations and by clearly separating public protocol and private implementation, you will be able to write more understandable and maintainable code. Sidebar Actor, designed and created by The Whitewater Group, is an object-oriented language that allows you to create standalone Microsoft Windows applications. Since Actor is an interactive environment that runs under Windows, you can type statements in the workspace window and get immediate feedback. Windows, menus, and dialogs can be created and modified directly in the development environment. Actor has a source-level debugger and an inspector that lets you debug programs interactively. Code is written in a browser, a special editor that lets you create new classes and compiles methods as you write them. The compiler translates Actor statements into a low-level format used at run time. The browser also shows the hierarchy of classes in the system. Figure A shows a screen shot of a browser in the Actor development environment. Actor is a pure object-oriented language. That means that everything in the system is an object and all operations are performed by sending messages to objects. Even an expression like 5 * X is a message to X to multiply itself by 5. In hybrid languages such as C++, some things are objects that respond to messages and others are not. Actor's syntax is a combination of Pascal and C. For example, the := symbol is used for assignment, whereas curly braces, { }, denote a block. Comments appear within C-style delimiters, /* and */. Actor includes an if/else statement, a case/select statement, while loops, and so on. Method definitions begin with the keyword Def, followed by the name of the method and parameters within parentheses. The first parameter in a method definition is the receiver of the message and is always called self. The vertical bar, |, is used to separate arguments from temporary local variables. No type definitions are necessary, since Actor determines the class of an object at run time. The sample method definition shown in Figure B has two arguments, resource and parent, and one local variable, retValue. The caret, ^, is used to return a value. If no value is explicitly returned, the receiver of the message, self, is returned. This very brief view of Actor indicates that it has a relatively standard procedural language syntax. It differs from procedural languages in the way it is programmed. Programming in Actor can be summarized as the process of creating objects and sending messages to the objects. Messages provide the objects with the information necessary to execute a method. In a sense, the message is matched to a method already defined within the object. Actor is dedicated to the goal of creating high-level applications that hide complex underlying issues. For more information about Actor, contact: The Whitewater Group Technology Innovation Center 906 University Place Evanston, IL 60201 (312) 491-2370 MDI: An Emerging Standard for Manipulating Document Windows Kevin P. Welch The Multiple Document Interface (referred to herein as MDI) is a user interface style developed for Microsoft(R) Windows and OS/2 Presentation Manager (referred to herein as PM) that supports the viewing of multiple child windows within a main application. Each of these smaller child windows can be used to display different sets of data or multiple views of the same set of data. This article describes MDI, focusing on the user interface as well as programming aspects of the standard. In the process, it describes a Windows library (MDI.LIB), which will let you easily incorporate the MDI interface into your own applications. The use of this library will then be demonstrated within the context of a simple application (COLORS.EXE). Finally, the MDI standard will be contrasted and compared to the IBM(R) Systems Application Architecture (SAA) Common User Access (CUA) guidelines for user interfaces. Background & Motivation Windows and PM developers have long been fascinated with applications that contain windows within windows. This interest stems from both the natural capabilities of the host environments and the obvious original influence of the Apple(R) Macintosh(R). MDI can therefore be considered an outgrowth of programmers' interest and experience as they have attempted to create a Macintosh-like environment inside Windows. This work, after considerable refinement in the Windows environment, is now being applied to OS/2 PM. To a degree, the development of MDI satisfied some creative instincts expressed by developers while also providing Macintosh-like functionality for IBM-compatible personal computers. In addition, its specification is facilitating the development of an entirely new class of interoperable applications that sport similar user interfaces on the Macintosh, Windows, and OS/2 PM. From this beginning, the MDI specification was refined and extended (primarily by Microsoft) in a determined effort to make it reasonably CUA conformant. The result of this effort was the formal definition of MDI in the Microsoft Windows Software Development Kit (SDK) followed by its implementation in Microsoft Excel--the first major application to use the new specification. Following the lead of Microsoft Excel, many software developers have tried using multiple child windows in their applications, but unfortunately only a few have succeeded in fully implementing the original specification. This failure is due in part to the fact that MDI is reasonably difficult to implement correctly since it requires a good low-level understanding of the underlying environment. Furthermore, implementing MDI in Windows involves a number of subtle tricks, which in the best circumstances might be considered poor programming practice. Despite its difficulties, in recent months the acceptance of MDI has been further solidified with its incorporation into the new (although in my opinion not very well designed) OS/2 file system. This coupled with the also new but more consistent PM programming model should further enhance its appeal to both developers and publishers alike. Definition & Specification From the start, it must be clearly understood that MDI is a user interface style; that is, it is not a set of absolute rules but a collection of a few underdefined guidelines. Although many software developers have implemented MDI within the general style guidelines, few have implemented it in exactly the same way. The first thing to understand about MDI is its main, or top-level, desktop window. This window is almost always resizable, with a title bar equipped with a standard system menu and minimize/maximize icons. In most circumstances, the title bar, or caption, of the main window contains only the name of the application (more on this later). As with most windows, the standard system menu is provided with the following options and accelerators: Restore Alt-F5 Move Alt-F7 Size Alt-F8 Minimize Alt-F9 Maximize Alt-F10 Close Alt-F4 The main window is also used to display each of the application menus belonging to its associated child windows. The application menus vary according to the type of document the active child window contains. For example, when the user moves from a child window that contains a chart document to one that contains a spreadsheet document, the main application menu changes to reflect the capabilities of the active child window. Some menu items can remain active regardless of which child window is selected (for example, various file or formatting commands that have equivalent meaning in different contexts). In addition, the main menu provides a Window management pull-down menu. Commands on the pull-down menu are applicable regardless of the child window selected, allowing the user to manage the main window and all its children. The first part of the pull-down menu has commands that allow the user to manage the size, position, and visibility of each of the child windows. The next part has a list of all the currently visible (including iconic) child windows. The New command lets the user create a new view into the currently selected document. That is, the user creates a new child window that contains the currently active document. Although this might not always be appropriate, it is useful in situations in which the user wishes to view a different portion of the same document. It could be used, for example, to support multiple enlargements of a drawing in a paint program: one window could contain the entire image and another a detailed view. Since screen "real estate" is quite limited, most MDI implementations incorporate some mechanism to arrange the various child windows. Here, following the New command, are two commands that help manage the visual arrangement of the child windows. The Tile command "tiles" each of the active child windows inside the parent client area. Although many effective tiling algorithms can be devised, most assign some sort of priority to the currently active child window and place it in the largest space available. Note that in most implementations the Tile command is a one-time event--any subsequent movement of the child windows will destroy the tiling. The Tile Always command is an extension of the Tile command as it forces all of the child windows to remain continuously tiled. When the size of one child window is adjusted, the relative sizes of the other children are changed to compensate. Any change to the parent window or to one of the children automatically causes a tiling to occur around it (much as it did with Windows Version 1.03). Although this technique has not been used in any major Windows or PM applications, it has many merits that warrant serious consideration. Frequently users end up with many documents open simultaneously, resulting in a cluttered desktop. Following the tiling commands are therefore two commands that enable the user to hide or show one or more child windows. The Hide command lets the user hide the currently active child window. One popular variation on this theme is to let the user simultaneously hide one or more child windows using a dialog box that contains a multiple selection list box. This makes it possible for the user to clear a portion of the desktop in one fluid motion. When windows are hidden, they remain active but cannot be accessed. By using the Unhide command, the user can select one or more windows (from a list of all hidden windows) and make them visible again. The windows can be restored to their original size and location or, if tiling is active, merged into the desktop. The last group of items on the Window pull-down menu is a list of all currently active child windows. The windows are listed by title, with each title preceded by a digit that serves as a short mnemonic. This facilitates quick and consistent keyboard access to each child window regardless of the current title. If a currently active child window exists, it is indicated by a check mark beside its title. In some applications, certain commands may only be applicable to the main window. When this is the case, the main window may be listed at the beginning of the window list. This allows the user to access the commands that are supported by the main window quite easily. Applications that do not need this feature can omit the main window from the window list. Child Windows The next thing to understand about MDI--after its main desktop window--is its associated child windows. Like the desktop, each child window is resizable and contains a title bar. The title bar normally has the name of the document being edited. If a single document is being viewed by more than one child window, a number is appended after the document name; for example, CHART.XLC:1 CHART.XLC:2 CHART.XLC:3 Only one child window can be active at a time, and it is distinguished from the others by a change in the color or pattern of the title bar (usually with the same mechanisms used to differentiate the main desktop from other top-level windows). Note that the main desktop window remains active when one of the child windows is enabled. To a certain extent, this appears to be a visual contradiction since the input focus seems to be simultaneously shared between two windows, to say nothing of the programmatic hoops you have to jump through to accomplish this sleight of hand. The active MDI child window also contains a control or system menu box. Although similar to the one maintained by the parent window, it is activated by the Alt-Minus key combination (I'm not completely sure of the rationale behind this). The commands on the child system menu are identical to those on the main window, except that the Alt key is replaced by the Ctrl key. The end result is a system menu that contains the following options and accelerators: Restore Ctrl-F5 Move Ctrl-F7 Size Ctrl-F8 Minimize Ctrl-F9 Maximize Ctrl-F10 Close Ctrl-F4 It is left up to the application to disable or gray any of these commands that are inappropriate. In most implementations, only the Minimize command is disabled. The Move and Size commands (accessed by Ctrl-F7 and Ctrl-F8, respectively) allow the location and size of the child window to be controlled. These functions mirror the ones available on the desktop but are restricted to keep the child window inside the parent. Like most operations, the movement and resizing of the child windows can also be accomplished using the mouse. An interesting item to note is how the child window frame is handled when moved. Although the mouse is clipped to the client area of the desktop, in Windows the frame can extend outside the parent window boundary. In PM implementations of MDI, the frame is clipped by the system to the client area of the desktop. The Minimize command (Ctrl-F9), seldom implemented in Windows, reduces the child window to an icon inside the MDI desktop. The resulting icon can then be selected, moved around the desktop to a new location, and restored to its original size and location. As is the case with all visible child windows in the MDI desktop, the icon can be hidden or selected using the Window pull-down menu. Note that throughout this process the icon must remain inside the client area of the desktop window and cannot be moved elsewhere on the display. Although this is relatively easy to accomplish in OS/2 PM, it adds a whole new level of complexity to a Windows implementation of MDI (and so is seldom implemented). In PM, however, it is considerably easier to implement this feature, and I expect that more applications will take advantage of it when implementing MDI. Refer to the WITHIN sample application in the MS(R) OS/2 Software Development Kit Version 1.1 (OS/2 SDK) if you're curious. The Maximize command (Ctrl-F10) causes the child window to be enlarged to fill the entire client area of the desktop window. As a shortcut, you can use the mouse and click inside the maximize icon or double-click anywhere in the title bar. Since the client area of the child window fills the main window, you can consider that the title bar of the child "slides under" the menu bar of the desktop window. When this happens, two other changes occur. The first regards the main window caption. Originally, it contains only the application name. But when a child window is maximized, the title bar of the desktop is changed to include the name of the currently active document, much as is done in normal, non-MDI applications. The second, and perhaps even more complicated change, involves the movement of the child window's system menu to the beginning of the application menu. This allows continued access to the child system menu, letting you close or restore the window to its original size. If you think things are complicated enough, consider what happens when a new MDI child is created or an existing one is selected while another is maximized. The MDI specification dictates that when a different child window is selected or a new one created, it automatically assumes the characteristics of the previously selected window. This implies that if you create a new child window while in a maximized state, the new window will also be displayed in a maximized state. Similarly, when you close a maximized child window, the MDI desktop automatically selects the next available child window and maximizes it for you--whether you wanted it to or not. The Restore command (Ctrl-F5), as you might expect, causes the maximized child window (or minimized if implemented) to be restored to its original size and location among the other windows, much as it does with top-level system menus. Finally, the Close command (Ctrl-F4) destroys the currently selected child window. In situations in which the child window is one of several views into a common document, the title bars of the remaining windows are automatically renumbered to reflect the change. If the child window being closed is the last one accessing a document, a dialog box is normally displayed to confirm any required save operations. As is the case with all system menus, double-clicking the mouse inside the system menu box is a shortcut for choosing the Close command. Another item to note about all these commands is that they apply only to the child window that is currently active. This means only that the window has a system menu and minimize/maximize menu boxes. Unfortunately, the original MDI specification does not make this terribly clear. The end result is that each of the child windows is responsible for changing its visible attributes when it receives and loses the input focus. If that isn't enough, the MDI specification also calls for a number of keyboard accelerators to move between the various child windows: F6 select next active document subdivision, clockwise Shift-F6 select next active document subdivision, counterclockwise Ctrl-F6 select next active document, from front to back Shift-Ctrl-F6 select next active document, from back to front Despite the fact that the keyboard accelerators are not listed on the child system menu, they represent the only mechanism for moving between child windows without a mouse. It is left up to users to remember (assuming they read their manuals) what they are and how they work. Furthermore, the task of implementing these accelerators is yet another activity that must be managed by the already overburdened MDI desktop window. Design Issues Before I get into the actual programming issues involved with implementing MDI, it seems appropriate to discuss the design issues that are bound to come up when working with the specification. Perhaps foremost is the additional complexity associated with using MDI. If you don't have the idea by now, it takes a great deal of time to implement MDI and get it to work correctly. From a design standpoint, MDI requires that each child window be object-oriented in nature (maintaining its own instance data) yet be able to access shared data that is held in common when multiple views are in effect. In addition, the standard has some serious performance implications since it introduces more support code and yet another level of hierarchy into the system. It is also natural to compare multiple instances of tightly coupled applications to the MDI alternative. On the positive side, a group of independent applications are often easier to design, implement, and test, especially when the environment takes care of the data-handling issues for you. This approach works especially well when using non-Windows-aware applications or those whose hold on the environment is tenuous in the best of circumstances. On the negative side are the resulting cluttered display, lack of interoperable consistency with the Macintosh, and difficulties of well-integrated interprocess communication between separate applications. Furthermore, since many applications require the use of multiple views into the same document, MDI is something you will probably have to think seriously about. Another troublesome design area with MDI is the way that it treats menus. Although menus are relatively simple to structure when each child window is homogeneous in nature and shares the same or similar capabilities, the design can be difficult when child windows are very heterogeneous or involve compound documents. Menu design is further complicated when the desktop is empty or all the child windows are hidden. Most of the normal menu options will be disabled and of no interest to the user. Many applications respond to this situation by severely pruning their menus, adding additional menu-handling complexity. One of the most serious drawbacks of the MDI standard is the problem that results when the desktop window is resized. Although you can't move a child window completely outside the client area, it is possible to have it end up there if you change the size of the MDI parent. The end result is a window (perhaps even an active one) that is completely invisible. Despite the fact that you can't point to the invisible window with a mouse, you can use the child window accelerators to get to it--but you still can't see it. An interesting visual phenomenon can be created if you use the Ctrl-Minus key combination while the active child window is outside the client area. As you would expect, the child's system menu appears but the child window remains hidden. The result is that the child's system menu appears unconnected to the desktop, magically making itself visible with no apparent connection to anything else. Interesting, maybe, but certainly somewhat confusing for users. MDI API By now you may be wondering whether MDI is something you want to take on--especially since it doesn't really do anything except manage a collection of related child windows. But there is a good reason to use it--the definition and implementation of an Application Program Interface (API) that manages the MDI. The end result of the API is a small library of object modules (approximately 16Kb in total size) that performs all the work of integrating MDI into your application for you. And best of all, it's no extra charge with the price of your MSJ subscription. In order to accomplish the task of integrating the API into an application I enlisted the help of friend, long-time associate, and Windows guru--Geoffrey Nicholls. Together we came up with an API that lets you write complex MDI applications as if they were standalone, independent applications. We recognized from the start that developing such an API would be quite dirty (doing things we would never do in conventional Windows programming) and that we would really have to try to keep it small and simple. We also realized that we would not be able to implement the entire specification, only some of the more important facets--the rest we would leave to you. Finally, we wanted to make it as object-oriented as possible. After several false starts and rewrites we ended up accomplishing our goals but in so doing perhaps used property lists and window offsets to excess. The MDI API we came up with in a sense represents an analog of the existing Windows API. From previous experience, we knew that to a large degree the operating characteristics of a window are defined by the default window message processing function. The MDI API attempts to change this foundation and give each window a new and different set of characteristics. The net result is a small number of routines with familiar parameter lists that can be used together to give your application MDI characteristics. MdiMainCreateWindow() Main window creation function MdiMainDefWindowProc() Main MDI window default window function MdiChildCreateWindow() MDI child window creation function MdiChildDefWindowProc() MDI child window default window function MdiGetMessage() MDI application message retrieval function MdiTranslateAccelerators() MDI application translate accelerator function MdiGetMenu() MDI menu retrieval function MdiSetAccel() MDI set document window accelerator table function Message Flow and Process Sequencing In the next two sections we will examine the inner workings of the MDI API. We first describe the general message flow and processing sequence used by the API. Then we describe each of the top-level functions and explain some of the subtle ways in which they work. As you read these sections, refer to the MDI source code listings accompanying this article. The code is reasonably well documented, so you should be able to understand it if you have a good background in Windows programming. Now, perhaps the most efficient way to learn about the MDI API is to study the MDI message flow diagram carefully. It tracks the path of each message received by an application that uses the API, focusing on those that are of particular interest. The first thing to notice is the rather normal message retrieval, translation, and dispatch loop at the top of the diagram. This occurs much as it would in any other Windows application, the only difference being in a specific check for menu-related messages (performed inside the MdiGetMessage function). When such messages are encountered, they are immediately dispatched to a window function in order to activate the various system and application menus correctly. After the message-handling loop, each message is dispatched to an appropriate window function. As far as MDI is concerned, there are only two types of windows present--a desktop or parent window and child or document windows. On the left side of the diagram, the flow of events for the desktop window is listed; on the right side, a similar flow for each of the document windows is listed. Tracing down the left, or desktop side, each message is processed by the main application window function, then passed on to the default MDI main window function. The remainder of events listed below occur inside this default function, finally ending in most messages being sent on to the standard DefWindowProc. As you can see from the diagram, the default MDI main window function is primarily interested in activation, initialization, and command-related messages. All other messages are sent on without modification to the DefWindowProc. Of those intercepted, some result in a particular action being performed (like the activation of a particular child window); others are processed and sent directly to an appropriate child window--bypassing the default window function. On the right, or document side of the diagram is the sequence of events that occur when messages are received by the default MDI child window function. As is the case with the left side, the flow of events listed occur inside this default function, ending with most of the messages being sent on to the standard DefWindowProc. Like the default desktop window function, the child window function is primarily interested in activation, initialization, and command-related messages. Of particular importance are the various system commands. Some are handled directly and not passed to the system. In certain cases, such as those that involve activation of a menu or a new document window, the message is sent directly to the desktop. In a normal Windows application, the activation of a child window occurs with little fanfare, but in an MDI implementation a number of important steps must be performed. The first is changing the title bar color. Although Windows allows only one window to have the input focus, when a child window is selected it seems that both the desktop and the child are simultaneously active. This is done by manually sending a WM_NCPAINT message (with appropriate parameters) to the DefWindowProc of the window being activated. Furthermore, under the interface specification, only the currently active document window contains an MDI system menu and maximize/minimize icons. Because of this, the second step is to change the style of the window to include these new attributes and then force the system to display the changes. The next major task to perform when a document window is activated involves replacing the desktop menu. Each child window is associated with its own menu. When it is activated, the current desktop menu is replaced and the new one inserted, retaining all the attributes that it previously had. Finally, just before the input focus is transferred to the document, the Window pull-down menu has to be updated to reflect the current status of the desktop. Like Figure 12, Figures 13 and 14 list the sequence of events that transpires either when a document window is maximized or a different document window is selected while in maximized state. Of these two sequences, the only somewhat technical task is the automatic insertion of the MDI system menu at the beginning of the application menu. This involves retrieving the MDI menu icon from Windows with the following call: hBitmap = LoadBitmap( NULL, MAKEINTRESOURCE( OBM_CLOSE) ); The resulting bitmap contains both the standard system menu icon and the MDI one. After retrieving the bitmap dimensions (via a GetObject call), you can extract the MDI system menu bitmap for use when updating the application menu. This entire sequence of events (never before publicly documented) is performed by the MdiCreateChildSysBitmap function, should you have the inclination to see how it is actually accomplished. Top-Level Functions Now that I've discussed the message flow and process sequencing of MDI, I will focus on the top-level function calls provided by the API. Programmatically speaking, if you understand these functions, you will be able to use the MDI interface in your own applications without a great deal of difficulty. The first of these calls is MdiMainCreateWindow, which is responsible for the creation of the main desktop window and all the associated MDI property lists required to make the interface work. The actual data structures used by the API are maintained with property lists attached to the desktop and document windows. A property list is an attempt to give each window access to some sort of instance data (to borrow an object-oriented programming term). This is accomplished by associating a window handle with a named block of memory. Using the interface provided by Windows, any window can set, enumerate, retrieve, and destroy properties. Although there is no predefined limit to the number of properties that a window can have, the property list itself, like other window-related data, is actually allocated in the local heap of the user library. The MdiMainCreateWindow function also attaches the Window pull-down menu to the main application menu, making the MDI interface almost transparent to the desktop. The second API call is MdiMainDefWindowProc. As described in the MDI message flow diagram, this function is responsible for the default processing of all desktop-related messages. More specifically, it is interested in messages that involve the activation and deactivation of the desktop, menu-related messages, and messages relating to the visibility and sizing of the associated document windows. Implemented as a large switch statement, it passes most of the messages on to the DefWindowProc. The next API call is the MdiChildCreateWindow function. This function, identical in many ways to the standard Windows CreateWindow call, creates a new child window inside the desktop. Behind the scenes it sets up the related property lists, keeps track of the window menu and accelerator table, and activates the child window correctly, depending on the current state of the desktop. As with the desktop window, each document window is associated with a default MDI message-processing function, MdiChildDefWindowProc. This function is responsible for the default handling of all document window related messages, especially those that involve system menu choices and window creation, activation, and destruction. Those messages not handled are either sent directly to the desktop window or are passed on to the DefWindowProc. After the default child window function is a replacement for the standard Windows message function, MdiGetMessage. This function is responsible for retrieving all of the application messages from the system queue. It also checks for keyboard menu access and activates the correct menu when cursor keys are used while a pull-down menu is visible. Following this is the MdiTranslateAccelerators function, which is responsible for translating each message according to the currently active accelerator table. Though most Windows applications have only one accelerator table, MDI applications can have several--one for the main desktop and one for each type of document window. This function automatically checks the state of the application and uses the appropriate accelerator table. Finally, there are two utility functions contained in the MDI API--MdiGetMenu and MdiSetAccel--that are implemented as macros. These two functions are required since most applications need to define an accelerator table and access the current menu. The current menu handle is retrieved by the MdiGetMenu macro which returns it to the document window. The MdiSetAccel macro attaches an accelerator table to the property list of the document window. The accelerator table can then be used automatically when messages are translated and dispatched throughout the application. Taken together, these eight functions represent the entire MDI API. Although you cannot use these functions with impunity, they should work well even in the most demanding applications. If carefully used, they hide most of the subtleties of MDI and let you focus on solving customer problems, not implementing yet another scheme for managing child windows. The only really nasty side effect is the installation of a system keyboard message hook. This hook intercepts cursor movement keystrokes while manipulating menus. Without this message hook it would be very difficult to implement a truly authentic MDI keyboard user interface. Building MDI.LIB In order to build the MDI API library, you will have to create these files: MDI Library make file MDI.H Library header file MDI1.C Main MDI API functions MDI2.C Activation and switching of document windows MDI3.C Handling of menus and keyboard user interface In addition to these source files, you will need the Microsoft Windows Version 2.1 SDK and the Microsoft C Optimizing Compiler Version 5.1. The library MAKE file (MDI) will compile each of the modules in the medium model and combine them into an object library using the LIB utility provided with the C compiler. The resulting library is then ready for use without modification by any medium model Windows application. If you wish, you can change the make file compilation flags and create equivalent small, compact, or large versions of the same library. Using the MDI API I will use the program COLORS.EXE to show how the MDI API is used in the context of a simple application. I chose this program since it will clearly demonstrate the simple and straightforward use of the MDI API. In many ways, COLORS can be considered a collection of three different, yet related programs. For one, although the three parts of COLORS share the same window procedure, they act as if they were three separate applications. Using the MDI API they are brought together into one desktop. With the COLORS desktop, you can create a number of red, green, and blue colored document windows. The colored windows are created by using the New... option under the File pull-down menu. Each window is successively numbered and, via an associated pull-down menu, can be modified to display different intensities of color. Of the three types of document windows, the blue one is unique in that it is also associated with an accelerator table. By using keys 0, 1, 2, 5, and 7 you can change the intensity of the blue background color to 0 percent, 100 percent, 25 percent, 50 percent, and 75 percent respectively. See Figure 16 for an example. When several documents are present on the desktop, you can move from window to window using one of three mechanisms--selecting a window with the mouse, moving to another window using the keyboard user interface, or pulling down the Window menu and manually selecting a different window. Additionally, using the Window pull-down menu, you can hide the currently active document window or possibly redisplay hidden ones in a traditional MDI fashion. You can build COLORS from the source code listed in Figure 17, which includes the following files: COLORS COLORS.DEF COLORS.RC COLORS.H COLORS.C Each reference to the MDI API is clearly identified and highlighted in Figure 17. The first reference to notice is in the application MAKE File. Here, COLORS is dependent on both MDI.H and MDI.LIB. In addition, the MDI library is referenced in the linkage command line, which allows COLORS to use any of the public MDI API routines we previously defined. The next MDI reference of interest is contained in COLORS.DEF, which is where both the MdiMsgHook and MdiDlgUnhide functions are exported. They both must be exported since they represent movable entry points used by the system. Failure to do so will cause serious problems. The third reference to the MDI API is in COLORS.RC. Note the inclusion of the MDI.H header file and the definition of a number of MDI-related resources at the end of the file. The first of these resources, the MDI window menu, is used as a template for the Window pull-down menu. The MDI API creates a duplicate of this menu, attaching a list of the currently visible document windows at the end. The next resource is the MDI child window accelerator table. This table, which is automatically loaded by the API, is used to implement the document window keyboard user interface. Last in the resource file is the template for the MDI Unhide dialog box. This dialog box is displayed when the Unhide... command is selected from the Window pull-down menu. With this dialog box you can select a hidden document window and have it redisplayed on the desktop. You can also change the style and characteristics of the dialog box to suite your application, although you should be careful not to alter the name and identifiers used. Following the resource file is COLORS.C, which contains all of the C source code for the COLORS application and is structured much like any other windows program. COLORS.C (like COLORS.RC) also references MDI.H. In addition to defining all MDI related identifiers, MDI.H needs to be included since it defines function prototypes for each member of the MDI API. The first MDI-related task COLORS performs is the creation of the main desktop window using the MdiMainCreateWindow function inside MainInit. This call creates an empty window that contains the default desk-top menu. Not long after MdiMainCreateWindow is a call to ColorCreate, a utility function that creates a new document window using the MdiChildCreateWindow function and associates it with an appropriate accelerator table. In this case, the default action is to create a single red document window. Once the desktop has been created and initialized, the application retrieves and processes all related messages. Like most Windows applications, this is accomplished with a simple GetMessage loop followed by the translation and dispatch of each message retrieved. In this case, however, MdiGetMessage and MdiTranslateAccelerators are used in place of the normal Windows functions. The next reference to the MDI API occurs in the MainWndProc of COLORS. Each message that is dispatched by the message-processing loop is sent directly to the responsible window function. The MainWndProc handles all the messages that relate to the desktop window. In addition, since the desktop window is the only one that contains a menu, it also receives all menu-related messages. The desktop message processing function traps only the file-related commands and passes the rest of the messages to the MdiMainDefWindowProc for additional handling. The MdiMainDefWindowProc in turn processes the commands in which it is interested (redirecting some to the appropriate document window function) and passes the rest on to the system via the DefWindowProc. Throughout this process, the main window message processing function can receive menu commands belonging to any one of the child windows. Because of this capability, it is recommended that each document window menu share a common set of commands that are applicable at the desktop level. In COLORS, these commands are all those listed under the File pull-down menu. Note that it is only necessary to conceptually separate the desktop and document menus, not each of the associated document menus. This is because menu commands not intercepted by the desktop are only destined for the currently active document window, not for those that are inactive. The last references to the MDI API occur in the ColorWndProc message-processing function. This function, shared among each of the colored child windows, responds to the document menu commands and paints the window background using the default color at the selected intensity level. Throughout ColorWndProc, MdiGetMenu is used in place of GetMenu. This is because the desktop window contains the menu for the document window and isn't always the immediate parent [else GetMenu(GetParent(hWnd)) would be a suitable alternative]. Like the desktop window function, ColorWndProc passes most of the messages on to the default MDI message-processing function, in this case MdiChildDefWindowProc. This function in turn processes a subset of the messages and passes the balance on to the DefWindowProc. In certain situations, messages are redirected to the desktop window and not sent directly to the system. When you build COLORS, experiment with it and see how the internal functions respond in a variety of situations. Try and hide all the document windows or create new ones while one is in a maximized state. In particular try out the keyboard user interface, moving from document to document without the aid of a mouse. In a while you will begin to appreciate how much is going on in the background to make the interface work consistently. Yet despite the visual sophistication, there is the increased overhead required by the API. If you switch rapidly between different document windows, then the additional overhead will be readily apparent. Although in part due to the relatively simpleminded message-handling approach of COLORS (which passes everything on to the default window function), to a large degree it can be attributed to the MDI API itself. Nevertheless, keep in mind that the MDI API implemented here was designed for clarity and readability, not for size and performance. Our internal working version of the API (on which the published library was based) implemented the full MDI specification considerably more efficiently than this one does (including Window New, Tiling, and the ability to minimize document windows). The central structure, however, remains the same--with a little tuning and enhancement, the base API presented here is capable of supporting world-class MDI applications with unparalleled ease. Coupled with a little rethinking of your current data-handling techniques, you will be able to adapt many of your existing Windows applications to the MDI user interface easily. And, perhaps best of all, with the MDI API you can accomplish this with few changes to your source code. MDI and SAA In addition to the interoperability benefits of MDI, one of the most significant forces behind its acceptance in the development community is IBM's SAA. Although a comprehensive overview of SAA is outside the scope of this article, we will briefly describe it to show how the SAA specification influences MDI. SAA is a set of selected software interfaces, conventions, and protocols that serve as a common framework for application development, portability, and use across three major computing platforms--the IBM System/370, System/3X, and the personal computer. A significant part of SAA is the CUA specification. This standard defines, in a lengthy set of rules and guidelines, what SAA-compliant user interfaces should look like and how they are to be used. The end result is a 300+ page document (available from your local IBM representative or branch office) that describes in laborious detail the SAA human/machine interface. Why are SAA and CUA important? Regardless of what you think about them, the compelling fact of the matter is that many large corporations are attempting to settle on a common user interface that spans a variety of hardware platforms. This drive is in part motivated by the hope that users will be able to migrate easily from machine to machine without the customary learning curve associated with the transition. Corporations are starting to require vendors to provide SAA, CUA-compliant software. Microsoft has been actively trying to capitalize on this by making Windows SAA, CUA-compliant (hence all the unusual keyboard accelerators). MDI, being an integral part of the Microsoft Windows strategy, fits into this overall standard. The net effect--and this is why MDI is important for you as a software developer--is that if you use the MDI interface (as opposed to some other scheme) in your application, your potential users will already be familiar with the interface and you could potentially sell more software. At the very least, you should take a close look at the IBM SAA, CUA specification and give it careful consideration. Personally, I have a hard time living with the constraints CUA puts on me as a developer, but I am willing to live with them if I can put my programs in front of a larger customer base. I hope this discussion has given you ideas and insights that will help you in your own development. MDI just might be the answer to some technical problem you are struggling with. As you consider MDI, realize that to a large extent it has evolved from the need for an organized way of handling multiple documents within a single desktop. This evolution has been at best troublesome and is still somewhat at odds with the underlying environment. Perhaps in the future something like the MDI API might be included in the Windows or OS/2 Presentation Manager API, saving both you and me a great deal of effort. Until that time, you have access to a little more information than you did before. COLORS - MAKE File # compilation flags CFLAGS=-AM -c -Gsw -Osal -W2 -Zp # COLORS colors.res: colors.rc colors.ico colors.h mdi.h rc -r colors.rc colors.obj: colors.c colors.h mdi.h cl $(CFLAGS) colors.c colors.exe: colors.obj colors.def mdi.lib link4 colors,colors/ALIGN:16,colors,mdi+mlibw+mlibcew/NOE,colors rc colors.res colors.exe: colors.res rc colors.res COLORS.DEF - DEF File NAME COLORS DESCRIPTION 'Multiple Document Interface' STUB 'WINSTUB.EXE' CODE MOVEABLE DISCARDABLE PURE LOADONCALL SHARED DATA MOVEABLE MULTIPLE HEAPSIZE 2048 STACKSIZE 2048 EXPORTS MainWndProc @1 ColorWndProc @2 MainDlgNew @3 MainDlgAbout @4 MdiMsgHook @5 MdiDlgUnhide @6 COLORS.RC - Resource File /* COLORS.RC - Resources for COLORS program */ /* COLORS section of file */ #include <windows.h> #include "colors.h" #include "mdi.h" MainIcon ICON colors.ico MainMenu MENU BEGIN POPUP "&File" BEGIN MENUITEM "&New...", IDM_NEW MENUITEM "&Open...", IDM_OPEN, GRAYED MENUITEM "&Save", IDM_SAVE, GRAYED MENUITEM "Save &As...", IDM_SAVEAS, GRAYED MENUITEM "&Close", IDM_CLOSE, GRAYED MENUITEM SEPARATOR MENUITEM "&Exit", IDM_EXIT MENUITEM "A&bout...", IDM_ABOUT END END RedMenu MENU BEGIN POPUP "&File" BEGIN MENUITEM "&New...", IDM_NEW MENUITEM "&Open...", IDM_OPEN, GRAYED MENUITEM "&Save", IDM_SAVE, GRAYED MENUITEM "Save &As...", IDM_SAVEAS, GRAYED MENUITEM "&Close", IDM_CLOSE MENUITEM SEPARATOR MENUITEM "&Exit", IDM_EXIT MENUITEM "A&bout...", IDM_ABOUT END POPUP "&Red" BEGIN MENUITEM "&0 %", IDM_0 MENUITEM "&25 %", IDM_25 MENUITEM "&50 %", IDM_50 MENUITEM "&75 %", IDM_75 MENUITEM "&100 %", IDM_100, CHECKED END END GreenMenu MENU BEGIN POPUP "&File" BEGIN MENUITEM "&New...", IDM_NEW MENUITEM "&Open...", IDM_OPEN, GRAYED MENUITEM "&Save", IDM_SAVE, GRAYED MENUITEM "Save &As...", IDM_SAVEAS, GRAYED MENUITEM "&Close", IDM_CLOSE MENUITEM SEPARATOR MENUITEM "&Exit", IDM_EXIT MENUITEM "A&bout...", IDM_ABOUT END POPUP "&Green" BEGIN MENUITEM "&0 %", IDM_0 MENUITEM "&25 %", IDM_25 MENUITEM "&50 %", IDM_50 MENUITEM "&75 %", IDM_75 MENUITEM "&100 %", IDM_100, CHECKED END END BlueMenu MENU BEGIN POPUP "&File" BEGIN MENUITEM "&New...", IDM_NEW MENUITEM "&Open...", IDM_OPEN, GRAYED MENUITEM "&Save", IDM_SAVE, GRAYED MENUITEM "Save &As...", IDM_SAVEAS, GRAYED MENUITEM "&Close", IDM_CLOSE MENUITEM SEPARATOR MENUITEM "&Exit", IDM_EXIT MENUITEM "A&bout...", IDM_ABOUT END POPUP "&Blue" BEGIN MENUITEM "&0 %\t 0", IDM_0 MENUITEM "&25 %\t 2", IDM_25 MENUITEM "&50 %\t 5", IDM_50 MENUITEM "&75 %\t 7", IDM_75 MENUITEM "&100 %\t 1", IDM_100, CHECKED END END BlueAccel ACCELERATORS BEGIN "0", IDM_0 "2", IDM_25 "5", IDM_50 "7", IDM_75 "1", IDM_100 END STRINGTABLE BEGIN IDS_TITLE, "Multiple Document Interface" IDS_MAINCLASS, "MdiMainClass" IDS_COLORCLASS, "MdiChildClass" END MainNew DIALOG 50, 50, 144, 60 STYLE WS_DLGFRAME | WS_POPUP BEGIN GROUPBOX "New" -1, 4, 4, 100, 52 RADIOBUTTON "&Red", DLGNEW_RED, 8, 16, 88, 10 RADIOBUTTON "&Green", DLGNEW_GREEN, 8, 28, 88, 10 RADIOBUTTON "&Blue", DLGNEW_BLUE, 8, 40, 88, 10 DEFPUSHBUTTON "&OK" IDOK, 108, 8, 32, 14 PUSHBUTTON "&Cancel" IDCANCEL, 108, 28, 32, 14 END MainAbout DIALOG 22, 17, 156, 100 STYLE WS_DLGFRAME | WS_POPUP BEGIN CTEXT "Multiple Document Interface", -1, 0, 8,152, 8 CTEXT "By Geoffrey D. Nicholls", -1, 0, 20,152, 8 CTEXT "and Kevin P. Welch", -1, 0, 28,152, 8 CTEXT "(C) Copyright 1988", -1, 0, 40,152, 8 CTEXT "Eikon Systems, Inc.", -1, 0, 48,152, 8 CTEXT "Version 1.00 1-Nov-88", -1, 0, 60,152, 8 DEFPUSHBUTTON "Ok" IDOK, 60, 80, 32, 14, WS_GROUP END /* MDI section of file */ MdiMenu MENU BEGIN MENUITEM "&New", IDM_NEWWINDOW, GRAYED MENUITEM SEPARATOR MENUITEM "&Tile", IDM_ARRANGE, GRAYED MENUITEM "Tile &Always", IDM_ARRANGEALL, GRAYED MENUITEM SEPARATOR MENUITEM "&Hide", IDM_HIDE, GRAYED MENUITEM "&Unhide...", IDM_UNHIDE END MdiChildAccel ACCELERATORS BEGIN VK_F4, IDM_CLOSE, VIRTKEY, NOINVERT, CONTROL VK_F5, IDM_RESTORE, VIRTKEY, NOINVERT, CONTROL VK_F6, IDM_NEXTWINDOW, VIRTKEY, NOINVERT, CONTROL VK_F6, IDM_PREVWINDOW, VIRTKEY, NOINVERT, CONTROL, SHIFT VK_F7, IDM_MOVE, VIRTKEY, NOINVERT, CONTROL VK_F8, IDM_SIZE, VIRTKEY, NOINVERT, CONTROL VK_F10, IDM_MAXIMIZE, VIRTKEY, NOINVERT, CONTROL END MdiUnhide DIALOG 50, 50, 132, 68 STYLE WS_DLGFRAME | WS_POPUP BEGIN LTEXT "&Unhide", -1, 4, 4, 88, 10 LISTBOX DLGUNHIDE_LB, 4, 16, 88, 48, WS_TABSTOP DEFPUSHBUTTON "&OK" IDOK, 96, 8, 32, 14 PUSHBUTTON "&Cancel" IDCANCEL, 96, 28, 32, 14 END COLORS.H - Header File / * COLORS.H - Include for COLORS program */ /* Resource file constants */ /* Strings */ #define IDS_TITLE 1 #define IDS_MAINCLASS 2 #define IDS_COLORCLASS 3 /* Debugging menu choice */ #define IDM_DEBUG 0x100 /* File Menu Choices */ #define IDM_NEW 0x101 #define IDM_OPEN 0x102 #define IDM_SAVE 0x103 #define IDM_SAVEAS 0x104 #define IDM_PRINT 0x105 #define IDM_ABOUT 0x106 #define IDM_EXIT 0x107 /* Color Menu Choices */ #define IDM_0 0x108 #define IDM_25 0x109 #define IDM_50 0x10a #define IDM_75 0x10b #define IDM_100 0x10c /* New dialog box */ #define DLGNEW_RED 0x100 #define DLGNEW_GREEN 0x101 #define DLGNEW_BLUE 0x102 /* Window extra constants */ #define WE_COLOR 0 #define WE_SHADE 2 #define WE_EXTRA 4 #define COLOR_RED 0 #define COLOR_GREEN 1 #define COLOR_BLUE 2 /* Function prototypes */ int PASCAL WinMain( HANDLE, HANDLE, LPSTR, int ); HWND MainInit( HANDLE, HANDLE, int ); long FAR PASCAL MainWndProc( HWND, unsigned, WORD, LONG ); BOOL ColorInit( HANDLE ); HWND ColorCreate( HWND, int ); long FAR PASCAL ColorWndProc( HWND, unsigned, WORD, LONG ); int FAR PASCAL MainDlgNew( HWND, unsigned, WORD, LONG ); int FAR PASCAL MainDlgAbout( HWND, unsigned, WORD, LONG ); COLORS.C - Source File for COLORS.EXE /* COLORS.C - Colorful MDI Children */ #include <string.h> #include <stdio.h> #include <windows.h> #include "colors.h" #include "mdi.h" /* Static variables */ /* Text for client area */ static char *szShadings[5] = {"0 %", "25 %", "50 %", "75 %", "100 %"}; /* Titles of documents */ static char *szTitles[3] = { "Red", "Green", "Blue" }; /* Count of each document (for titles) */ static int wCounts[3] = { 0, 0, 0 }; /* Color & Shading table */ static DWORD rgbColors[3][5] = { { /* RED */ RGB(255,255,255), /* 0 % */ RGB(255,192,192), /* 25 % */ RGB(255,128,128), /* 50 % */ RGB(255,64,64), /* 75 % */ RGB(255,0,0) /* 100 % */ }, { /* GREEN */ RGB(255,255,255), /* 0 % */ RGB(192,255,192), /* 25 % */ RGB(128,255,128), /* 50 % */ RGB(64,255,64), /* 75 % */ RGB(0,255,0) /* 100 % */ }, { /* BLUE */ RGB(255,255,255), /* 0 % */ RGB(192,192,255), /* 25 % */ RGB(128,128,255), /* 50 % */ RGB(64,64,255), /* 75 % */ RGB(0,0,255) /* 100 % */ } }; /* * First routine called by windows. Calls the initialization routine * and contains the message loop. */ int PASCAL WinMain( HANDLE hInst, HANDLE hPrevInst, LPSTR lpszCmdLine, int nCmdShow ) { HWND hwndColors; /* Handle to our MDI desktop */ MSG msg; /* Current message */ /* Initialize things needed for this application */ hwndColors = MainInit( hPrevInst, hInst, nCmdShow ); if ( !hwndColors ) { /* Failure to initialize */ return NULL; } /* Process messages */ while ( MdiGetMessage( hwndColors, &msg, NULL, NULL, NULL ) ) { /* Normal message processing */ if ( !MdiTranslateAccelerators( hwndColors, &msg ) ) { TranslateMessage( &msg ); DispatchMessage( &msg ); } } /* Done */ return msg.wParam; } /* * First, initialize the MDI desktop and the color document windows. Second, create the MDI desktop and then create one red document. */ HWND MainInit( HANDLE hPrevInst, HANDLE hInst, int nCmdShow ) { char szTitle[80]; /* Title of our MDI desktop */ char szClass[80]; /* Class name of our MDI desktop */ HWND hwndColors; /* Handle to our MDI desktop */ WNDCLASS WndClass; /* Class structure */ /* Window classes */ if ( !hPrevInst ) { /* Main window */ LoadString( hInst, IDS_MAINCLASS, szClass, sizeof( szClass ) ); /* Prepare registration */ memset( &WndClass, 0, sizeof( WndClass ) ); WndClass.style = CS_HREDRAW | CS_VREDRAW; WndClass.lpfnWndProc = MainWndProc; WndClass.hInstance = hInst; WndClass.hIcon = LoadIcon( hInst, "MainIcon" ); WndClass.hCursor = LoadCursor( NULL, IDC_ARROW ); WndClass.hbrBackground = COLOR_APPWORKSPACE + 1; WndClass.lpszMenuName = "MainMenu"; WndClass.lpszClassName = szClass; /* Register main class */ if ( !RegisterClass( &WndClass ) ) return NULL; /* Allow each of the MDI children to do its own initialize */ if ( !ColorInit(hInst) ) return NULL; } /* Create our main overlapped window */ LoadString( hInst, IDS_TITLE, szTitle, sizeof( szTitle ) ); hwndColors = MdiMainCreateWindow( szClass, szTitle, WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInst, NULL ); /* Did we create successfully? */ if ( !hwndColors ) return NULL; /* Give us one red child to begin with */ if ( !ColorCreate( hwndColors, COLOR_RED ) ) { DestroyWindow( hwndColors ); return NULL; } /* Ready */ ShowWindow( hwndColors, nCmdShow ); UpdateWindow( hwndColors ); /* Done */ return hwndColors; } /* Handle messages for our MDI desktop. This includes any WM_COMMAND messages that are received when NO document is visible on the desktop.*/ long FAR PASCAL MainWndProc( HWND hwndColors, unsigned message, WORD wParam, LONG lParam ) { FARPROC lpProc; /* Procedure instance for dialogs */ HANDLE hInst; /* Current instance handle */ switch ( message ) { case WM_COMMAND: hInst = GetWindowWord( hwndColors, GWW_HINSTANCE ); switch( wParam ) { case IDM_NEW: /* New dialog box */ lpProc = MakeProcInstance( MainDlgNew, hInst ); switch( DialogBox( hInst, "MainNew", hwndColors, lpProc ) ) { case DLGNEW_RED: ColorCreate( hwndColors, COLOR_RED ); break; case DLGNEW_GREEN: ColorCreate( hwndColors, COLOR_GREEN ); break; case DLGNEW_BLUE: ColorCreate( hwndColors, COLOR_BLUE ); break; } FreeProcInstance( lpProc ); break; case IDM_OPEN: break; case IDM_ABOUT: /* About dialog box */ lpProc = MakeProcInstance( MainDlgAbout, hInst ); DialogBox( hInst, "MainAbout", hwndColors, lpProc ); FreeProcInstance( lpProc ); break; case IDM_EXIT: /* Tell application to shut down */ PostMessage( hwndColors, WM_SYSCOMMAND, SC_CLOSE, 0L ); break; } break; case WM_DESTROY: PostQuitMessage( 0 ); break; } return MdiMainDefWindowProc(hwndColors, message, wParam, lParam); } /* Register the document class. */ BOOL ColorInit( HANDLE hInst ) { char szClass[80]; /* Class name */ WNDCLASS WndClass; /* Class structure */ /* Get class name */ LoadString( hInst, IDS_COLORCLASS, szClass, sizeof( szClass ) ); /* Prepare registration */ memset( &WndClass, 0, sizeof( WndClass ) ); WndClass.style = CS_HREDRAW | CS_VREDRAW; WndClass.lpfnWndProc = ColorWndProc; WndClass.cbWndExtra = WE_EXTRA; WndClass.hInstance = hInst; WndClass.hCursor = LoadCursor( NULL, IDC_ARROW ); WndClass.hbrBackground = GetStockObject( GRAY_BRUSH ); WndClass.lpszClassName = szClass; /* Register */ return RegisterClass( &WndClass ); } /* Create a document window of a given color on the MDI desktop. It loads the appropriate menu, and accelerator table if the color is BLUE. It initializes color and shading in the window extra words.*/ HWND ColorCreate( HWND hwndParent, int wType ) { char szClass[80]; /* Class name for documents */ char szTitle[80]; /* Title for this document */ HANDLE hAccel = NULL; /* Accelerator for blue doc only */ HANDLE hInst; /* Current instance handle */ HMENU hmenuChild; /* Handle to document's menu */ HWND hwndChild; /* Handle to document */ /* Get important info */ hInst = GetWindowWord( hwndParent, GWW_HINSTANCE ); LoadString( hInst, IDS_COLORCLASS, szClass, sizeof( szClass ) ); sprintf( szTitle, "%s%d", szTitles[wType], ++wCounts[wType] ); switch( wType ) { case COLOR_RED: hmenuChild = LoadMenu( hInst, "RedMenu" ); break; case COLOR_GREEN: hmenuChild = LoadMenu( hInst, "GreenMenu" ); break; case COLOR_BLUE: hmenuChild = LoadMenu( hInst, "BlueMenu" ); hAccel = LoadAccelerators( hInst, "BlueAccel" ); break; } /* Create */ hwndChild = MdiChildCreateWindow( szClass, szTitle, WS_MDICHILD, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, hwndParent, hmenuChild, hInst, 0L ); /* Success? */ if ( hwndChild ) { SetWindowWord( hwndChild, WE_SHADE, IDM_100 ); SetWindowWord( hwndChild, WE_COLOR, wType ); MdiSetAccel( hwndChild, hAccel ); } return hwndChild; } /* Handle messages for our documents. WM_COMMAND messages arrive at this procedure just as if the menu were attached to this window.*/ long FAR PASCAL ColorWndProc( HWND hwndChild, unsigned message, WORD wParam, LONG lParam ) { char szText[20]; /* Client area text */ HBRUSH hBrush; /* Brush for filling document */ PAINTSTRUCT Paint; /* Paint structure */ int wColor; /* Color of current document */ int wShade; /* Shading of current document */ switch ( message ) { case WM_COMMAND: switch( wParam ) { /* File menu */ case IDM_SAVE: case IDM_SAVEAS: case IDM_PRINT: break; case IDM_CLOSE: PostMessage( hwndChild, WM_SYSCOMMAND, SC_CLOSE, lParam ); break; case IDM_0: case IDM_25: case IDM_50: case IDM_75: case IDM_100: CheckMenuItem( MdiGetMenu( hwndChild ), SetWindowWord( hwndChild, WE_SHADE, wParam ), MF_UNCHECKED ); CheckMenuItem( MdiGetMenu( hwndChild ), GetWindowWord( hwndChild, WE_SHADE ), MF_CHECKED ); InvalidateRect( hwndChild, (LPRECT) NULL, TRUE ); break; } break; case WM_ERASEBKGND: return TRUE; case WM_PAINT: wColor = GetWindowWord( hwndChild, WE_COLOR ); wShade = GetWindowWord( hwndChild, WE_SHADE ); BeginPaint( hwndChild, &Paint ); hBrush = CreateSolidBrush( rgbColors[wColor][wShade - IDM_0]); FillRect( Paint.hdc, &Paint.rcPaint, hBrush ); DeleteObject( hBrush ); strcpy( szText, szShadings[wShade - IDM_0] ); TextOut( Paint.hdc, 0, 0, szText, strlen( szText ) ); EndPaint( hwndChild, &Paint ); break; } return MdiChildDefWindowProc( hwndChild, message, wParam, lParam); } /* Handle the NEW dialog box.*/ int FAR PASCAL MainDlgNew( HWND hDlg, unsigned message, WORD wParam, LONG lParam ) { static int iButton; /* Keep track of radio buttons */ int iReturn = FALSE; /* Return value */ switch ( message ) { case WM_INITDIALOG: SendMessage( hDlg, WM_COMMAND, DLGNEW_RED, 0L ); iReturn = TRUE; break; case WM_COMMAND: switch( wParam ) { case DLGNEW_RED: case DLGNEW_GREEN: case DLGNEW_BLUE: iButton = wParam; CheckRadioButton( hDlg, DLGNEW_RED, DLGNEW_BLUE, iButton ); if ( HIWORD( lParam ) == BN_DOUBLECLICKED ) { SendMessage( hDlg, WM_COMMAND, IDOK, 0L ); } break; case IDOK: EndDialog( hDlg, iButton ); break; case IDCANCEL: EndDialog( hDlg, 0 ); break; } break; } return iReturn; } /* Handle the ABOUT dialog box.*/ int FAR PASCAL MainDlgAbout( HWND hDlg, unsigned message, WORD wParam, LONG lParam ) { int iReturn = FALSE; /* Return value */ switch( message ) { case WM_INITDIALOG: iReturn = TRUE; break; case WM_COMMAND: EndDialog( hDlg, TRUE ); break; } return iReturn; } Planning and Writing a Multithreaded OS/2 Program with Microsoft C Richard Hale Shaw From a programmer's perspective, OS/2 systems are a lot like a new programming language. In order to become fluent you have to begin learning the idiom by writing programs with it. In this article, we'll cover the highlights of setting up and installing the Microsoft C Version 5.1 compiler for OS/2 development and briefly look at the header files included with it. Then we'll take a closer look at the OS/2 Application Programming Interface (API) with the object of writing our first OS/2 program. Last, we'll begin to explore the world of multithreaded programming and produce a multithreaded version of the C programmer's much beloved HELLO.C program. Compiler Setup The first concern for installing Microsoft C 5.1 is to ensure that you have enough disk space. The minimum space needed for the protected mode version of the compiler is approximately 3.5Mb (which includes two libraries). If you install the full protected mode compiler, you'll need closer to 4.5Mb. If you're using the compiler to produce applications for both DOS and OS/2, you'll probably need over 6Mb. The Setup program can be instructed to install the portions of the compiler that you want. You must also decide which directory structure the compiler will use. We discussed the preferred OS/2 directory structure, shown in Figure 1, in the first article of this series. You may recall that the \OS2\PBIN directory contains only protected mode executables; \OS2\RBIN holds only real mode programs; and \OS2\BIN contains bound executables, that is, programs that can operate under DOS and OS/2. If you install the compiler while you are running OS/2, you'll find that the Setup program recommends that you incorporate the traditional C compiler directories into the same structure, as shown in Figure 2. The \OS2\LIB directory will contain all compiler libraries, \OS2\INCLUDE will contain the header files, and \OS2\SOURCE will be used for ancillary documentation and source files. Note that the compiler will also install an alternative set of header files under the \OS2\INCLUDE\MT directory. These are headers for the multithreaded version of the library, which we'll discuss later in this article. Microsoft C 5.1 includes facilities for producing traditional DOS programs, OS/2 programs, and programs designed to execute under both environments (bound applications). To this end, one of the more significant features available when you install the compiler under OS/2 are combined libraries. Previous versions of the C compiler already include a plethora of libraries, including three floating-point libraries and a graphics library. When you add to these libraries the need to keep versions of them for each memory model used, plus libraries for OS/2 development, you will find yourself running out of disk space fast. You can, however, combine the basic library for each memory model with the graphics library (if selected) and appropriate floating-point library. Combining the libraries allows you to keep only one library per memory model. The Installation/Setup program will do this automatically for you and will delete the leftover component libraries for you if you elect to do so. Another important option is the ability to create a dual-mode or bound compiler. This is a version of the compiler that will run under both OS/2 and DOS. The Setup program will leave a copy of BINDC.CMD (BINDC.BAT if you installed the compiler while running DOS), which can be run to produce the bound version. We'll discuss the production of bound applications later, but for now keep in mind that a bound application is one that calls only the subset of API services known as Family Application Programming Interface (FAPI) functions. These are functions that are available under both DOS and OS/2. Although limiting yourself to these functions will restrict you from using OS/2 multitasking services (after all, DOS does not offer these services), it does ensure that a program will run without recompilation under both environments. BINDC will leave a bound version of the compiler in the directory of your choice when it finishes. Since it's a bound executable, it is recommended that you have BINDC place its output in the \OS2\BIN directory (where dual-mode programs are kept) and delete the original copies of the compiler. You won't need them, and they'll just take up disk space. The dual-mode version will be bigger than the original, but you'll find it simpler to use only the one version. The setup program produces two files: NEW-VARS.CMD and NEW-CONF.SYS. The NEW-VARS.CMD file contains the environment variable settings that should be in place when you run the compiler, based on the directory selections you made during installation. You can include these either in STARTUP.CMD or in the CMD file used for setting up your C development session through the OS/2 Program Starter [see "Using the OS/2 Environment to Develop DOS and OS/2 Applications," MSJ (Vol. 4, No. 1)]. NEW-CONF.SYS contains the additions that ought to be added to CONFIG.SYS (CONFIG.OS2 in older, dual-boot environments). New Compiler Options There are three compiler command-line options that are pertinent to OS/2 programming. The /Lr option designates the compilation of a conventional real mode executable (the default in earlier versions of the compiler). The /Lp option, however, signals the compiler to compile and link for the protected mode. When you use this switch, the protected mode version of a given library will be used. For instance, if a conventional, real mode, small mem-ory model compilation used SLIBCE.LIB (the small model combined library with floating-point emulation), adding the /Lp option instructs the linker to use SLIBCEP.LIB (the protected mode version of the library). The third option, /Fb, directs the linker to run BIND.EXE after linking is complete. BIND is a utility that converts protected mode programs into dual-mode applications, which can be run in either real or protected mode. With these options in mind, and using the directory structure discussed earlier, we can construct a generic MAKE file for which we use MAKE to build our first OS/2 programs (shown in Figure 3). This MAKE file, which is called HELLO (since it will be used to compile HELLO.C), forms the basis for the MAKE files we'll use to build other programs. It sets up the environment variables INCLUDE and LIB for the compiler and passes a number of options to CL. Note that the MAKE file uses both /Lp and /Fb to instruct the compiler to produce a protected mode bound version of the program. The /W3 option forces the compiler to use warning level 3, the most severe warning level available from the compiler. This option is very useful to ensure strict type checking of different objects and arguments against OS/2-type definitions. The /Zpe option combines two options: /Zp and /Ze. The former instructs the compiler to produce packed, or byte-aligned (as opposed to word-aligned), structures. The latter allows extensions to C, particularly the far, near, and pascal keywords, which OS/2 programs will reference frequently. The /Ox option turns on maximum optimization, and the /I option specifies the INCLUDE directory, which the compiler will use. The /G2 option forces the production of code for the 80286, which is a handy option, since 286 instructions are generally more efficient than pure 8086 instructions. Although this option restricts the program to run on either an 80286 or above, it's not a problem in the OS/2 environment, since OS/2 will only run on a machine with an 80286 or 80386 (the 80386 also executes 80286 code). However, it could cause problems if the bound executable is run under DOS on an 8088/8086. Several additional points should be made about this MAKE file. First, note that for debugging purposes we will want to include symbolic debugging information with the /Zi option and turn off optimization with /Od. Thus, I've included the additional line of options for debugging. You can enable these options and disable the others by removing the # in front of one line and inserting a # in front of the other line of options (# is the comment marker in MAKE files). Also, note the use of the /NOE option that CL passes to the linker, which prevents the linker's extended library search. This option will ensure that the correct versions of the library functions are linked to the program. Figure 3: #file: hello #generic OS/2 MAKE file for producing 'bound' executables # #Currently set for making HELLO.C #Usage: C>make hello INCLUDE=\os2\include LIB=\os2\lib COPT=/Lp /Fb /W3 /Zpe /G2 /Ox /I$(INCLUDE) #COPT=/Lp /Fb /W3 /Zpie /G2 /Od /I$(INCLUDE) hello.exe: hello.c hello cl $(COPT) hello.c /link /noe #end OS/2 System Calls OS/2 system services are available to application programs through the OS/2 API. Unlike the interrupt-based interface of DOS, the API makes all services available through system calls (also known as call-gates). While the DOS interface was limited to 256 functions (via Int 21h), there is no limit on the number of services that could be added to OS/2 in the future. And whereas DOS services required the use of registers to receive or return values, OS/2 system services pass these on the stack. Thus, all OS/2 services use the same format: if a system service is capable of returning error values, it will return a zero when successful and an unsigned nonzero integer on error. In addition, as stated earlier, the subset of API functions known as Family API (FAPI) functions will operate under both OS/2 and DOS, allowing you to write bound executable programs that operate in both environments. When you include a call to an API function in your OS/2 program, the linker does not add the code for the function to the executable program as it would for a DOS program. Instead, it will add instructions for loading the function's code from the appropriate OS/2 dynamic-link library (DLL). When the program executes, OS/2 will load the necessary DLLs (if it hasn't already loaded them for some other application program). Thus, the functions will be available to the program at run time. Only one instance of an OS/2 API function will be loaded into memory, even if more than one application program is using it. Note that for FAPI functions in a bound executable program, the linker will include both the DLL calling code and the real mode executable code for the function. The former will be used when the program is operating in the OS/2 protected mode, and the latter will invoke Int 21h when in MS-DOS real mode. Since OS/2 loads API routines from a DLL, they are located in a different segment from that of the calling routine and must be reached with a FAR call. There are four general types of parameters that can be passed to an API system service: byte (or char), word (or unsigned), double word (unsigned long), and address (far pointer). If the service requires only the value of the parameter, then a copy of it is passed (call by value). If the service requires a pointer to a parameter, however, the address is passed to the service (call by reference). Because the calls are FAR calls, addresses passed must also be FAR. To ensure that you are passing a far address correctly to a system service, you should declare the object as far, use a cast, or compile with the large data model. Meticulous use of the OS/2 object definitions found in the new header files (described below) will eliminate most problems. You might also note that segment values that are found in far addresses are really selectors. Although they have the same segment:offset form found in far addresses under MS-DOS real mode, OS/2's protected mode requires a selector value. A selector is actually an index into a table of segment addresses and has no correspondence to a physical segment. Selectors must be used since OS/2's virtual memory management may change the physical segment location as it is moved around in memory or swapped to disk. The layer of abstraction they provide allows you to address a far object without having to know its real physical location in memory--or whether it's in memory at all (taken care of for you by OS/2). API functions use the Pascal calling convention. This convention specifies that there be a fixed number of arguments to the function and that the arguments are pushed on the stack left to right (the order in which you specify them in your source code). In addition, Pascal calls do not require the function being called to clean up the stack. All these requirements result in smaller, faster function calls. This scheme is the reverse of the traditional C calling convention, which pushes arguments right to left (allowing a variable number of arguments); requires the caller to clean up the stack; and generally produces slower, larger code. The OS/2 header files discussed below specify API functions as far pascal calls, so most of the time you will not have to take any steps beyond including the appropriate header files in your program. The far pascal convention is defined in the OS/2 header files as APIENTRY. Additionally, API functions use the Microsoft(R) Windows convention of two- or three-phrase descriptive names with both uppercase and lowercase letters. Keyboard subsystem services are provided by functions whose names begin with Kbd; Mouse and Video subsystem services names begin with Mou and Vio respectively. The names of all remaining operating system services (OS/2 kernel calls) begin with Dos. Some brief examples of these include DosRead, DosCreateThread, KbdCharIn, and VioWrtTTY. The documentation for the OS/2 API functions can be found in the OS/2 Programmer's Reference. This manual is a part of the Microsoft OS/2 Programmer's Toolkit and the Microsoft OS/2 Software Development Kit. Header Files Microsoft C 5.1 provides six new header files, some or all of which need to be included in programs that take advantage of the OS/2 API. These header files (listed in Figure 4) provide the API declarations, function prototypes, macros, constants, and type definitions needed by an OS/2 C program. As a routine part of getting started programming under OS/2, you should become intimately familiar with their contents. You'll find that most of the type definitions and structures use a naming convention similar to that used by the system services described earlier. Also, since many system services will place return values in the structures defined in these header files, OS/2 programming will be easier later if you study their contents now. The OS/2 header files are hierarchically nested, so that you need only include OS2.H in your program most of the time. The remaining header files and definitions can be included by using various combinations of control macros (listed in Figure 5), which should be defined in your program before including OS2.H. This is particularly helpful when you are using only a small subset of API functions in your program, since the headers are large and compilation will proceed more quickly if you include only what the program requires. OS2.H itself includes two header files. The first file, OS2DEF.H, contains most of the commonly used definitions, typedefs, macros, constants, and structures. The second, BSE.H, indirectly contains the base definitions for the various OS/2 subsystems (Keyboard, Video, Mouse, and Dos), plus error-handling macros by optionally including three additional header files: BSEDOS.H, BSESUB.H, and BSEERR.H. BSEDOS.H contains definitions required for using the OS/2 kernel system service, and can be included by defining INCL_DOS before the #include for OS2.H in your program. BSESUB.H contains all the definitions required for using any of the OS/2 subsystems (Kbd, Mou or Vio) and is included by defining INCL_SUB. BSEERR.H contains all error-related macros and is included through INCL_DOSERRORS. If necessary, you can force the blanket inclusion of all API declarations and definitions by defining INCL_BASE in your program prior to the #include for OS2.H: #define INCL_BASE #include <os2.h> Note that many commonly used components will be defined by default unless you define INCL_NOCOMMON in your program. You can also include specific kernel services components by defining the macros listed in Figure 5. Figure 4: OS2.H - Always #included in your program OS2DEF.H - Common definitions BSE.H - Base definitions, #includes the following: BSEDOS.H - Kernel services definitions BSESUB.H - Kbd, Vio, Mou definitions BSEERR.H - Error macros Figure 5: Define this macro: To include definitions/declarations for: INCL_BASE All services INCL_DOS Kernel services INCL_SUB Subsystem (Kbd, Vio, Mou) INCL_DOSERRORS Error macros INCL_DOSPROCESS Processes and threads calls INCL_DOSINFOSEG Information segment calls INCL_DOSFILEMGR File management calls INCL_DOSMEMMGR Memory management calls INCL_DOSSEMAPHORES Semaphore functions INCL_DOSDATETIME Date/Time and Timer calls INCL_DOSMODULEMGR Module management services INCL_DOSNLS National language services INCL_DOSSIGNALS Signal functions INCL_DOSMONITORS Monitor services INCL_DOSSESMGR Session management calls INCL_DOSDEVICES Device and IOPL services INCL_DOSQUEUES Queue functions INCL_RESOURCES Resource-support functions A First OS/2 Program Once you have successfully installed the compiler, there is nothing to keep you from writing your first OS/2 program. Figure 6 lists the code for an OS/2 version of Dennis Ritchie's famous HELLO.C. At first glance, an OS/2 program such as this version of HELLO.C might appear extremely bizarre. It certainly does not resemble the Kernighan and Ritchie version that we've all come to know and love. Nevertheless, it accomplishes many of the same purposes. It allows us to begin to explore the OS/2 API; introduces us to a real use of some of the header files, defines, and functions; and most of all, gets us started writing our first real OS/2 program. A second glance will reveal the familiar structure so often used to write maintainable, efficient C programs. You'll note the use of INCL_SUB and INCL_DOS to include function prototypes for the single call to the Vio subsystem and the single kernel call in the program. The printf call has been replaced with a call to VioWrtTTY, which prints a string on the logical screen used by our program (all video access in OS/2 is done through a logical screen group). Note that this function requires both the address of the string and the string length and that it appropriately handles the C escape sequences \r and \n, to generate a carriage-return/line-feed combination. Also note that all Vio calls require a zero for their last parameter. After printing the string, a kernel call to DosExit terminates the entire program. The first parameter specifies whether the entire process or the current thread should be terminated (more on this in the next section). The second parameter is the exit code, which is passed to the parent process and is identical to the one that is passed in exit, the traditional C termination function. Finally, note that this program can be compiled, linked, and bound to create a dual-mode application. Thus, the same executable program will run, unmodified, under either OS/2 or DOS. Note that the MAKE file for this program can be found in Figure 3. If you haven't already compiled and run a C program using API calls, I suggest you type in HELLO.C, compile it, and run it. It will certainly help make OS/2's magic more real and prepare you for our next step: the world of multithreaded programs. Figure 6: /* hello.c RHS 10/14/88 * * 1988 OS/2 version of K&R's hello.c */ #define INCL_SUB /* for Vio calls used */ #define INCL_DOS /* for DosExit call used */ #include <stdio.h> #include <string.h> #include <os2.h> void main(void); /* function prototype */ void main(void) { char *hello_str = "Hello, world!\r\n"; int len = strlen(hello_str); VioWrtTTy(hello_str,len,0); /* print the message */ DosExit(EXIT_PROCESS,0); /* exit the program */ } Multiple Threads The single most significant difference between OS/2 and DOS are the former's facilities for multitasking. Multitasking will dramatically increase the efficiency of most applications that are designed to take advantage of it. However, not only do OS/2's facilities allow for the simultaneous execution of more than one program, but what's more they permit OS/2 to execute different parts of the same program at the same time. The OS/2 multitasking model, shown in Figure 7, is built of threads, processes, and screen groups. A thread is the smallest unit of execution, a piece of code dispatched by the system. Threads are organized into a process, or the portion of a program that controls the ownership of resources, such as files, memory, and threads. A process is composed of at least one thread and may consist of as many as 255 separate threads. A process's main thread is the one in which execution begins. Note that though a process may use a thread to manage a resource, a thread by itself does not own any resources. A thread inherits the environment (open files, and so on) of which it is a part and shares the same code and data segments as its parent process. Collectively, the processes that share the same logical keyboard and screen are a part of the same screen group. The model allows for multitasking at all three levels (screen group, process, and thread). In this article, however, we're primarily concerned with the simultaneous execution of threads within the same process. Programs that execute in such a manner are called multithreaded programs. Multiple Thread Execution OS/2 lets concurrently executing threads share a single computer's CPU through the use of a preemptive, priority-based task scheduler (later editions of OS/2 will take advantage of multiple-CPU architectures). In reality, only one thread at a time is executing, but the CPU's attention turns so quickly from one thread to another that the threads appear to be executing at the same time--as long as the applications that use multiple threads do not abuse system resources and CPU time in their multithreaded code. The task scheduler controls which thread gets slices of CPU time and how much time is doled out to the thread. A thread's priority controls its access to the CPU (if it has a higher priority, it will get more CPU time relative to other threads). Thus, a previously idle, higher priority thread that is ready to run can preempt CPU time from a lower priority thread that is currently executing. On the other hand, a lower priority thread must wait until all higher priority threads are idle before the scheduler will give it CPU time. The OS/2 task scheduler employs three categories of priorities for scheduling tasks. The highest priority is time-critical, which should be used by tasks that must respond to some type of regularly occurring event (like a communications stream or keyboard input). The second category, regular, is the default priority for a new thread and should be used by most normal threads. The last category, idle, is for threads that should execute when there are no higher priority tasks that are ready or able to execute (such as a print spooler). Note that there are 32 levels of priorities in each category and that the default priority for foreground processes is regular, level 0, whereas processes that run in the background are also regular but have a lower priority level. In general, foreground tasks are given a higher priority than background tasks. The task scheduler always runs the highest priority thread that is capable of executing. If two or more threads with the same priority are ready to run, the scheduler will evenly grant them CPU time on a round-robin basis. If a thread is blocked--that is, it is waiting until some event occurs--OS/2 will suspend it and run another thread. Note that the TIMESLICE= statement in CONFIG.SYS controls the minimum and the maximum time slice values used by OS/2. As mentioned above, a thread is blocked when it is waiting for an event to occur. A thread is running when it is being given CPU time slices. If a thread is no longer blocked but hasn't yet been given CPU time slices, then it is said to be ready to run. Planning a Multithreaded Program As mentioned above, a program or process can have more than one thread of execution. Using multiple threads lets it manipulate and control machine resources more efficiently than would a single-threaded application. For instance, printing, communications file transfers, and database sorting all are tasks that can be performed simultaneously by separate threads of execution while the main thread of an application program continues to serve the end user. Furthermore, in the multitasking environment of OS/2, a multiple-thread architecture is essential to help ensure that no single task will hog machine resources. Gordon Letwin, OS/2 architect, first identified this libertarian approach to sharing resources: programs must obey the rules in order to work together. This approach makes it obvious who the violator is when a program abuses the environment, and permits the system to operate in the most efficient manner possible. Thus, planning a multithreaded application presupposes that the tasks that the program will perform can best be implemented by using multiple threads of execution. There are a number of caveats that help in planning such a program, which can be summarized as: never assume that OS/2 will execute a multitasking program or routine in a specific way. Corollaries of this rule include: ■ Never assume that one routine will execute before another. ■ Never assume that a given routine will execute for a given number of milliseconds. ■ Never assume that future versions of OS/2 will schedule tasks the same way the current one does. ■ Never assume that different threads will always be competing for CPU time. Future versions of OS/2 will run on parallel processors, allowing different threads to execute simultaneously. While you and I both know that they don't really execute concurrently at this point, treat them as if they already do. ■ Never assume any direct correlations between CPU time slices and CPU cycles. ■ Never assume that OS/2 can guarantee which thread will execute first at any point during the course of your program. Although the main thread is always the first thread to execute in the program, there are no guarantees on execution order once the second thread has begun. Obviously, the last caveat doesn't mean that there aren't any controls available for manipulating events or serializing access to resources among multiple threads: that's where semaphores and priority levels come in. We'll discuss these later, but for now remember that when writing a multithreaded application, never assume! A Multithread Program The process of creating an additional thread is fairly simple in itself. For instance, suppose you wanted an application to run uninterrupted, ignoring all keyboard input until the user presses the Esc key, at which point the application would terminate. In a DOS application, there are two possible solutions: you could design the program to occasionally poll the keyboard, which is cumbersome in a complex application, or your program could trap the BIOS keyboard interrupt with code that would signal the main program if the Esc key is pressed. Under OS/2 these approaches are neither necessary nor relevant. Instead, you can start a thread that blocks on keyboard input (that is, it waits until there is keyboard activity). When the user presses a key, the thread examines the key. If the key is any key other than Esc, it will continue to block. If the key is Esc, the thread will terminate the entire process. A brief examination of the program shown in Figure 8 will make this procedure clearer. The main thread begins where all C programs begin, with the call to main. The main thread creates the keyboard thread with the call to the API kernel function, DosCreateThread. Note that DosCreateThread takes several parameters, as shown by the function prototype in Figure 9. The first parameter is the address of a function that contains the code for the thread. Here, the function is innocently named keyboard_thread. The second parameter is the address of a variable into which OS/2 will place the thread's identifier once it has successfully created the thread. The final parameter is the address of the top of the stack allocated for the thread, which should be at least 512 bytes in size. Note that in order to pass the address of the top of the stack area, keythreadstack, we must pass the address of the last byte of keythreadstack in the manner shown. As mentioned earlier, all API functions return nonzero on failure, so we know that a new thread was successfully created if DosCreateThread returns a zero value. The newly created thread will immediately begin execution of the code found in keyboard_thread. Although the call to DosCreateThread could have been placed almost anywhere, calling it early in the program ensures that the program will terminate immediately if the user presses the Escape key. Note that the while(TRUE) statement can be replaced with whatever code you would use for processing in the main thread. Now let's take a look at the keyboard_thread function. The code for a thread should always be contained in a single function. You cannot incorporate the code for this function into main or any other function, nor can you call a thread function directly. Thus, thread functions are really an OS/2 extension to C. Note that keyboard_thread begins executing immediately after creation and then goes into the loop to call the Kbd subsystem function, KbdCharIn. This function's first parameter is the address of an OS/2 KBDKEYINFO structure, which will contain information about the keys pressed upon return (this structure will be discussed in detail in an upcoming installment of this series). The chChar member of the structure will contain the ASCII value of the key pressed. The second parameter specifies how long the function should wait until the user presses a key. Finally, passing IO_WAIT will cause the function to wait for the key indefinitely, in turn causing OS/2 to suspend the calling thread until that thread's screen group receives a key from the user. Once the user presses a key, OS/2 will wake up the thread and return from the KbdCharIn call. The thread then examines the key and breaks out of the loop if it is the Esc key, calling DosExit to terminate the entire process. You might note that the thought behind the function is similar to object-oriented programming: the keyboard_thread function completely encapsulates the keyboard control and program termination sequence. The main program doesn't have to know how it works or what it does--that's taken care of for it by the thread itself. A program's main thread must be kept alive until all other thread activity has finished or can be terminated. When the main thread dies, the other threads die. Thus, if you insert code in the main thread that will exit the process, the keyboard thread (and any other threads) will be destroyed with the main thread. If a secondary thread like the keyboard thread shown here reaches the end of its code without calling a termination routine, it will die, but it will not affect other threads. Threads can explicitly terminate themselves by means of a call to DosExit: DosExit(EXIT_THREAD, term_code); or a thread can terminate the entire process via: DosExit(EXIT_PROCESS, term_code); The MAKE file for this example can be found in Figure 10. The /Gs or /G2s options are used to turn off standard run-time stack checking, since the run-time stack checks will report false stack overflow errors in code for the thread. If, however, you wish to isolate this to the thread code itself, you can insert a #pragma: #pragma check_stack(off) /* thread function is placed here */ #pragma check_stack(on) which will turn off run-time stack checking for the code inserted between the #pragmas. If the main thread needs to close files or shut down other processes or resources before terminating, the thread function could set a semaphore (as a flag) that could be checked occasionally by the main thread, instead of terminating outright. Another alternative would be to use DosExitList, shown in Figure 11. Figure 8: /* * Simple keyboard thread example * * This program illustrates how a process might start * a keyboard thread which will terminate the process when * the user presses the Esc key. * * The program starts a keyboard thread which blocks on keyboard * input and terminates the entire program when the user presses * the Esc key. All other keys are ignored and thrown away. */ #define INCL_DOS #define INCL_SUB #include <os2.h> #include <mt\stdio.h> #include <mt\process.h> #define ESC 0x1b #define TRUE 1 #define THREADSTACK 512 char keythreadstack[THREADSTACK]; void keyboard_thread(void); void main(void); void main(void) { TID threadid; if(DosCreateThread(keyboard_thread, &threadid, &keythreadstack[THREADSTACK-1])) exit(-1); while(TRUE) /* replace this with ; code for main program */ } void keyboard_thread(void) /* keyboard thread code */ { KBDKEYINFO keyinfo; while(TRUE) { KbdCharIn(&keyinfo,IO_WAIT,0); /* wait for keystroke */ if(keyinfo.chChar == ESC) /* if ESC pressed, break */ break; } DosExit(EXIT_PROCESS,0); /* terminate the process */ } Figure 9: unsigned DosCreateThread(void (far *) functionptr(void), TID *threadidptr, void *stack); Reentrance Issues There is one important constraint to consider when writing multiple-thread programs. It is the problem encountered when more than one thread tries to simultaneously execute code that is being used by another thread. This problem does not arise with the OS/2 API functions: their code has been written for a multitasking environment. The standard C library and your own functions, however, are another matter. Consider a scenario like the following: The standard library routine printf uses an internal buffer to format the characters that it will write to the standard output. Suppose one thread is in the middle of executing printf, with half of the buffer formatted, when another thread begins to execute printf's code, overwriting the buffer with its own characters--and resulting in chaos. This behavior is, of course, intolerable, and unless you're going to implement your own set of semaphores to control the use of every library routine (which is not a viable solution), you're going to end up with a mess on your hands. There are, fortunately, two solutions to this problem. First, when writing multithread programs, refrain from using any standard library routines in any but the main thread. This guarantees that only one thread will be using the standard library routines at a time. With the exception of a few reentrant routines, the standard library routines are not reentrant and are designed for single-threaded execution (see Figure 12). If you must control access to a specific routine from the standard library (or one of your own routines), there are two ways you can do it: ■ Use the DosEnterCritSec API call to temporarily freeze the other threads. Although this approach does work, it isn't the best solution and is mentioned here for informational purposes; there are too many things that can go wrong. ■ Use a semaphore to control access to a function. This solution is more practicable, since only the threads that are trying to access the shared code will be affected--the rest will continue to execute (DosEnterCritSec, on the other hand, will freeze all other threads). If you find it impossible to live without the standard library functions, there is one additional alternative: the multithreaded standard library. While earlier versions of the C compiler required that you distinguish between reentrant and nonreentrant functions, Microsoft now supports a version of the standard library, LLIBCMT.LIB, that is completely reentrant and supports multiple threads. If you write programs that use this library, you must use the new _beginthread and _endthread functions that are contained in the library (instead of using DosCreateThread and DosExit). (For more information, see the sidebar "Using the Multithreaded Library: DosCreateThread vs. _beginthread.") Finally, you can write your own functions to accommodate multiple threads. If you choose to do so, there are at least three key guidelines to follow. First, multithreaded functions cannot disable interrupts or issue an INT instruction. Second, they should not alter the contents of a segment register or perform segment manipulations. Last, there must be strict controls on access to global or static data by functions that can be called by multiple threads (alluded to earlier in the discussion of the standard library printf routine). The preferred mechanism for incorporating these controls into your program is OS/2 semaphores. Thread Control A detailed discussion of the use of OS/2 semaphores can be found in "Using OS/2 Semaphores to Coordinate Concurrent Threads of Execution," MSJ (Vol. 3, No. 3), but we will briefly reiterate some of the points made in that article that are pertinent here. Although OS/2 offers several facilities for interprocess communication (pipes, queues, signals, and shared memory), semaphores are the preferred method of coordinating multiple threads. You can use them to serialize access to pieces of code or resources that cannot be shared. Alternatively, you can use semaphores when you need to have one thread signal to another that an event has occurred. Of the several types of semaphores OS/2 offers, RAM semaphores (used by threads in the same process) are the simplest to implement and are the easiest to deal with in terms of our first multithreaded program. The MS-DOS operating system is a single-tasking environment: only one thread operates at a time. A program executing under DOS has considerable control over a resource, including the ability to disable interrupts and ensure uninterrupted access to it. Signaling between processes in the DOS environment is easy, since a global variable or flag can be used to coordinate different pieces of code. You can have one process wait while the flag is set, that is, until another process clears the flag. Once the flag has been cleared, the process continues, setting the flag for itself, safe in the knowledge that it alone has access to a particular resource, including the flag variable used for signaling. Under OS/2, this type of signaling is not possible, since there is no way (without controls) to guarantee the order in which threads will execute. Further, one thread may be reading a flag while another may be setting or clearing it, or a second thread might end up setting the flag between the moment that the first thread stopped waiting and began to set the flag itself. Therefore, more than one thread might end up with access to the same resources. The outcome of such a situation is predictably disastrous. Furthermore, continually checking the value of such a flag uses the CPU unnecessarily, lowering the efficiency of the system. Semaphores provide an elegant alternative solution to these problems in the context of the OS/2 multitasking environment. In one uninterruptible step a semaphore kernel call can test and set a semaphore. Thus, the semaphore controls access to a shared resource and allows one task to signal another that an event has occurred. To demonstrate a simple use of semaphores, suppose we added a facility to the keyboard thread program shown in Figure 8. This facility causes the keyboard thread to increment a counter every time the user presses the Esc key (instead of terminating the program). The main thread looks at the counter periodically, and as soon as the counter is greater than a certain value (say, 3), the main thread will terminate the program. The problem in OS/2's multithreaded environment concerns the serialization of a resource, specifically the counter variable that more than one thread may be sharing. Here's where the semaphore comes in: by using a semaphore, we can serialize the access to the counter variable, so that only one thread at a time actually reads or writes it. The revised listing, shown in Figure 13 (it uses the same MAKE file mentioned earlier and shown in Figure 10) illustrates the solution. It creates a semaphore variable, CountSem, which the main thread clears with the call to DosSemClear. The while loop has been expanded to handle the reading of the counter variable. The main thread sleeps for 100 milliseconds (about three 32-millisecond time slices), then calls DosSemRequest to gain access to CountSem. The -1L parameter causes the function to block the calling thread until the semaphore has been cleared--that way it will not be able to access the counter variable if the keyboard thread has already gained control. Next, the main thread evaluates the counter thread, breaks out of the loop, and terminates the program if the counter is greater than 3. Otherwise, it clears the semaphore (giving up ownership of the resource, the counter variable) and returns to the top of the loop. Note that the call to DosSleep suspends the current thread, allowing OS/2 to give CPU time to threads of the same or greater priority. Without the call to DosSleep, the thread would attempt to run in a continual loop, unnecessarily burning CPU time. Note, too, that the keyboard thread does not require DosSleep: the IO_WAIT parameter to KbdCharIn blocks that thread until there is keyboard input available. The keyboard thread code also uses the CountSem semaphore to gain access to the counter variable. Every time the user presses the Esc key, the keyboard thread requests access to the semaphore, blocking until the semaphore has been cleared. Then it increments the counter variable and clears the semaphore. Again, this mechanism prevents it from accessing the counter variable at the same time as the main thread. Thus, access to the counter variable has been serialized and the activity of the two threads has been synchronized. From this simple example, we can now move to something a little more complex: a multithreaded version of HELLO.C. Figure 10: # # make file for key.c example found in Figure 8 # INCLUDE=\os2\include\mt LIB=\os2\lib COPT=/Lp /W3 /Zp /Zie /Zl /G2s /I$(INCLUDE) /Alfw key.exe: key.c key cl $(COPT) key.c /link /co llibcmt A Multithreaded HELLO.C The multithreaded version of HELLO.C is designed to help illustrate the use of threads and semaphores for serializing access to and controlling resources. Whereas the original HELLO.C simply printed a message on the screen and terminated, HELLO0.C (shown in the listing in Figure 14), logically subdivides the screen into frames and prints the message in each frame. The program assigns each frame a thread that is responsible for writing and clearing the message. The program's threads continue to write and clear their messages until the user presses the Esc key. This frame-based format will be the basis for other example programs as we explore the OS/2 subsystems and other facilities in later articles in this series. Each frame's thread receives a pointer to the frame's data structure, which contains the information the thread will use during the course of the program. This structure includes the frame's row/column coordinates, the thread's ID (returned from _beginthread), a semaphore that the main program thread will use to activate the thread, and the thread's stack. Exactly how many frames will appear on the screen depends on the screen mode when you run the program (25, 43, or 50 lines). HELLO0.C can take one optional command-line argument: the number of milliseconds that the main thread should sleep between each activation of a frame thread. This argument allows you to slow down the visual activity deliberately so that you can see what's happening a little more clearly. The command-line parameter, stored in a variable called sleeptime, defaults to 1 millisecond, which the DosSleep kernel function will round up to 32 milliseconds--the minimum OS/2 time slice. It also prevents HELLO0.C from hogging too much CPU time. You might try running the program with a parameter of 50, 100, 500, or 1000 (the equivalent of 1 second) to get a better idea of what's happening. The program begins by checking the command-line parameter and then calls _beginthread to spin off a keyboard thread, which blocks until keyboard input is received and terminates the program when the user presses the Esc key. The code for this thread is lifted directly from the earlier keyboard thread example, shown in Figure 8. Next the main thread goes through several housekeeping and preparatory steps: it obtains the video mode through a call to VioGetMode, sets up for the number of lines on the screen, and calculates the maximum number of frames to display. Then it allocates and initializes the frame structures (FRAME data types) and randomly chooses a FRAME for each screen frame, assigning the appropriate row/column coordinates. Consequently, the frames will seemingly appear and disappear in a random order, although the order remains static throughout the program. The real fun in HELLO0.C begins when the main thread calls DosSemSet for each FRAME, followed by a call to _beginthread to start the FRAME's thread (which will block until the semaphore clears). Finally, the main thread enters a loop where it stays for the remainder of the program. In this loop, it activates the thread for each FRAME by clearing the FRAME semaphore. The call to DosSleep suspends the thread, forcing it to give up some CPU time before activating the next thread. The main thread will do this for every FRAME and then repeat the process. Adding calls to DosSleep when a thread is in a loop will make the program more efficient, since it eliminates a thread's ability to waste CPU time. What does each FRAME thread do? The code for each FRAME thread is contained in the hello_thread function, which takes one parameter: the address of the thread's FRAME, which is passed to it by means of _beginthread. The FRAME thread code is structured around a loop, at the top of which is a call to the DosSemRequest kernel function. By passing the address of a semaphore and a -1 to this function, the calling thread blocks until the semaphore clears. Thus, each thread is idle until the main thread clears its semaphore. Once activated, a FRAME thread will either clear or write its message, depending on a variable that is toggled every time the thread is active. Note that the VIO subsystem function, VioWrtCharStr, performs all video output and writes a specific number of characters from a string at a specific row/column coordinate on screen. An example of a FRAME thread's output is shown in Figure 15. As mentioned earlier, the program will continue until the user presses the Esc key. At that time, the keyboard thread will wake up (it's been blocked in the absence of keyboard input) and terminate the program. HELLO0.C is probably multithreaded overkill (how many times will you need to run as many as 25 threads in an application?), but it should get you off to a strong start and clarify the multithread issues addressed in the article. With this foundation, you'll be able to write some rather complex programs that use multiple threads. Indeed, program development will become even more interesting in the next issue, when we explore the VIO subsystem. Figure 11: Using DOSEXITLIST OS/2 lets a process establish a set of routines that will always be called when the process terminates.Typically these routines are functions that free the process's resources (such as closing open files). Regardless of how and when the process terminates, OS/2 will execute the functions upon termination. An application can "register" functions that OS/2 will execute, thus ensuring an orderly shutdown and disposal of the process's resources in spite of the unexpected termination of the process. The kernel function, DosExitList, registers the termination functions with OS/2, and terminates the functions themselves. The function prototype for DosExitList is: unsigned DosExitList(unsigned code,void far *fptr(unsigned)); When registering the functions with OS/2, the first parameter can be either EXLST_ADD or EXLST_REMOVE, which add or remove a function from the list, respectively. By allowing a process to dynamically add and remove functions from the termination list, a process can control the destiny of its resources after its death (in a way, not unlike a human will). A process can remove the functions from the list prior to normal termination if they are no longer needed. The second parameter is, obviously, a pointer to the termination function being registered. When OS/2 begins execution of the termination functions, the process and all of its threads have been destroyed, with the exception of the thread executing the DosExitList functions. OS/2 will transfer control to each function registered, but in no particular order. Once OS/2 has executed all the registered functions, the process ends. The termination functions registered with DosExitList must be defined in the process (that is, in its code segment) and should be as short and fail-safe as possible. A termination function may call any OS/2 system function with the exception of DosCreateThread and DosExecPgm. A skeleton definition of a termination function follows: void far termfunc(unsigned code) { if(code != TC_EXIT) { . /* do cleanup here */ . } DosExitList(EXLST_EXIT,0); } Note that a termination function has one parameter and no return value. The parameter will always be one of the following: TC_EXIT - normal exit TC_HARDERROR - hard-error abort TC_TRAP - trap operation TC_KILLPROCESS - unintercepted DosKillProcess This allows the termination function to detect whether a normal termination of the process has occurred. It can also determine what actions the function should take. Termination functions must terminate themselves by calling DosExitList(EXLST_EXIT,0). They cannot execute a 'return' (explicitly or implicity, by falling past the curly brace), or the process will hang and never terminate. The EXLST_EXIT code tells OS/2 that the termination processing is complete, and that it should call the next function on the termination list. Figure 12: abs atoi atol bsearch chdir getpid halloc hfree itoa labs lfind lsearch memccpy memchr memcmp memcpy memicmp memmove memset mkdir movedata putch rmdir segread strcat strchr strcmp strcmpi strcpy stricmp strlen strlwr strncat strncmp strnicmp strncpy strnset strrchr strrev strset strstr strupr swab tolower toupper Figure 13: #define INCL_DOS #define INCL_SUB #include<stdio.h> #include<process.h> #include<os2.h> #define ESC 0x1b #define TRUE 1 void keyboard_thread(void); void main(void); #define THREADSTACK 512 char keythreadstack[THREADSTACK]; long CountSem = 0L; unsigned count = 0; void main(void) { TID threadid; DosSemClear(&CountSem); if(DosCreateThread(keyboard_thread,&threadid, &keythreadstack[THREADSTACK-1])) exit(-1); while(TRUE) /* insert code for main program here */ { DosSleep(100L); DosSemRequest(&CountSem,-1L); if(count > 3) break; DosSemClear(&CountSem); } } void keyboard_thread(void) /* keyboard thread code */ { KBDKEYINFO keyinfo; while(TRUE) { KbdCharIn(&keyinfo,IO_WAIT,0); /* wait for keystroke */ if(keyinfo.chChar == ESC) /* if ESC pressed, break */ { DosSemRequest(&CountSem,-1L); count++; DosSemClear(&CountSem); } } DosExit(EXIT_PROCESS,0); /* terminate the process */ } Figure 14: /* os2hello.c by RHS, 10-14-88 * * OS/2 and 1988 version of K&R's hello.c * demonstrates multiple threads */ /* This program provides an introduction to the use of threads and semaphores under OS/2. It divides the screen up into a series of logical frames. Each frame is a portion of the screen that is managed (written to) by a single thread. The exact number of frames will depend on the current screen length (25, 43 and 50 lines). Each thread has its own data from which it knows where the frame can be found on screen. This includes a semaphore which signals the thread when to proceed. These elements can be found in the FRAME data type. Upon receiving a signal from its semaphore (i.e., the semaphore has been cleared), the thread either draws a message on the frame or clears the frame, and reverses the flag that determines this. Then it again blocks until its semaphore has been cleared again. The main program thread starts by setting up the frame information: checking the screen size, determining the number and size of the frames. It also "randomly" selects the order in which the frames will appear. Then it sets each thread's semaphore and initiates each thread (remember the threads will block until their semaphores are cleared. Finally, the main program goes into an infinite loop, clearing each thread's semaphore, sleeping for at least 1 millisecond, and then continuing to the next thread. Thus the threads asynchronously call the VIO subsystem to draw or clear each frame, while the main program thread continues. An optional parameter can be passed to set the number of milliseconds passed to DosSleep, allowing the operator to more accurately "see" the order in which the frames appear/erase. This value must always be at least 1 to allow the main program thread to give time to the CPU scheduler. A call to _beginthread() early in main() sets up a thread to monitor keyboard input. This thread blocks until a key is pressed, then examines the key, and if the key is the Escape Key (27 decimal or 1bH), the thread calls DosExit to kill the whole process. */ #define INCL_SUB #define INCL_DOSPROCESS #include <os2.h> #include <mt\stdio.h> #include <mt\string.h> #include <mt\assert.h> #include <mt\stdlib.h> #include <mt\process.h> #if !defined(TRUE) #define TRUE 1 #endif #if !defined(FALSE) #define FALSE 0 #endif #define LINES25 4 /* height in lines of frames*/ #define LINES43 6 #define LINES50 7 #define MAXFRAMES 28 /* limited to max frames possible */ #define RAND() (rand() % maxframes); #define THREADSTACK 400 /* size of stack each thread*/ #define IDCOL 15 #define ESC 0x1b char *blank_str = " "; /* string for blanking frame */ /* frame data */ char *hello_str25[LINES25+1] = { " ", " Hello, world! ", " from Thread # ", " ", "\0" }; char *hello_str43[LINES43+1] = { " ", " ", " Hello, world! ", " from Thread # ", " ", " ", "\0" }; char *hello_str50[LINES50+1] = { " ", " ", " Hello, world! ", " ", " from Thread # ", " ", " ", "\0" }; char **helloptr; int numlines; typedef struct _frame /* frame structure */ { unsigned frame_cleared; unsigned row; unsigned col; unsigned threadid; long startsem; char threadstack[THREADSTACK]; } FRAME; FRAME far *frames[MAXFRAMES]; /* pointers to frames */ unsigned maxframes; ULONG sleeptime = 1L; /* minim sleep time */ char keythreadstack[THREADSTACK]; /* function prototypes */ void hello_thread(FRAME far *frameptr); void keyboard_thread(void); void main(int argc, char **argv); void main(int argc, char **argv) { int row, col, maxrows, maxcols, len, i, loops = 0; VIOMODEINFO viomodeinfo; if(argc > 1) sleeptime = atol(argv[1]); if(sleeptime < 1L) sleeptime = 1L; /* start keyboard thread */ if(_beginthread(keyboard_thread,keythreadstack, THREADSTACK,NULL) == -1) exit(-1); viomodeinfo.cb = sizeof(viomodeinfo); VioGetMode(&viomodeinfo,0); /* get video info */ maxrows = viomodeinfo.row; maxcols = viomodeinfo.col; switch(maxrows) { case 25: helloptr = hello_str25; numlines = LINES25; break; case 43: helloptr = hello_str43; numlines = LINES43; break; case 50: helloptr = hello_str50; numlines = LINES50; break; default: /* fail if not 25,43,50 lines */ assert(0); exit(-1); } len = strlen(*helloptr); maxframes = (maxrows / numlines) * (maxcols / len); assert(maxframes <= MAXFRAMES); for( i = 0; i < maxframes; i++) /* initialize structures */ { if(!(frames[i] = malloc(sizeof(FRAME)))) exit(0); frames[i]->frame_cleared = FALSE; frames[i]->startsem = 0L; memset(frames[i]->threadstack,0xff, sizeof(frames[i]->threadstack)); } i = RAND(); /* get first random frame */ /* set up random appearance */ for(row = col = 0; loops < maxframes ; ) /* set row/col each frame */ { if(!frames[i]->frame_cleared) { frames[i]->frame_cleared = TRUE; /* set for empty frame */ frames[i]->row = row; /* frame upper row */ frames[i]->col = col; /* frame left column */ col += len; /* next column on this row */ if(col >= maxcols) /* go to next row? */ { col = 0; /* reset for start column */ row += numlines; /* set for next row */ } i = RAND(); /* get next random frame */ } else ++i; if(i >= maxframes) { i -= maxframes; loops++; /* keep track of # of frames*/ } } for( i = 0 ; i < maxframes; i++) /* start a thread for each */ { DosSemSet(&frames[i]->startsem); /* initially set each sem. */ /* start each thread */ if((frames[i]->threadid = _beginthread( (void far *)hello_thread, (void far *)frames[i]->threadstack, THREADSTACK, (void far *)frames[i])) == -1) { maxframes = i; /* reset maxframes on failure */ break; } } while(TRUE) /* main loop */ { /* swing thru frames, signalling to threads */ for( i = 0; i < maxframes; i++) { DosSemClear(&frames[i]->startsem); /* clear: thread can go */ DosSleep(sleeptime); /* sleep a little */ } } } void hello_thread(FRAME far *frameptr) /* frame thread function */ { register char **p; register int line; int len = strlen(*helloptr); unsigned row, col = frameptr->col; char idstr[20]; while(TRUE) { DosSemRequest(&frameptr->startsem,-1L); /* block until cleared */ itoa(frameptr->threadid,idstr,10); /* init idstr */ row = frameptr->row; /* reset row */ if(!frameptr->frame_cleared) /* if frame in use, erase */ for( line = 0; line < numlines; line++, row++) VioWrtCharStr(blank_str,len,row,col,0); else /* else frame not in use */ { p = helloptr; /* print message */ for( line = 0; **p; line++, row++, p++) VioWrtCharStr(*p,len,row,col,0); /* write id # in frame */ VioWrtCharStr(idstr,3, row-(numlines/2), IDCOL+col,0); } /* toggle use flag */ frameptr->frame_cleared = !frameptr->frame_cleared; } } void keyboard_thread(void) { KBDKEYINFO keyinfo; while(TRUE) { KbdCharIn(&keyinfo,IO_WAIT,0); /* wait for keystroke */ if(keyinfo.chChar == ESC) /* break if ESC pressed */ break; } DosExit(EXIT_PROCESS,0); /* terminate process */ } /* end of hello0.c */ Using The Multithreaded Library: DosCreateThread vs. _beginthread The OS/2 API interface for creating and terminating threads works through the kernel functions DosCreateThread and DosExit. Unfortunately, although the code for a thread must be contained within a function, you cannot pass parameters to it by using DosCreateThread. So if you prefer more of a C-like interface to write multithreaded programs, or you're going to use the multithreaded standard library, LLIBCMT.LIB, you should be familiar with an alternative interface via _beginthread and _endthread. If you plan on calling standard library functions from within your thread function, it's imperative that you use LLIBCMT.LIB. The Case of printf The discussion of printf in the main text illustrates the need for this interface. A function like printf uses a large internal buffer for formatting its output string. Although this buffer is adequate if a single thread is executing printf's code, the outcome is unpredictable if more than one thread is trying to execute printf at the same time. There are two ways that printf can be written to resolve this: you can use a semaphore to permit only one thread at a time to execute the code for printf, or you can use a set of semaphores to allow a finite number of threads to access printf simultaneously. Let's take a closer look at these two solutions. With the first method (sketched in Figure A), a semaphore is set at the beginning of printf and cleared at the end. This approach serializes the code for printf so that only one thread can execute it at a time. Unfortunately, that means that any time a thread calls printf, it will block if some other thread is executing the printf code and will remain suspended until the thread using printf clears the semaphore. Additionally, there is no guarantee that the next thread will be allowed to execute printf, nor can the scheduler ensure which thread that will be. OS/2 cannot guarantee that the next thread you want to call printf will be the one allowed to execute it with this approach. Thus, a scenario might develop where a thread of lesser priority might constantly be preempted by higher priority threads executing printf--and that can cause undesirable visual results. Alternatively, the second method limits the number of threads that can simultaneously execute the code for a function like printf. However, it offers no possibility of collision between threads competing for access to printf's code. With this approach (illustrated in Figure B), printf is structured to provide a fixed set of formatting buffers, with access to each controlled by a different semaphore. Consequently, every thread is given its own private buffer while executing the code. The catch is the limit on the number of buffers and therefore the limited number of threads that can gain access. This limit is one reason _beginthread is provided. The function offers more of a C-like approach to creating a new thread by allowing you to pass parameters to the thread and returning the thread ID when successful. But more specifically, it restricts the calling process to 32 threads, far less than the 255 threads that can be created by DosCreateThread. For many applications, however, 32 threads will be more than enough, allowing the multithreaded library, LLIBCMT.LIB, to operate on the assumption that no process will consist of more than 32 threads at a time. For this reason, _beginthread is available only when you use this library, and you should use it instead of DosCreateThread when writing a program that uses this library. Figure A: void printf(char *fmt,...) { static long printfSem = 0L; static char formatbuffer[BUFSIZ]; DosSemRequest(&printfSem,-1L); ■ ■ ■ DosSemClear(&printfSem); } Figure B: #define MAXTHREADS 32 void printf(char *fmt,...) { static long printfSems[MAXTHREADS] = {0L,0L,0L,0L,0L,0L,0L,0L,0L,0L, 0L,0L,0L,0L,0L,0L,0L,0L,0L,0L, 0L,0L,0L,0L,0L,0L,0L,0L,0L,0L, 0L,0L}; char formatbuffers[MAXTHREADS][BUFSIZ]; int semno; for(semno = 0; semno < MAXTHREADS; semno++) if(!DosSemRequest(&printfSems[semno],0L)) break; assert(semno < MAXTHREADS); ■ ■ ■ DosSemClear(&printfSem[semno]); } Using _beginthread How does _beginthread work? From the function prototype shown in Figure C, you can see that, like DosCreateThread, it requires the address of a function that contains the code for the thread. Unlike DosCreateThread, however, _beginthread takes not the address of the top of the stack, but the address of a stack area as you would declare it in your C program (that is, the bottom of the stack). Therefore you must provide it with the stack size (the third parameter). Last of all, _beginthread takes a parameter that makes it more valuable than DosCreateThread in some cases: an argument parameter for passing arguments to the thread function itself. An example of this use of _beginthread can be found in the multithreaded HELLO0.C program. Finally, you'll notice that _beginthread itself returns -1 on error or the thread ID of the new thread. Obviously, _beginthread must at some point call DosCreateThread. In fact, it even calls DosCreateThread when you have exceeded the number of threads allowed by the multithreaded library. The only way it can limit the number of threads to a process is to get the thread ID returned from its own call to DosCreateThread and return -1 if the ID is greater than 32. However, this reveals the use of two undocumented assumptions about OS/2: that DosCreateThread will always return the lowest available thread ID and that OS/2 will reuse thread IDs of previously terminated threads. Figure C: #include<mt\process.h> #include<mt\stddef.h> int cdecl far _beginthread( void (cdecl far *start_address) (void far *), void far *stack_end, unsigned stack_size, void far *arglist); ■ ■ ■ void far cdecl _endthread(void) Linking LLIBCMT.LIB To use _beginthread and its counterpart to DosExit, _endthread (also shown in Figure C), make sure that the LLIBCMT.LIB and DOSCALLS.LIB libraries are available in the current directory or in the directory pointed to by the LIB variable (in the environment or in your MAKE file--see the MAKE file for HELLO0.C as an example). The DOSCALLS.LIB library is required, since _beginthread, _endthread, and some of the other library functions will make calls to OS/2 API functions. LLIBCMT.LIB should be used in place of any other run-time libraries. In addition, you may want to change your INCLUDE variable (again in the environment or in your MAKE file) to point to the MT directory that the compiler installed beneath the standard INCLUDE directory. This directory contains copies of the standard header files and should be used for creating multithreaded programs. You can set the INCLUDE variable on the compiler command line with the /I option as an alternative. Incidentally, the prototypes for _beginthread and _endthread can be found in PROCESS.H. Limitations Multithreaded code must make several assumptions as it executes. First, all code and data addresses are expected to be far. In addition, the code must assume that the data segment is fixed but should not assume that the stack and data segments are the same. In addition, conventional run-time stack checking must be turned off, since it is taken care of for each thread in a multithreaded program. You can conveniently use compiler switches to take care of these concerns by employing the /Alfw and /G2s options. Also, either the /Zl compiler option or the /NOD linker option should be used to prevent the default library search by the linker. The MAKE file for HELLO0.C illustrates this usage and can easily be adapted to compile and link your own multithreaded programs. Finally, remember that multithreaded programs cannot be bound into dual-mode applications since there is no facility in MS-DOS for simultaneous execution of multiple threads of code. ────────────────────────────────────────────────────────────────────────────

Volume 4 - Number 3

──────────────────────────────────────────────────────────────────────────── A Technical Study of Dynamic Data Exchange Under Presentation Manager Susan Franklin and Tony Peters The IBM Corporation and Microsoft recently shipped an updated version of the OS/2 operating system, which contains some important enhancements over the initial release of the OS/2 systems. The major enhancement to the OS/2 Version 1.1 release is the inclusion of the Presentation Manager (referred to herein as PM) as a standard component. The OS/2 Presentation Manager is based on Microsoft(R) Windows and provides the same benefits Windows provided to DOS: a windowed, graphical user interface and support for a variety of input and output devices. An important component of Microsoft Windows that has been implemented in OS/2 PM is the Dynamic Data Exchange (DDE) protocol. The Windows DDE version was described in "Inter-Program Communication Using Windows' Dynamic Data Exchange," MSJ (Vol. 3, No. 6). DDE is a published message protocol for the exchange of data between participating programs and has gained wide acceptance among Windows applications as the standard messaging protocol for data exchange. The evolution of DDE from DOS to the OS/2 environment has required some enhancements to address limitations associated with the original Windows DDE specification. This article describes that evolution and provides a graphical data exchange program as an example. Protocol Modifications In the Windows environment, DDE provided a consistent, flexible method for communication between applications. When moving DDE to the multitasking, protected memory environment of the OS/2 PM, however, certain changes to the protocol were needed. These changes had to address the new concepts introduced by the OS/2 environment without significantly changing the DDE model, which has proved successful in the Windows environment. An early approach to the migration of the DDE protocol to OS/2 and Presentation Manager was a simple remapping of the message parameters. The primary change necessary was the parameter used when actually passing the data to another application, which requires crossing OS/2 process boundaries. Whereas a handle to the data is sufficient in the Windows environment, a memory selector is required to pass data between separate OS/2 processes. String data could still be passed in the global atom table, although applications were necessary to explicitly request access to the atom table. This approach solved the problems introduced by protected memory into the DDE message set. But several other problems and limitations still existed. They could be solved by further modification of the protocol. DDE suffered from the two-parameter limit inherent in PM messages. Following the Windows DDE model, the first parameter in any DDE message is the handle of the sending window. This left just one 32-bit parameter to pass all other conversation parameters and references to data. Although the necessary parameters and selectors could fit into the remaining long parameter, there was no room for any future expansion to the protocol. Communicating with other machines on a local area network (LAN) or with other types of computers proved difficult given the parameter limits. This limitation was also a problem if and when the operating system's addressable space increased. Even in the current environment, any expansion to the DDE model or conversation parameters would not be possible given the lack of parameter space. Clearly, the protocol had to be modified such that it permitted expandability and application freedom to include additional parameters as necessary. In order to expand the parameter space for DDE messages, the PM version of DDE uses the second DDE message parameter as a 32-bit pointer to one of two available DDE data structures. These structures contain all necessary DDE conversation parameters as well as the actual data when necessary. As the requirements for DDE change, this structure can be expanded without changing the parameters of the DDE messages. The long address of the structure permits compatibility to future system software and other machines. By packaging all parameters into one structure, the message parameters become more consistent since the variant parameters are contained in the structure itself. All parameters are packaged into a single structure, so the use of the atom table is no longer necessary. Adding string parameters to the atom table just introduces a second data access method that complicates the protocol. Instead string data is included in the DDE structures. Since the majority of DDE messages will be sent or posted to windows in a separate process, the memory containing the DDE structure had to be made accessible to the receiving window. Rather than require applications using DDE to grant this access, the PM DDE protocol provides new Application Program Interfaces (APIs) for sending and posting DDE messages. Applications do not use WinPostMsg or WinSendMsg to transmit DDE messages. Instead, the message and parameters are passed to a system API, which grants the receiving window access to the DDE structure and sends or posts the message on behalf of the calling application. These APIs ensure that the access to the structure is granted consistently and correctly, while simplifying the programming efforts of an application implementing DDE. These enhancements to DDE for OS/2 have provided a standard framework for applications to communicate without having to design a new protocol that would be suitable for DDE within a multitasking operating system. Additionally, using OS/2 DDE provides a concise method of implementing ever-increasingly complex graphical data exchange in an efficient manner. Single Client/Server In the simplest case, DDE is used when one application, called the client application, requires data from another independent application, called the server application. The classic example for this model is a charting program receiving updates of data from a spreadsheet and reflecting those changes by redrawing the chart. In that case, the charting program is the client and the spreadsheet is the server. We will begin by following the logic for the simplest of cases, the single client/single server model. In this example, the purpose of the DDE conversation is the exchange of graphical data between programs. The server application is any application wishing to display a picture in the client application's window. The graphical data is packaged and sent to the client application each time the server application wishes to change the appearance of its picture. Figure 1 shows the general logic flow for the single client/single server type of application. The client application initiates the conversation. Once the server acknowledges the initiate, the client requests the data and the server sends it inside the appropriate structure. A single client/single server DDE conversation begins when the client application broadcasts a WM_DDE_INITIATE message to all other top-level windows in the system. The client specifies the string name of the application expected to reply, as well as a string name identifying the topic of the proposed conversation. Either of these string names may be NULL to indicate that the desired server application or the topic name is not specific and any application implementing DDE may participate. The WM_DDE_INITIATE message is not broadcast directly by the application. Instead, the client application uses the WinDdeInitiate call to send the message. Figure 2 illustrates the procedure for initiation. When a server application gets the WM_DDE_INITIATE message, it checks the application and topic names to determine whether it will participate in the conversation. Sample code for initiate processing appears in Figure 3. Note that the application and string pointers that were input to the WinDdeInitiate call do not surface as explicit message parameters in the WM_DDE_INITIATE message. Instead, they are included in the DDEINIT structure, which is referenced by the second parameter. In all DDE messages, the first parameter contains the window handle of the window that originated the DDE message. If the server decides to participate in the conversation, then the WinDdeRespond call is used in order to send the WM_DDE_INITIATEACK message back to the client application. Again, the strings referenced in the parameters of this call will be copied to the DDEINIT structure, and the pointer to the structure will be passed as the second parameter in WM_DDE_INITIATEACK. Once the conversation link has been established, the actual data transfer may take place. This exchange may be a one-time data transfer, an ongoing data transfer, or a transfer of remote commands. One-time data transfer is accomplished by using the WM_DDE_REQUEST message when data is flowing from server to client and the WM_DDE_POKE message when data is flowing from client to server. In either case, a DDESTRUCT is allocated and filled by the client application. The structure contains status flags describing the format of the data that is being requested (or poked) as well as the status and the size of the data. Once the structure is filled, the WM_DDE_REQUEST or WM_DDE_POKE message is posted to the server application using the WinDdePostMsg API. This is a call that is used for all messages except for WM_DDE_INITIATE and WM_DDE_INITIATEACK. The call ensures that the receiving window handle gets access to the memory allocated for the DDESTRUCT. When the data is being poked, the WM_DDE_POKE message is the only one required to complete the transfer, since the transfer is unsolicited. When the data is requested, the server application responds with the WM_DDE_DATA message. If the server cannot supply the data in the requested format, it responds with a negative WM_DDE_ACK message. Figure 4 illustrates the code required in the server application to handle both one-time data transfer methods. Perhaps the most common type of data transfer in DDE is the establishment of a permanent data link between applications. In that case, the client application posts a WM_DDE_ADVISE. The DDESTRUCT is filled in the same manner as it is for the WM_DDE_REQUEST message. This message informs the server application that the client would like to receive WM_DDE_DATA messages as the data change. As in the case of the WM_DDE_REQUEST message, the server responds with a negative WM_DDE_ACK message if the data cannot be supplied in the requested format. If the data can be supplied, the client will continue to receive WM_DDE_DATA messages until either the conversation terminates or the permanent link is terminated. Figure 5 shows the server handling a request for a permanent data link. When the client terminates the permanent link, it posts the WM_DDE_UNADVISE message. This message does not end the DDE conversation; rather, WM_DDE_DATA messages will no longer be posted when data changes. Another requirement during a DDE conversation is the execution of commands by a server application on behalf of the client. In this particular case, DDESTRUCT contains a string of commands to be executed. A positive or a negative WM_DDE_ACK message, depending on the outcome of the execution, is posted to the client. A code fragment for remote execution of commands is illustrated in Figure 6. Termination of a DDE conversation may be instigated by either the client or the server application. Typically, the WM_DDE_TERMINATE message is posted when the user has requested to close the application, although this isn't always the case. Whatever the reason for the termination, posting of the WM_DDE_TERMINATE indicates that no further DDE messages will be sent. The application that is posting the WM_DDE_TERMINATE may not shut down until the other application has responded with another WM_DDE_TERMINATE. There are no parameters used with the terminate message. Figure 2 case WM_CREATE: /* broadcast the initiate message */ WinDdeInitiate(hwnd, "App_Name", "Graphics_Exchange"); break; Figure 3: case WM_DDE_INITIATE: /* respond if app and topic strings match */ pDDEInit = (PDDEINIT)lParam2; if ((HWND)lParam1 != hwnd) { if((!strcmp("App_Name", pDDEInit->pszAppName)) && (!strcmp("Graphics_Exchange", pDDEInit->pszTopic))) { WinDdeRespond(lParam1, hwnd, "Client", "Graphics_Exchange"); } } DosFreeSeg(PDDEITOSEL(pDDEInit)); break; Figure 4: case WM_DDE_REQUEST: /* allocate DDESTRUCT and send data */ pDDEStruct = (PDDESTRUCT)lParam2; strcpy(szTemp, "Text_Data"); if((pDDEStruct->usFormat == DDEFMT_TEXT) && (!strcmp(szTemp, DDES_PSZITEMNAME(pDDEStruct)))) { nNumBytes = (strlen(szTemp) + 2 + strlen(szData)); pDDEStruct = st_DDE_Alloc((sizeof(DDESTRUCT) + nNumBytes), "DDEFMT_TEXT"); pDDEStruct->cbData = strlen(szData) + 1; pDDEStruct->offszItemName = (USHORT)sizeof(DDESTRUCT); pDDEStruct->offabData = (USHORT)((sizeof(DDESTRUCT) + strlen(szTemp)) + 1); memcpy(DDES_PSZITEMNAME(pDDEStruct), szTemp, (strlen(szTemp) + 1)); memcpy(DDES_PABDATA(pDDEStruct), szData, (strlen(szData) + 1)); pDDEStruct->fsStatus |= DDE_FRESPONSE; WinDdePostMsg((HWND)lParam1, hwnd, WM_DDE_DATA, pDDEStruct, TRUE); DosFreeSeg(PDDESTOSEL(lParam2)); } else { /* send negative ACK using their DDESTRUCT */ pDDEStruct->fsStatus &= (~DDE_FACK); WinDdePostMsg((HWND)lParam1, hwnd, WM_DDE_ACK, pDDEStruct, TRUE); } break; case WM_DDE_POKE: /* unsolicited data from client */ pDDEStruct = (PDDESTRUCT)lParam2; strcpy(szTemp, "Text_Data"); if((pDDEStruct->usFormat == DDEFMT_TEXT) && (!strcmp(szTemp, DDES_PSZITEMNAME(pDDEStruct)))) { strcpy(szData, DDES_PABDATA(pDDEStruct)); DosFreeSeg(PDDESTOSEL(lParam2)); } else { /* send negative ACK using their DDESTRUCT */ pDDEStruct->fsStatus &= (~DDE_FACK); WinDdePostMsg((HWND)lParam1, hwnd, WM_DDE_ACK, pDDEStruct, TRUE); } break; Figure 5: case WM_DDE_ADVISE: /* set ADVISE bit in window data and ACK */ pDDEStruct = (PDDESTRUCT)lParam2; if(pDDEStruct->usFormat == DDEFMT_TEXT) { pWWVar->bAdvise = TRUE; if((pDDEStruct->fsStatus & DDE_FACKREQ) == DDE_FACKREQ) { pDDEStruct->fsStatus |= DDE_FACK; WinDdePostMsg((HWND)lParam1, hwnd, WM_DDE_ACK, pDDEStruct, TRUE); } else { DosFreeSeg(PDDESTOSEL(lParam2)); } else { /* Send a negative ACK using their DDESTRUCT */ pDDEStruct->fsStatus &= (~DDE_FACK); WinDdePostMsg((HWND)lParam1, hwnd, WM_DDE_ACK, pDDEStruct, TRUE); } break; Figure 6: case WM_DDE_EXECUTE: /* execute a command */ pDDEStruct = (PDDESTRUCT)lParam2; strcpy(szCommand, DDES_PABDATA(pDDEStruct); if(!Dde_Cmd_Processor(szCommand)) { /* parse and execute the command */ pDDEStruct->fsStatus &= (~DDE_FACK); WinDdePostMsg((HWND)lParam1, hwnd, WM_DDE_ACK, pDDEStruct, TRUE); } else { DosFreeSeg(PDDESTOSEL(lParam2)); } break; User-Defined Formats It is possible for an application to create its own DDE data format if an appropriate, system-provided data format is not available. The system provides one predefined format for interchanging text strings, DDEFMT_TEXT. Before an application uses an application-defined data format, however, it must establish a convention for obtaining a unique ID and registering that format so other applications can associate the format ID in the DDESTRUCT with their specialized data format. Although appearing similar to clipboard data formats, DDE formats are not to be confused with clipboard formats. Clipboard formats identify handle types, whereas DDE formats identify the actual layout of the data in the DDESTRUCT block. We have chosen to register DDE formats using the system atom table. The prefix of DDE formats is DDEFMT_. Using the atom manager to register DDE formats guarantees unique IDs among all applications that use this method to register DDE formats. Figure 7 represents a function, Register_DDEFMT, which illustrates how to register a user-defined DDE data format with the system. Register_DDEFMT returns the DDE data format for either an existing or a newly created data format. The first time Register_DDEFMT is invoked for a particular DDE data format, that format is registered in the system atom table. Every call that is sent to Register_DDEFMT for a particular DDE format string identifier results in the format being retrieved and returned to the caller. Looking ahead, Figure 9 shows an example of a function call to Register_DDEFMT. The user DDE data format for the DDE graphics exchange sample program in this article is shown in Figure 8. The format consists of a graphics descriptor control block in the abData area. This graphics descriptor control block contains, among other information, an offset pointer to the graphics data that is passed in the same shared memory block following the GDE data in abData. Figure 7: USHORT Register_DDEFMT(pszFormat) PSZ pszFormat; { HATOMTBL hAtomtbl; USHORT retn; hAtomtbl = WinQuerySystemAtomTable(); if (retn = WinFindAtom(hAtomtbl,pszFormat)) return retn; else return (WinAddAtom(hAtomtbl,pszFormat)); } Using Shared Memory DDE uses shared memory for all communications. Two different types of memory objects are allocated for DDE transactions--the DDEINIT and the DDESTRUCT structures. The way these objects are allocated may differ, but both result in shared memory made available to the recipient. These shared memory objects need to be freed properly for OS/2 shared memory management to be effective. The data areas containing szAppName and szTopicName in the WinDdeInitiate and WinDdeRespond calls may be in private or shared memory. When the WinDdeInitiate and WinDdeRespond calls are executed, the system will copy those strings into DDEINIT and make DDEINIT available to the recipient. The data area that contains the DDESTRUCT structure must be allocated using the SEG_GIVEABLE flag in the DosAllocSeg and DosAllocHuge calls. The WinDdePostMsg API makes the DDESTRUCT memory object available to the message recipient and frees the object from the sender. Any pointers that are part of the abData field must point to shared memory. It is the application's responsibility to manage the allocation and sharing synchronization of these shared data areas. The application may use either base OS/2 memory management API calls or higher level memory subsetting common services to achieve this, as long as the memory is managed properly. The message recipient is responsible for freeing all memory objects after retrieving the information. DosFreeSeg is used to release the memory. DDEINIT and DDESTRUCT must both be released by the message recipient. Figures 9 and 10 show the allocation and deallocation of the shared memory objects necessary for processing of the WinDdePostMsg API. The first part of Figure 9 is a call to a local function, which allocates and initializes the DDE shared memory object. The accompanying function, DDE_Alloc(), does the actual shared memory allocation by making a call to DosAllocSeg() and clears the entire shared memory object. The DDE shared memory block is initialized with the user-defined data DDE format by setting usFormat field in DDESTRUCT. This is accomplished by calling the function Register_DDEFMT(), which we previously discussed. By allocating and initializing the DDE shared memory objects in this manner, we can readily obtain ready-to-use shared memory objects for each of our DDE transactions. Figure 10 shows how deallocation of the DDESTRUCT, in this case during the processing of a WM_DDE_DATA message, is accomplished by calling DosFreeSeg with the selector of the DDE memory object as its parameter. Figure 9: /* send a request for data */ /* allocate memory */ DDEstrptr = DDE_Alloc(sizeof(DDESTRUCT), IDS_GDE); WinDdePostMsg((HWND)lParam1, DDEtoHWND, (ULONG)WM_DDE_REQUEST, DDEstrptr, TRUE); . . . PDDESTRUCT DDE_Alloc(size, format) int size; char *format; /************************************************************ * 1. Allocate a block of size. bytes * of giveable, shared memory for DDE call. * 2. Fill in DDE data format by * calling Register_DDEFMT((PSZ)format);. ************************************************************/ { SEL ddepsel; USHORT dasret; PDDESTRUCT DDEstrptr; if ((dasret = DosAllocSeg(size, &ddepsel, SEG_GIVEABLE)) == 0) { DDEstrptr = (PDDESTRUCT)SELTOPDDES(ddepsel); memset(DDEstrptr, (BYTE)NULL, size); /* set allocated memory to nulls */ /* fill in DDE data format */ DDEstrptr->usFormat = Register_DDEFMT((PSZ)format); } else { /* error */ return((PDDESTRUCT)NULL); } } Figure 10: MRESULT APIENTRY MasterDDEWndProc(hwnd,message,lParam1,lParam2) ■ ■ ■ DDEstrptr = (PDDESTRUCT)lParam2; case WM_DDE_DATA: ■ ■ ■ DosFreeSeg(PDDESTOSEL(DDEstrptr)); DDEINIT and DDESTRUCT Presentation Manager processes the data and allocates the memory for the WinDdeInitiate and WinDdeRespond API calls as shown in Figure 11. The system takes the strings passed in the WinDdeInitiate call and copies the strings into multiple DDEINIT blocks that are broadcast to all applications running on the system. Participating DDE server applications process the WinDdeInitiate call by sending a WM_DDE_INITIATEACK message with the WinDdeRespond call to the client and freeing the DDEINIT memory object from the WinDdeInitiate call. Non-DDE applications don't respond to the WM_DDE_INITIATE message and the shared memory object is released by the system default winproc. Figure 11 also illustrates how the DDESTRUCT is allocated, passed, and released by the client and server applications. Figure 12 is a diagrammatic view of DDEINIT processing. Multiclient/Server Model Earlier we discussed the single client/single server DDE conversation model. It may be possible, and in a multitasking environment such as OS/2 PM it is likely, that 1) server applications may have to support multiple clients and 2) multiple clients can receive data simultaneously from multiple servers. Likewise, there may be little or no distinction between whether the client or server application is initiated first. In a multiclient/multiserver application relationship, multiple clients and server applications can be invoked in any conceivable order, with virtually any number of permutations. The real power of DDE to manage conversations efficiently becomes apparent in implementing multiclient/multiserver relationships. In managing an N-way conversation, the client, the server, or both applications may be involved in a one-to-many conversation. That is, a single server may be supplying data to multiple client applications, a client application may be receiving data from several servers, or both of these things may be happening. The DDE convention for managing one-to-many conversations is for the managing application to open a window--the conversational DDE window--for each conversation and to process the messages in a generic winproc for each conversation based on the conversational DDE window handle. There are several benefits associated with managing the conversations based on a window handle assigned to the conversation. First, the individual conversation window handles serve as an ID for the conversation, which is guaranteed by the operating system to be unique. Second, conversations can easily be maintained and manipulated by using PM API calls (for example, WinEnumerateWindow) without having to develop specific data structures, such as linked lists, to keep track of conversations. Third, it is possible to easily store and retrieve conversational specific data in window words created with each conversational DDE window. We have created one additional window in each DDE application to facilitate DDE processing by our applications. Each of our DDE applications creates a DDE anchor window, which imposes an artificial one-layer window hierarchy on the application window structure, isolating all the DDE windows under a single parent window. This lets us search through the DDE conversations directly using WinEnumerateWindow without having to search all application children windows for DDE conversations. The DDE conversation windows are normally traversed to locate information specific to a single conversation or during termination processing when all the links of a particular application are being terminated. Figure 13 illustrates the parent/child relationship of DDE windows in our applications. When either the server or the client can initiate the conversation, as can be done in a multiclient/multiserver application, a client/server relationship that is consistent with the single client/single server DDE conversation model should be maintained. An example of this situation is a newly invoked server application participating in an existing client/server conversation. When a server is invoked during execution of a client, the server must signal the client that it is willing to participate in a conversation. For our signal, we have chosen to send the WM_DDE_INITIATE message with a predefined application name. The client responds to the server's initiate message with an initiate message of its own, causing the server to respond with a WinDdeRespond message, as would be done under the single client/single server model. Graphics Exchange Program DDE extensions for the multiclient/multiserver model are implemented in the sample graphics exchange program. For sample purposes, the client application exists merely to display graphical pictures representing the running server applications. Server applications may differ greatly in the function provided, but they all use DDE to transfer their graphics data to the client. In our model, the client always establishes a permanent data link with the server to receive updates as the state of the server application warrants a change in the picture's appearance. We have chosen Graphics_Exchange as the topic name for this example. The server provided in the example is simulating a phone messaging service. As phone messages arrive, they are listed in the main window of the server application. Each time a message is added, the picture is updated so that it reflects the current number of phone messages, and the client is posted a WM_DDE_DATA message. In order to limit the complexity of the server code presented, phone messages are generated through the use of the timer. At regular intervals, phone messages are added or deleted from the list. This lets us focus our attention on the DDE implementation in the program rather than on the function behind the server application. In Figure 14 the server application has been invoked and is generating phone messages. When the client application is invoked, it broadcasts the WM_DDE_INITIATE message and the server responds. Note that the application string is zero length to indicate that the client will talk to any application that will respond to the topic of Graphics_Exchange. Figure 15 shows the screen after the conversation has been linked and the first picture has been transferred to the client application. Upon invocation of a second instance of the server application, the server must signal the client that it has begun execution. In our case, the server broadcasts the WM_DDE_INITIATE message using the same topic but specifying a target application of Client. This indicates that the initiate is a special case in which the server is signaling its invocation to any client applications that may want to subsequently initiate a conversation. Upon receiving this message, the client application broadcasts another WM_DDE_INITIATE message. If the application string name were zero length, however, the client would inadvertently establish a second link with the original server application. To prevent this occurrence, the client specifies an application name, which is simply the handle of the new server converted to a string name. The newly invoked server checks the application name and responds because the application name matches its handle. Figure 16 illustrates the message flow in establishing the second link. This signaling convention establishes several rules for initiation of a Graphics_Exchange conversation and the subsequent response. The client application must broadcast a WM_DDE_INITIATE upon invocation with the null application string. It must also be prepared to receive a WM_DDE_INITIATE message as a signal of a newly invoked server. If the application name Client is present in this message, then the client must again broadcast a WM_DDE_INITIATE, this time with an application string containing the handle of the new participant. Figure 17 contains the initiate processing for the Graphics_Exchange client application. The server must respond to a WM_DDE_INITIATE of topic Graphics_Exchange if and only if the application string name is zero length or if it contains the string representation of the server's window handle. Any other application string name should be ignored. If the server responds to the conversation, it creates a window to handle all future processing of the new link and responds to the client using this window handle. Note that a window word containing the conversation link count is incremented at this time. This counter will be used later by the server to determine when shutdown may occur. Figure 18 shows the response processing in the Graphics_Exchange server application. Upon receipt of the WM_DDE_INITIATEACK message, the client application creates a window to process the link. Just as the server did, the client increments a conversation link count stored in its window word. This will be used during TERMINATE processing. The WM_DDE_REQUEST and WM_DDE_ADVISE messages are posted to the server on behalf of the newly created conversation window. Figure 19 illustrates this processing. The primary activity of the server window is the packaging of the data and posting of the WM_DDE_DATA message. In the sample program, this activity always occurs during WM_DDE_REQUEST processing. That is, even when the main server application determines that data should be transferred as a result of an ongoing ADVISE, the result is the posting of a WM_DDE_REQUEST by the main application to all of the conversation windows on behalf of the applications being advised. The format of the data message is a user-defined data format that was registered as discussed previously. This data format is actually a data structure, GDEDATA, which is used as the input structure for a control that manipulates the graphics for the client application. Upon receiving a WM_DDE_REQUEST, the server allocates memory for the DDESTRUCT and the underlying data structure and fills in the necessary data fields required by the client and its control. The actual graphics may be deposited in either bitmap or GPI drawing orders format and included in the memory object. In the sample, they are always deposited as drawing orders. The whole data package is then transmitted via the WinDdePostMsg call. The complete processing of the WM_DDE_REQUEST message is shown in Figure 20. Figure 21 shows how the main server application generates WM_DDE_REQUEST messages on behalf of all advised clients when the data changes. This is done by enumerating all child windows of the anchor window handle and posting the message to those windows that are advising a client application. The advise status of a server application is stored in the window word of the conversation window procedure. Figure 22 illustrates the setting of this status field upon receiving either the WM_DDE_ADVISE or the WM_DDE_UNADVISE message. The data format for a Graphics_Exchange conversation is simply the input structure to a control, therefore the WM_DDE_DATA processing by the client is no more than an insertion or replacement of the data structure into the control. If the DDE_FRESPONSE bit is set, the client knows that the WM_DDE_DATA message is the result of the initial WM_DDE_REQUEST that was made after the conversation was linked. Since this is the first transmittal of data from the server, the picture must be inserted into the graphics control. If the DDE_FRESPONSE bit is not set, then the WM_DDE_DATA message is the result of an ongoing advise and the client must replace the picture in the control with the new data. The control messages themselves are quite simple. The first parameter identifies the ID of the picture in question, and the second points to the structure describing and containing the picture. The internal details of the control are not relevant; the control manages the appropriate sizing and placement of the graphics data. The complete WM_DDE_DATA processing is shown in Figure 23. During execution of the participating applications, the WM_DDE_DATA messages will be sent each time the timer triggers the delivery or removal of a phone message. The DDE conversation will continue until either application terminates the conversation. In our example, the conversation is only terminated when the user attempts to close either application. The WM_CLOSE processing as well as the subsequent WM_DDE_TERMINATE processing follow the same algorithm in both the client and server applications. When a WM_CLOSE message is received, the application enumerates all DDE conversation windows (by enumerating on the anchor window). For each conversation window, a window word is set to indicate that the user has requested a shutdown of the application. At the same time, another window word is queried to determine the handle of the conversation window with which the enumerated window is exchanging data. That window is posted a WM_DDE_TERMINATE message by the main application on behalf of the conversation window. No further shutdown processing will take place until all conversation links have terminated. Figure 24 shows the WM_CLOSE processing by the client application. Note that the server code is very similar. When it receives the WM_DDE_TERMINATE message, a conversation window checks its window word to determine if the close was initiated by its own application or by its conversation partner. If the close was initiated by the conversation partner, the window posts a corresponding WM_DDE_TERMINATE to the sender. If the close was not initiated by the conversation partner, the message is simply an acknowledgment by the partner that terminate processing may continue. In either case, the conversation window may now destroy itself, since its conversation link is ending. At this time, it posts an application-defined message, called APPM_CONV_CLOSE, to indicate to the main window that the link has successfully terminated. Figure 25 shows the WM_DDE_TERMINATE processing by the client. Again, the algorithm is the same in the server application. The APPM_CONV_CLOSE message serves as the signal that final shutdown may occur. As each APPM_CONV_CLOSE message is received, the link counter is decremented. When the link counter reaches zero, the main application checks its window word to determine whether a close was requested for the application. If close was requested and all links have successfully terminated, the application may complete its shutdown and terminate (see Figure 26). Graphics exchange provides an extremely visual working example of the use of DDE to establish permanent links between separate PM applications. Support for multiple conversation management is added simply by defining one additional message and making extensive use of the window word and window enumeration facilities. Of course, these applications may be expanded to exchange other data in addition to the graphics representations exchanged in this program. By using our example as a guide, you should be able to generalize the implementation presented in order to develop your own multitasking interprogram data exchange. We have found DDE to be a powerful and flexible element of Presentation Manager, providing an added dimension to developing interacting applications in the OS/2 multitasking environment. As the programming environment has evolved from Windows to the extensive capabilities of the OS/2 Presentation Manager, the changes made to the DDE protocol have kept pace with this new function. Moreover, it has provided a framework for further expansion as the environment continues to evolve. Figure 17 case WM_CREATE: /* broadcast the initiate message */ WinDdeInitiate(hwnd, "", "Graphics_Exchange"); break; case WM_DDE_INITIATE: /* reply with an initiate to specific server */ if (hwnd != lParam1) { if ((!strcmp("Client", DDEInitPtr->pszAppName)) && (!strcmp("Graphics_Exchange",DDEInitPtr->pszTopic))) { itoa((int)lParam1, itoa_buf, 10); WinDdeInitiate(hwnd, itoa_buf, "Graphics_Exchange"); } } DosFreeSeg(PDDEITOSEL(DDEInitPtr)); break; Figure 18 case WM_DDE_INITIATE: /* respond if app and topic strings match */ pDDEInit = (PDDEINIT)lParam2; /* check for null strings or Graphics_Exchange */ if ((HWND)lParam1 != hwnd) { itoa((int)hwnd, szTemp, 10); if(((!strlen(pDDEInit->pszAppName)) && (!strcmp("Graphics_Exchange", pDDEInit->pszTopic))) || ((!strcmp(szTemp, pDDEInit->pszAppName)) && (!strcmp("Graphics_Exchange", pDDEInit->pszTopic))) || ((!strlen(pDDEInit->pszTopic)) && (!strlen(pDDEInit->pszAppName)))) { /* create conversation window and respond */ hwndConv = WinCreateWindow(hwndDDE, (PSZ)"DDEConversation", (PSZ)NULL, WS_VISIBLE, 0,0,0,0, hwnd, HWND_TOP, ++nConvID, (PVOID)NULL, (PVOID)NULL); pWWVar->ConvCnt++; WinDdeRespond(lParam1, hwndConv, "Client", "Graphics_Exchange"); } } DosFreeSeg(PDDEITOSEL(pDDEInit)); break; Figure 19 case WM_DDE_INITIATEACK: /* establish conversation with server */ if( ((!strcmp("Client", DDEInitPtr->pszAppName)) && (!strcmp("Graphics_Exchange", DDEInitPtr->pszTopic))) || ((!strlen(DDEInitPtr->pszAppName)) && (!strcmp("Graphics_Exchange",DDEInitPtr->pszTopic)))) { /* create a window for the conversation - child of DDEanchorHWND */ DDEconversationHWND = WinCreateWindow(hwnd, (PSZ)"DDE_Win", (PSZ)NULL, WS_VISIBLE, 0, 0, 0, 0, WinQueryWindow(hwnd,QW_PARENT,FALSE), HWND_TOP, ++winDDEid, (PVOID)NULL, (PVOID)NULL); WinSetWindowULong(DDEconversationHWND, WW_CONV_HWND, (ULONG)lParam1); WinSetWindowULong(WinQueryWindow(hwnd, QW_PARENT, FALSE), WW_CONVCOUNT, WinQueryWindowULong( WinQueryWindow( hwnd,QW_PARENT, FALSE), WW_CONVCOUNT)+1); /* send a request for initial data */ DDEstrptr = st_DDE_Alloc(sizeof(DDESTRUCT) + strlen("Graphics")+1,"DDEFMT_graphics_data"); DDEstrptr->offszItemName = (USHORT)sizeof(DDESTRUCT); strcpy(DDES_PSZITEMNAME(DDEstrptr), "Graphics"); WinDdePostMsg((HWND)lParam1, DDEconversationHWND, (ULONG)WM_DDE_REQUEST, DDEstrptr, TRUE); /* send an advise to subscribe to receive future data updates */ DDEstrptr = st_DDE_Alloc(sizeof(DDESTRUCT) + strlen("Graphics")+1,"DDEFMT_graphics_data"); DDEstrptr->offszItemName = (USHORT)sizeof(DDESTRUCT); strcpy(DDES_PSZITEMNAME(DDEstrptr), "Graphics"); WinDdePostMsg((HWND)lParam1, DDEconversationHWND, (ULONG)WM_DDE_ADVISE, DDEstrptr, TRUE); } /* free the memory */ DosFreeSeg(PDDEITOSEL(DDEInitPtr)); break; Figure 20 case WM_DDE_REQUEST: /* allocate DDESTRUCT and dump graphics data */ pDDEStruct = (PDDESTRUCT)lParam2; strcpy(szTemp, "Graphics"); if((pDDEStruct->usFormat == pWWVar->usFormat) && (!strcmp(szTemp, DDES_PSZITEMNAME(pDDEStruct)))) { if(!pWWVar->bNoData) { nNumBytes = (strlen(szTemp) + 1 + sizeof(GDEDATA) + LOUSHORT(lPhFigCnt)); pDDEStruct = st_DDE_Alloc((sizeof(DDESTRUCT) + nNumBytes), "DDEFMT_graphics_data"); pDDEStruct->cbData = sizeof(GDEDATA) + lPhFigCnt; pDDEStruct->offszItemName = (USHORT)sizeof(DDESTRUCT); pDDEStruct->offabData = (USHORT)((sizeof(DDESTRUCT) + strlen(szTemp)) + 1); pGDEData = (PGDEDATA)DDES_PABDATA(pDDEStruct); st_Init_GDEData(pGDEData); pGDEData->cBytes = lPhFigCnt; strcpy(pGDEData->szItem, "phone"); pGDEData->pGpi = (unsigned char far *)((LONG)pGDEData + sizeof(GDEDATA)); GpiGetData(hpsGraphics, (LONG)IDSEG_PHONE, (PLONG)&lOffset, DFORM_NOCONV, (LONG)lPhFigCnt, (PBYTE)pGDEData->pGpi); memcpy(DDES_PSZITEMNAME(pDDEStruct), szTemp, (strlen(szTemp) + 1)); if(lParam1 != WinQueryWindow(hwnd, QW_OWNER, FALSE)) { pDDEStruct->fsStatus |= DDE_FRESPONSE; pWWVar->hwndLink = (HWND)lParam1; } WinDdePostMsg(pWWVar->hwndLink, hwnd, WM_DDE_DATA, pDDEStruct, TRUE); } else { WinDdePostMsg(pWWVar->hwndLink, hwnd, WM_DDE_DATA, NULL, TRUE); } DosFreeSeg(PDDESTOSEL(lParam2)); } else { /* post negative ACK using their DDESTRUCT */ pDDEStruct->fsStatus &= (~DDE_FACK); WinDdePostMsg(lParam1, hwnd, WM_DDE_ACK, pDDEStruct, TRUE); } break; Figure 21 case WM_TIMER: /* insert phone message into listbox, update picture, and generate new REQUESTS for all ADVISING windows */ . . /* listbox and picture have been updated */ . hEnum = WinBeginEnumWindows(hwndDDE); while((hwndEnum = WinGetNextWindow(hEnum))) { pWWChild = (PWWVARS)WinQueryWindowULong(hwndEnum, QWL_USER); if (pWWChild->bAdvise) { pDDEStruct = st_DDE_Alloc(sizeof(DDESTRUCT) + strlen("Graphics")+1, "DDEFMT_graphics_data"); pDDEStruct->offszItemName = (USHORT)sizeof(DDESTRUCT); strcpy(DDES_PSZITEMNAME(pDDEStruct), "Graphics"); WinDdePostMsg(hwndEnum, hwnd, WM_DDE_REQUEST, pDDEStruct, TRUE); } WinLockWindow(hwndEnum, FALSE); } WinEndEnumWindows(hEnum); break; Figure 22 case WM_DDE_ADVISE: /* set ADVISE bit in window data and ACK */ pDDEStruct = (PDDESTRUCT)lParam2; if(pDDEStruct->usFormat == pWWVar->usFormat) { pWWVar->hwndLink = (HWND)lParam1; pWWVar->bAdvise = TRUE; if(pDDEStruct->fsStatus & DDE_FNODATA) { pWWVar->bNoData = TRUE; } if(pDDEStruct->fsStatus & DDE_FACKREQ) { pDDEStruct->fsStatus |= DDE_FACK; WinDdePostMsg(pWWVar->hwndLink, hwnd, WM_DDE_ACK, pDDEStruct, TRUE); } else { DosFreeSeg(PDDESTOSEL(lParam2)); } } else { /* Send a negative ACK using their DDEStruct */ pDDEStruct->fsStatus &= (~DDE_FACK); WinDdePostMsg(lParam1, hwnd, WM_DDE_ACK, pDDEStruct, TRUE); } break; case WM_DDE_UNADVISE:/* turn off ADVISE bit in window data and ACK */ pDDEStruct = (PDDESTRUCT)lParam2; if((lParam1 == pWWVar->hwndLink) && (pDDEStruct->usFormat == pWWVar->usFormat)) { pWWVar->bAdvise = FALSE; pWWVar->bNoData = TRUE; pDDEStruct->fsStatus |= DDE_FACK; WinDdePostMsg(lParam1, hwnd, WM_DDE_ACK, pDDEStruct, TRUE); } else { pDDEStruct->fsStatus &= (~DDE_FACK); WinDdePostMsg(lParam1, hwnd, WM_DDE_ACK, pDDEStruct, TRUE); } break; Figure 23 DDEstrptr = (PDDESTRUCT)lParam2; switch (message){ case WM_DDE_DATA: /* process incoming picture */ gde_ptr = (PGDEDATA)DDES_PABDATA(DDEstrptr); gde_ptr->hwnd_idItem = LOUSHORT(hwnd); /* add if request * / if (DDEstrptr->fsStatus && DDE_FRESPONSE) { WinSendMsg(WinWindowFromID( WinQueryWindow(hwnd, QW_OWNER, FALSE), ID_GRAPHICS1),IC_INSERTITEM, MPFROMSHORT(ICM_END), (MPARAM)gde_ptr); } else { /* replace if advise */ WinSendMsg(WinWindowFromID( WinQueryWindow(hwnd,QW_OWNER,FALSE), ID_GRAPHICS1),IC_SETITEMSTRUCT, MPFROMSHORT(gde_ptr->hwnd_idItem), (MPARAM)gde_ptr); } if (DDEstrptr->fsStatus && DDE_FACKREQ) { DDEstrPtrAck = st_DDE_Alloc(sizeof(DDESTRUCT), "DDEFMT_graphics_data"); WinDdePostMsg((HWND)lParam1, hwnd, (ULONG)WM_DDE_ACK, DDEstrPtrAck, TRUE); } DosFreeSeg(PDDESTOSEL(DDEstrptr)); break; Figure 24 /* send WM_DDE_UNADVISE, shutdown all conversations for this application, then quit */ case WM_CLOSE: if (WinQueryWindowULong(hwnd, WW_CONVCOUNT)) { WinSetWindowULong(hwnd, WW_CLOSE, WinQueryWindowULong( hwnd, WW_CLOSE) | WIN_CLOSING_FLAG); henum = WinBeginEnumWindows(DDEanchorHWND); while (hwndenum = WinGetNextWindow(henum)) { WinSetWindowULong(hwndenum, WW_CONV_FLAGS, WinQueryWindowULong(hwndenum, WW_CONV_FLAGS) | WIN_TERM_FLAG); tohwnd = (HWND)WinQueryWindowULong(hwndenum,WW_CONV_HWND); DDEptr = st_DDE_Alloc(sizeof(DDESTRUCT) + strlen("Graphics")+1,"DDEFMT_graphics_data"); DDEptr->offszItemName = (USHORT)sizeof(DDESTRUCT); strcpy(DDES_PSZITEMNAME(DDEptr), "Graphics"); WinDdePostMsg(tohwnd, hwndenum, WM_DDE_UNADVISE, DDEptr, TRUE); WinDdePostMsg(tohwnd, hwndenum, WM_DDE_TERMINATE, NULL, TRUE); WinLockWindow(hwndenum, FALSE); } WinEndEnumWindows(henum); } else { WinPostMsg(hwnd,WM_QUIT,0L,0L); /* quit if no conversations open */ } break; Figure 25 /* post terminate to server, tell client, and die */ case WM_DDE_TERMINATE: if (!WinQueryWindowULong(WinQueryWindow(hwnd, QW_OWNER, FALSE), WW_CLOSE)) { WinDdePostMsg((HWND)lParam1, hwnd, WM_DDE_TERMINATE, NULL, TRUE); } WinPostMsg(WinQueryWindow(hwnd,QW_OWNER,FALSE), APPM_CONV_CLOSE,MPFROMLONG(hwnd),(MPARAM)NULL); WinDestroyWindow(hwnd); break; Figure 26 /* decrement conversation count and delete picture */ case APPM_CONV_CLOSE: WinSetWindowULong(hwnd,WW_CONVCOUNT, WinQueryWindowULong(hwnd, WW_CONVCOUNT) - 1); WinSendMsg(WinWindowFromID(hwnd,ID_GRAPHICS1),IC_DELETEITEM, (MPARAM)LOUSHORT(lParam1), (MPARAM)NULL); if (WinQueryWindowULong(hwnd,WW_CLOSE) && !WinQueryWindowULong(hwnd, WW_CONVCOUNT)){ WinPostMsg(hwnd,WM_QUIT,0L,0L); } break; Creating a Virtual Memory Manager to Handle More Data in Your Applications Marc Adler The amount of memory that is available to a program under the Microsoft(R) OS/2 operating system is beginning to spoil many programmers. For example, when Magma's ME Programmer's Text Editor (not to be confused with the Microsoft Editor) was ported to OS/2, one of the advantages was the ability to easily edit files larger than the available memory. Going back to the DOS version of the editor, with its limited file size, became very difficult. To satisfy the desire to edit very large files under DOS, the DOS version of ME had to be enhanced. The logical way to do that was to design and build a virtual memory manager (VMM) that could handle the demand. Figure 1 lists the APIs for the VMM. The motivation for writing a virtual memory manager was enhanced by a desire to overcome the shortcomings of malloc, the staple function of the C run-time library. Different compiler manufacturers have implemented the C memory allocation functions in different ways, each implementation being equally mysterious to the average programmer. From a programming perspective, it is advantageous to take the mysteries out of malloc and to put the inner workings of a memory management function at programmers' fingertips, to be tinkered with in special situations and to be traced meaningfully with the Microsoft CodeView(R) debugger. Being able to do such tracing and tinkering might be very useful, for example, if one felt that a program had corrupted a chain of allocated blocks of memory. Finally, there was an overriding desire to ensure that ME would never again be limited by the amount of free memory left in a given system. Unbelievably, there are still people who are using 256Kb machines. The amount of memory that DOS itself takes up combined with both the memory used by terminate-and-stay-resident (TSR) programs and the huge size of the EXE files that make up today's major applications, severely limits the amount of space that can be allocated, even with 640Kb of memory. One specific goal in designing the virtual memory manager for the ME text editor was to keep it simple enough and general enough so that it could be used in any other application that needed memory management beyond what malloc offers. Since many people have installed additional memory boards in their systems, it was also desirable to be able to exploit the capabilities of expanded memory in the virtual memory manager. (Even though ME's virtual memory manager supports EMS, its implementation won't be covered here. Such an implementation, however, would be useful as an exercise for the reader.) The VMM must be compiled in large model. The reason is that we must remain consistent with the far pointer that we use and the pointers that some of the C library routines need (such as memset). A little work needs to be done if you would like to use the VMM under a different memory model. (The actual VMM source code listings, VM.H and VM.C, are available on MSJ's bulletin boards and are not included here due to space constraints--Ed.) Interspersed throughout the text will be comments about possible extensions you could implement to make the VMM more powerful. If you decide to implement any of the suggestions, I'd be interested in receiving changes, along with your comments about how they affected the performance of the VMM. They should be forwarded to the Technical Editor of Microsoft Systems Journal. (Interesting additions and comments may be published by MSJ in a future issue.--Ed.) Figure 1 VMInit Initializes the VMM. Must be called at the beginning of the application before any memory is requested. VMTerminate Shuts down the VMM. Call it at the end of your application. char far *MemDeref(HANDLE h) Dereferences the object pointed to by handle h and returns the memory address of that object. HANDLE MyAlloc(unsigned size) Allocates size bytes from the VMM and returns a handle to that block. The block is also filled with zeros. MyFree(HANDLE h) Frees the memory block pointed to by handle h. SetVMPageSize(int kbytes) Sets the default page size to kbytes kilobytes. For instance, SetVMPageSize(16) sets the default page size to 16Kb. MakePageDirty(HANDLE h) Sets the dirty bit of the page that contains the memory block referenced by handle h. Initializing the VMM The first thing every program that uses the virtual memory manager must do is to initialize it. This is done simply by calling the function VMInit. The main job of VMInit is to create the swap file that is used when blocks of memory must be paged out to disk. VMInit will first check to see if the user defined an environment variable called METEMP. METEMP should contain the path name of where the swap file should go. If the METEMP variable is not defined, the swap file will be created in the current directory. The use of a user-definable destination for the swap file allows the user to take advantage of any RAMdisks that might be available. Swapping to a RAMdisk, of course, is significantly faster than swapping to a hard disk. You can set the METEMP variable with a line in your AUTOEXEC.BAT file that looks like this : set METEMP=<swap path> To create the name of the swap file, you use the mktemp function, which is part of the C run-time library. This function takes a single parameter, a string representing a file name template, and creates a unique file name from that template. In this case, the template that is used is VMXXXXXX. The mktemp function will replace the uppercase Xs with characters that would make the file name unique in the current directory. For instance, mktemp might return the string VM065291 to VMInit, and we would use that as the name of our swap file. At this point, I must confess that the virtual memory manager has one major limitation; the swap space is bounded by the available space on the swap disk (plus the amount of expanded memory that is free). A possible extension would be to allow the VMM to use multiple volumes when swapping, including swapping over a network to a totally different computer (such as a remote file server). If you do this, beware of the DOS limitation on the number of files that an application can have open simultaneously. Terminating the VMM Before terminating, an application must call VMTerminate to do some cleanup work. VMTerminate simply closes the swap file and deletes it. If you added routines to do performance analysis or to swap to multiple volumes, you might need to do some cleanup work at this point. Obtaining Memory The function that obtains memory from the VMM and returns it to the caller is MyAlloc. It replaces the standard call to malloc. Since the memory returned is zeroed out, MyAlloc can replace calloc as well. A single argument is passed to MyAlloc--the number of bytes needed--and a handle is returned. The word handle is becoming an increasingly popular term, mainly because of its frequent usage in Microsoft Windows. A handle is simply an identifier that is associated with a block of memory; it is used by the internal routines to identify an object. In the true spirit of data hiding, the value of a handle (also referred to as a magic cookie) will typically have no meaning to the application that uses the VMM. In the case of our VMM, a handle is an unsigned long quantity comprised of two parts. The upper 16 bits is the page number in which the block of memory is found, and the lower 16 bits constitutes the zero-based offset from the beginning of that page. For example, a handle whose value is 00030400H signifies that the memory block is located 400H bytes away from the beginning of the block allocated for page 3. Figure 2 illustrates this example. This design allows us to have at most 64Kb pages, each page containing as many as 64Kb of memory. Thus our VMM can access a total of more than 4Gb, well over the maximum size of the largest hard disk. The Structure of a Page As I stated earlier, MyAlloc expects a single argument that represents the amount of memory we want to allocate from the VMM. If we cannot find a page with enough free memory, a new page will be allocated. At this point, let's see what is in the PAGE data structure. You'll find the declaration of the PAGE data structure (shown in Figure 3) in the listing of VM.H. Since a block can reside anywhere in RAM, we need a field to hold its far address. A block can reside on disk, so we also need a field to hold the offset from the beginning of the swap file where the block is found. These two fields are called memaddr and diskaddr. In addition to these two fields, we have fields that contain the ID of the page (a unique integer), the size of the page (in case we modify the VMM to deal with different sized pages), a bit mask to represent the status of the page (if it's in memory, on disk, or both, and whether the page is dirty), the offset to the first free byte in the page (used for implementing a linked list of free blocks within the page), and the clock for the least recently used (LRU) swapping algorithm. We also have fields for the number of free bytes within the page and the size of the largest free block of contiguous memory. These two fields can be used in conjunction, in the event we modify the VMM to do true compaction of free blocks. The amount of space allocated for a page should be a power of 2. With careful experimentation, you might optimize the performance for your application. A routine known as SetVMPageSize is provided that allows the application to set the page size from within your application. It should be called directly after VMInit. If you decide to alter the default size of a page, then you must call SetVMPageSize only once within your application, and the call should be made before any pages are actually allocated. The reason for this is that the swapping algorithm thinks that each page is the same size. We use a default size of 4Kb for each page; however, in the version that uses expanded memory, the page size is increased to 16Kb to match the size of an expanded memory page. Within each page, a linked list of the free blocks within that page is maintained. This list is defined by the FREEINFO structure. Each free block (and allocated block) has a header that records the number of bytes in the block and the offset to the next free block in the page. A block that has the value of FFFFH in its offset field is the last block in the chain. This linked list is a simple one with no implicit ordering of blocks. We will talk about enhancing this list when we discuss the freeing of blocks. Figure 4 shows an example of a typical chain of free blocks. When MyAlloc is called, we search the page list for the first page with the necessary amount of contiguous free bytes. The function that handles this search is FindNContigBytesFree. Precedence is given to pages that are already in memory, but if there are no pages in memory that contain the needed bytes, we look for the first disk-based page that has the free space. If there are no pages either on disk or in memory that have a sufficient number of contiguous bytes free, we allocate a new page and return its address. The AllocPage routine is responsible for allocating space for a new page header and for the associated buffer. Back in MyAlloc, we have a pointer to a page with the necessary number of bytes free, and we are assured that the page is in conventional memory. We then traverse the list of free blocks within that page and stop when we find the first block with the necessary free bytes. The block is zeroed out and the handle is returned to the calling routine. Figure 3 typedef struct page { struct page *next; /* chain to next free page in list */ char far *memaddr; /* memory address of the page block */ unsigned long diskaddr; /* disk address of the page block */ PAGEID id; /* page identifier */ unsigned long LRUcount; /* least-recently-used count */ unsigned pagesize; /* how many bytes is this page */ unsigned freebyte; /* index of 1st free byte in page */ unsigned bytesfree; /* # of bytes free in this page */ unsigned maxcontigfree; /* max # of contiguous free bytes */ unsigned flags; #define PAGE_IN_MEM 0x0001 #define PAGE_ON_DISK 0x0002 #define IS_DIRTY 0x0004 #define SET_PAGE_DIRTY(p) ((p)->flags |= IS_DIRTY) #define NON_SWAPPABLE 0x0008 #define PAGE_IN_EMM 0x0010 } PAGE; Swapping Pages In this version of the VMM, a call is made to the Microsoft C library routine, _dos_allocmem, to allocate memory for a page. This routine is really a front end for the main DOS memory allocation service (Int 21H, function 48H). Using the DOS memory functions lets us totally bypass the malloc family found in the C run-time library. (Actually, each page header is allocated using malloc, but we can choose to use DOS memory for this if we want.) We continue to use _dos_allocmem to grab space for a page until we run out of memory. At this point, we have a page header allocated for the new page but no block of memory allocated for its data. What we need to do is borrow the memory used by a previously allocated page. But before that can be done, we must save that page's data somewhere. Then the new page can use that page's block of memory to store its own data in. This process is called swapping or paging. How do we know just where in the swap file the old page's contents should be stored? The array VMFile.slottable contains a map of which sectors of the swap file are used by which pages. A NULL entry for a sector means that the sector is free. When we swap a page to disk for the first time, we search the slot table for the first NULL entry and then write the page to the corresponding sector. The remaining question is, How do we determine which page to swap out to disk? If we have a bad algorithm for choosing the swappable page, we can run into a hideous phenomenon know as thrashing. If a VMM thrashes, it is spending an inordinate amount of time swapping pages between disk and memory. That can happen if we choose to swap out a frequently referenced page. For our swapping algorithm, we use the old, time-tested least recently used algorithm. In order to implement the algorithm, we must keep a clock which is incremented every time a page is accessed. Each page has a variable which records the time when it was last accessed. To find the LRU page, we just scan the page list for the page with the minimum clock time and return a pointer to that page. A possible enhancement to the VMM is to implement a means of making certain important pages nonswappable. For instance, instead of using malloc to allocate the headers for each page as we do now, we could allocate memory from the VMM for them. However, it would be disastrous if the page headers were swapped out to disk! In this case, we might use a nonswappable page to hold the headers and other important system information. The LRU algorithm would require a simple modification that would tell it not to consider pages marked as nonswappable. Another possible enhancement is to keep the list of pages in a doubly linked list rather than a singly linked one and to maintain a pointer to the tail of the list. Since we move a page to the head of the page list whenever we access that page, we will be guaranteed that the least recently used page will be at the tail of the list. Using this method, we find the LRU page merely by looking at the tail; we don't need to implement a clock for the LRU algorithm, and we don't have to allocate a counter for each page in the system. A Perfect Fit? The method of allocation followed here is called the first fit approach. It's given that name because we stop our search at the first block that fits the criterion, that is, the first block that has enough contiguous bytes free. Another popular approach is called best fit; we traverse the entire free list in search of a block with the smallest size that satisfies our criterion. A third method is called next fit; we remember the position the last block that was allocated came from, and the next time we search for a free block, we start at that spot. The first fit method has the advantage of taking less time to find the proper memory block, and best fit has the advantage of reducing fragmentation (if you use one of the methods of reducing fragmentation discussed below). Fragmentation of memory is a major concern when designing a memory manager. It is caused when you allocate part of a memory block that has more free bytes than you really need: part of that block will be allocated and the remainder will be put back onto the free list; however, if the remaining block is too small for any subsequent allocation request, it may never be allocated. For example, let's say that I need a block of 24 free bytes, and after searching the chain of free blocks, I come to a block that has 32 bytes free. I will allocate 24 bytes out of that block and leave a block of 8 bytes on the free list. This free block is probably too small to satisfy any future allocation request, so it will remain forever on the free list. That may not seem so bad, except that the memory manager will still have to examine the block whenever it searches the free list; multiply this example by the thousands of memory requests that a typical application might make and you'll see why fragmentation is a severe problem. Figure 5 illustrates a graphic representation of fragmentation. A simple way of solving fragmentation problems is to round off an allocation request to a higher number; the excess bytes will be included in the free block that we return to the application. For the example above, I might use the simple heuristic of rounding off all allocations to the nearest power of 2. If I ask for 24 bytes, and I come upon a block of 32 bytes, then I will return the entire block to the application. Although 8 bytes may never be used, I will not have a small block floating about in the free list. Another solution is to perform periodic garbage collection and periodic compaction on the memory blocks, an approach we will also consider. Dereferencing Handles You will recall that the handle to a memory block is merely an identifier that tells the VMM how to reference that block; the application does not really know what memory address the handle points to. Once an application obtains a handle to a memory object, it has to go through a dereferencing step in order to use the memory block that the handle refers to. Remember, too, that a handle is an unsigned long value that comprises the page identifier and the offset within that page. The function MemDeref must be called whenever you need to transform a handle into a memory address. For example, if we wanted to copy the string XYZ into an allocated memory block, we must write the following code: #include "vm.h" ■ ■ ■ HANDLE h; char far *s; ■ ■ ■ /* Note--this assumes large memory model */ if ((h = MyAlloc(4)) == (HANDLE) NULL) /* perform error processing */ ■ ■ ■ if ((s = MemDeref(h)) != NULL) strcpy(s, "XYZ"); MemDeref simply breaks the handle into its constituent parts, searches for the page with the ID contained in the handle, swaps the page into memory if necessary, adds the base memory address of the page with the offset of the block that the handle specifies, and returns a pointer to the memory address. One nice thing that MemDeref also does is to move the referenced page to the front of the linked list of pages that are currently in use. The VMM assumes a locality of reference within an application--that is, once a page is referenced, it will continue to be referenced in the application's surrounding code. Moving the newly referenced page to the front of the page list reduces the time needed to traverse the page list for subsequent searches for that page. Touching a Page If you look carefully at the code for the WritePage function, you'll notice that a page will get written to disk only if it's currently in memory and if the page has been modified since it was allocated or last written to disk. If the same image of a page exists both in memory and in the swap file, there is no need to perform the actual write to disk. The VMM includes a routine that sets the dirty bit of the page that contains a referenced memory block. The MakePageDirty function takes a single argument, the handle of a memory block, and searches for the page corresponding to that block. When that page is located, its dirty bit is set. This scheme ensures that when we modify a certain portion of memory, the associated page will be swapped properly when memory begins to run low. Referring to the example above, if we wanted to change the bytes pointed to by handle h to the string ABC, we would need to do the following : s = MemDeref(h); strcpy(s, "ABC"); MakePageDirty(h); Freeing a Memory Block When a memory block is no longer needed, we call the MyFree routine to release it back into the memory pool. MyFree takes a single argument--the handle to the memory block that we will release. To release a memory block, we simply place it on the page's linked list of free blocks and increase the number of free bytes within that page. What makes the operation a little more complicated is the fact that we would like to examine the blocks adjacent to the newly freed block, and if they are free, coalesce them with the newly freed block. When coalescing is done, our most important statistic, the number of contiguous free bytes, also increases. To increase how quickly we traverse the linked list of free blocks and to help us examine adjacent blocks, we keep the list sorted by the blocks' offsets. When we free a block, we traverse the list until we find a free block whose offset is greater than that of the newly freed block. Then we insert the newly freed block in the list before this block. Figure 6 illustrates a typical coalescing action. The astute reader will notice that although the user can theoretically request up to 64Kb of memory (the maximum value of the argument to MyAlloc), the amount of memory requested is limited to the size of the page block. To accommodate this limit, we can modify the VMM to dynamically modify the basic page block size if a request comes in that is too large. The Double Indirection Method Earlier, we alluded to the fact that periodic compaction of the memory blocks would reduce fragmentation in our VMM. Before compaction, the free blocks are intermingled with the allocated blocks, and the free list must be traversed in order to locate a block with the desired amount of contiguous bytes free. After compaction, all the allocated blocks are pushed to one side of the page, and a single large free block is created out of the remaining space. When we look for a free block to allocate, there is only one block to examine in the page. Compaction presents one major problem, however. If we move an allocated block to another position in memory, we must modify all our application's variables that point to that block to point to the new place in memory. But how does the memory manager know which variables point to that memory block? To solve this problem, we use a scheme known as double indirection. Double indirection has been made popular by the memory managers of both the Macintosh(R) and of Microsoft Windows. When an application requests a block of memory, we will return not a pointer to that block, but a pointer to a pointer to that block. This is illustrated in Figure 7. Figure 8 illustrates what happens when we compact a number of memory blocks. Even though the blocks shift in memory, the pointers to the blocks remain stationary. Since all our application knows about are the indirect pointers, and since the position of those pointers doesn't change, the memory manager does not have to alert the application when the compaction operation occurs. Everything is totally invisible to the application. (Actually, in Microsoft Windows Version 2.x, an application can choose to be alerted when allocated blocks are moved by specifying the GMEM_NOTIFY flag in the call to GlobalAlloc.) As you can see from the diagrams in Figures 7 and 8, the double indirection method requires one extra memory reference in order to dereference a memory handle. However, with the speed of modern day CPUs increasing yearly, the extra memory reference is not as much of a problem as it used to be. If you consider the reduction of time involved in searching the free list, and also take into account the elimination of fragmentation, you'll see why the designers of Windows chose the doubly indirect way of doing these things. The McBride Allocator Let's take a brief look at another virtual memory management package, VMEM by Blake McBride. VMEM (see Figure 9), which also comes with full source code, uses the double indirection method to achieve high performance. Although it does not support expanded memory at this time, it compensates for this deficiency by allowing compaction of the swap file. It also uses one of the enhancements that was mentioned above--it maintains the virtual memory objects on a doubly linked list so that there is always a pointer to the least recently used object. The user has a choice of two methods of swap file compression. Using the first method, all objects are moved to the beginning of the swap file, so that one large hole remains at the end. The second method uses two swap files; all allocated objects are copied from the first swap file to a newly created second swap file, then the original swap file is deleted. Although both methods produce the same results, each method has an important advantage. With the single file method, even though the allocated blocks have been moved, the size of the swap file will never decrease. With the dual-file method, you need twice the disk storage during the compression operation. VMEM also has the ability to save and restore entire virtual memory images to and from disk. This capability is ideal if, for example, you'd like to save the entire state of the virtual memory system, release the memory back to DOS, shell out a large program (like a compiler), and restore the state of the system when the shelled program is completed. A study of the VMEM API will reveal that you have a bit more control over the operating parameters with VMEM than you do with our simple memory allocator. The API lets you choose whether you want to clear the memory when you allocate, set the dirty bit of a block when you deference that block, and directly change some of the important operating parameters of VMEM. Using a VMM as a replacement for heap-based allocation gives an application more flexibility in dealing with huge amounts of data. I believe that virtual memory is best done at the operating system level (for example, using Microsoft OS/2) and kept invisible from all applications in the system. But since a large part of the market is, and will remain for some time to come, firmly entrenched in the DOS world, virtual memory managers are still needed. In the future, all operating systems will have built-in virtual memory and will make use of the dedicated memory management chips produced by semiconductor vendors like Intel and Motorola. Let's hope that that day is not too far away. Figure 9 char far *VM_addr(VMPTR_TYPE voh, int dirty, int fFreeze) Dereferences the memory handle voh. The variable dirty should not be zero if you are going to change the contents of the memory block. fFreeze is not zero if the block's position in memory should be frozen. VMPTR_TYPE VM_alloc(long size, int fClear) Allocates size bytes from the memory pool. If fClear is not zero, the memory block will be cleared to zeros. VM_dcmps Initiates compression of the swap file. The compression method may be selected with VM_parm. int VM_dump(char *filename) Dumps the entire contents of the VM system to the disk file filename. Returns 0 if the dump was successful, nonzero if not. void VM_end Terminates VMEM. The swap file is deleted, but real memory is not released back to the operating system. void VM_fcore Same as VM_end, except that all real memory is returned to the operating system. void VM_free(VMPTR_TYPE voh) Frees the object referenced by handle voh. int VM_rest(char *filename) int VM_frest(char *filename) Restores the VMM to a previous state that was saved by VM_dump. The main difference between VM_rest and VM_frest is that the VM_rest will read in the memory blocks only as they are needed and is therefore much faster than VM_frest. int VM_init Initializes VMEM. void VM_parm(long rmmax, long rmasize, double rmcompf, long dmmfree, int dmmfblks, int dmctype) Sets various VMEM parameters. Used to fine-tune the system. The arguments are the following: rmmax -- maximum amount of real memory that VMEM will request rmasize -- minimum amount of real memory that VMEM will request rmcompf -- real memory compression factor dmmfree -- determines when automatic swap file compression occurs dmmfblks -- another method used to determine when swap file compression occurs dmmctype -- type of swap file compression used VMPTR_TYPE VM_realloc(VMPTR_TYPE voh, long newsize) Reallocates the memory block pointed to by voh and returns a handle to the new block. long *VM_stat Used to obtain various statistics about the current state of VMEM. Using the OS/2 Video I/O Subsystem to Create Appealing Visual Interfaces Richard Hale Shaw The most noticeable attribute of an application is the way it visually interacts with the user. If the application looks snappy and smart, the user will gain confidence when running it for the first time. If the application appears slow and dull, however, the user will become apprehensive or, worse yet, bored. Thus, from the user's perspective, the screen is the most essential mechanism of the application. To an OS/2 application developer, this makes the video I/O (VIO) the most important of the three subsystems available to OS/2 character-based applications. An Overview of VIO The MS-DOS(R) operating environment provided such limited and inefficient video services, that application developers looked for other means to improve the video throughput of their programs. A great number of DOS applications took advantage of the PC ROM BIOS video services (Int 10h) or wrote to and directly manipulated the video hardware and display buffer. This approach hindered portability, but it was not a problem under the real mode of the DOS operating environment, since only one application at a time was able to access the video hardware. In contrast, OS/2 systems are endowed with a robust highly efficient set of video services, which comprise the VIO subsystem. The subsystem consists of a set of character-oriented display services of the type that are generally required by the current generation of character-based applications. Think of VIO as a superset of the PC ROM BIOS services (Int 10h) found in real mode under DOS, with the difference being that VIO uses calls instead of an interrupt. The efficiency and effectiveness of the VIO services are such that there is little need for an application to manipulate the video hardware directly. Perhaps the only reason an OS/2 application would need to access the video hardware while using VIO would be to generate graphics images. VIO has only limited graphics support, but it does allow an application to move to and from graphics mode (this will be discussed in greater detail below). In the event that an application does need to access the video hardware, OS/2 can serialize such accesses and provide a means for applications to do so that's compatible with its protected mode environment. Although it's essential that a real operating system like OS/2 offer efficient video services (where DOS does not), there is another, more profound reason for the existence of VIO. The protected mode of the OS/2 operating environment requires that system facilities be virtualized and shared among processes, including access to the video screen and video hardware. An application should not be hindered from performing screen updates while running in the background, but a foreground application should not have its screen trashed by a program running in the background. By using VIO, the user is assured that the video output of one application will not interfere with that of another application. In addition, most VIO calls can be used in bound programs that must run under both OS/2 and DOS. Like the ROM BIOS calls or direct video hardware control code under DOS, VIO calls under OS/2 make a program less portable to the DOS environment and more OS/2 specific. Of course, this is true of all non-FAPI OS/2 calls. VIO is implemented as a dynamic-link library (found in VIOCALLS.DLL), so a program's references to VIO services are bound at execution time, not at link time. Thus, you can replace VIO functions at any time without recompiling or relinking the client application. This is how the OS/2 Presentation Manager (PM) handles VIO calls. By providing its own version of VIOCALLS.DLL, PM can allow non-PM OS/2 programs to run under it--even in the PM screen group (that is, in a PM window). VIO calls are efficient, but they do not offer the complex formatting facilities of the printf standard library function nor do they handle output redirection. A subsystem like VIO must be fast, so it's necessary that it avoid the encumbrances of the file system and I/O redirection. In those instances in which redirection is a consideration, however, you can use DosRead and DosWrite. These two kernel services use VIO to handle the screen component of their output. Note that when STDOUT points to a screen device, OS/2 routes it through VIO. If generic screen output is a consideration, you'll find it simpler and easier to use the standard C library routines, since a typical application will probably use a combination of these and VIO calls. The VIO services do, however, allow considerable flexibility in the way text is written to the screen, including control over color and cursor positioning. The Logical Video Buffer As mentioned earlier, OS/2 considers the screen a resource that is able to be simultaneously shared by more than one process. When a session is started, OS/2 creates a logical video buffer (LVB) for it. OS/2 retains control of the physical video buffer (PVB). If a program makes a VIO call, VIO will update the LVB of the program's session and notify OS/2 that the PVB must be updated. While a session is in the foreground, OS/2 will duplicate VIO updates of the LVB in the PVB. Thus, VIO calls always update the two buffers while a session is in the foreground. When you move the session into the background, OS/2 assumes that its screen contents are completely updated in the LVB. Therefore, it doesn't have to save the screen and can quickly update the PVB from the new foreground session's LVB. This makes screen switches very fast and allows OS/2 to virtualize screen access among processes. While the session is in the background, VIO services will continue to write to the session's LVB. OS/2 ignores calls from VIO to update the PVB, since the foreground session is using the PVB. When the session is brought into the foreground again, OS/2 will copy the contents of the LVB to the PVB and resume updating the PVB from the LVB. Thus, as long as the application uses VIO services to do its video updates, the LVB will always accurately depict an application's visual state in character mode. This remains true whether the application is in the foreground or background, and it makes VIO a safe means of updating the video screen. Each LVB is organized in 2-byte pairs, called cells (shown in Figure 1). The number of cells in the LVB will vary with the current video mode (for example, a 43-line mode will require more cells than a 25-line mode). Each cell corresponds to one character on the screen, with the first byte of each cell containing the character itself and the second byte holding the attribute value (which controls foreground and background color, intensity, and blinking). You can use different VIO services to write characters, attributes, or cells to the video screen. The VIO subsystem supports the full range of PC-based video adapters and displays, including monochrome, CGA, EGA, and VGA. The 24 different flavors of text and graphics modes that are available are shown in Figure 2. The smallest user-addressable unit of the screen is a character in text mode or a pixel or pel in graphics mode. PC display hardware is memory mapped, so whatever is currently stored in video memory (the PVB mentioned above) is displayed on the screen. Other than the description of the video buffers given above, however, it's not essential that you know how video memory is organized or where it's physically located, unless you're going to access the video hardware directly. Also, note that unlike DOS, OS/2 doesn't use mode numbers to specify a video mode. When getting or setting the video mode, you deal directly with the video characteristics themselves. As previously mentioned, the attribute byte of each pair or cell controls the colors, intensity, and blinking characteristics of a character displayed on screen. Of the 8 bits in an attribute byte, the lower 3 bits control the foreground color, a value of 0-7. Bit 3 toggles intensity on or off, bits 4-6 control the background color, and bit 7 toggles the blinking characteristic. These are shown in Figures 3 and 4. VIO and PM As you are probably well aware, an OS/2 system offers a high-powered graphical user interface called the Presentation Manager. PM offers a graphical presentation space, windowing, scroll bars, icons, and interfaces for both a mouse and the keyboard. It runs in its own screen group under OS/2 and can run other programs in PM windows in the same screen group. These capabilities differ from those of VIO applications under OS/2 Version 1.0, where a background process remains in its own screen group and is not visible until a user brings it into the foreground screen group. PM can also run ill-behaved programs (that is, those programs that do not run in a PM window or that write directly to the PVB), but they will run in their own screen group like any other OS/2 program. Although the Presentation Manager can do all of the things mentioned, there is still a need for VIO. First, PM applications are complex. They take more effort to write since simple visual ideas can be complex to express in PM code. Second, some applications (a command-line utility, for instance) do not require PM or wouldn't benefit very much from PM's graphical user interface. Other applications will have to be redesigned or completely rethought before they can take advantage of PM. These are problems that VIO can easily solve. In addition, VIO offers a path to OS/2 that requires minimal changes to existing DOS applications. Thus, it's considerably easier to adapt a DOS application to VIO than to PM. VIO-based programs will run under a PM text window. That's because the versions of VIO (and, for that matter, KBD and MOU) offered with PM (OS/2 Version 1.1) are different from those supplied with OS/2 Version 1.0. The VIO DLL supplied with PM is windows aware and can map each character and its attributes into the appropriate pattern of pixels inside a PM window, while clipping the output of each VIO call to fit the application's window size. The entire process is invisible to both the programmer and the compiled program. If you use VIO in your OS/2 applications today, you won't pay an extensive penalty later. As OS/2 is ported to other processor families, VIO calls will remain the same and should not require changes to those portions of an application that use VIO. Future releases of OS/2 for the 80286 family of processors won't require a recompile, since new versions of VIO will be supplied with each release. This is why VIO is implemented as a dynamic-link package. As this series continues, I'll include practical tips that will make it easier for you to produce efficient, high-quality programs that take advantage of OS/2 facilities. If you refer back to the second article in the series, "Planning and Writing a Multithreaded OS/2 Program With Microsoft(R) C," MSJ (Vol. 4, No. 2), there is a discussion of some of the ways a multithreaded program can use up too many CPU cycles, making it difficult for other processes to run (particularly those in the background). You'll see another example of this in the program listing for this article. From the perspective of using OS/2's video system, however, keep in mind that a well-behaved OS/2 program is one that confines itself to the use of VIO (most standard library functions call VIO) and does not access the PVB. VIO Data Structures Earlier in this series, I briefly discussed the new OS/2 header files, structures, and type definitions. Because of the variety of objects used to manipulate the screen, there are several data structures designed strictly for VIO programming. These are displayed in Figure 5, which you can refer to as you read about the sample program listing. The VIO data structures provide all of the information used by the old IBM(R) PC ROM BIOS calls, and more. But instead of passing information in registers (the convention in the DOS environment), OS/2 places the information in a structure whose address is passed to the appropriate function. Not only does this make retrieving and changing the video environment rather easy, it places a machine-independent interface on the entire mechanism. Moreover, in keeping with other OS/2 functions, it allows the function to return a single unsigned integer as an error return status. For example, the program listing contains a function (Cursor) that can save or restore the cursor. Notice that it uses the VIOCURSORINFO object type, which includes the starting and ending cursor scan lines, the cursor width (in columns or pixels depending on the current screen mode), and the cursor character attribute. Information in the VIOCURSORINFO object is easily retrieved and/or modified via the VioGetCurType and VioSetCurType functions, as shown in the listing. The VIO Pop-up Facility VIO offers a means by which a background process can temporarily pop up in the foreground or a foreground process can temporarily switch to a blank text screen. If you note the stress on temporary, you'll begin to see that the need for pop-ups is not the same as with DOS. Under DOS, a terminate-and-stay-resident (TSR) application could take control of the processor and pop up over the current application for an indefinite period of time. Since DOS is a single-tasking environment, this became an acceptable manner of communicating with a TSR program. And since the current application was frozen until the TSR gave up control of the processor, the TSR could stay popped up for as long as you wished. Under OS/2, the characteristics of a TSR program in the DOS environment are no longer needed since you can run an application in another screen group and switch that screen group into the foreground whenever, as often, and for however long you like. From the perspective of accessing a TSR program, every program is a TSR under OS/2. It's always there, running in its own private screen group, although you can still issue the commands to the application for it to terminate. This is even more obvious under PM, since PM and well-behaved VIO programs that run in the PM screen group can simultaneously display and run in overlapping windows. Remember that a process can be detached and run in a special screen group, where it does not have to have regular keyboard input or show screen displays. Detached processes are good candidates for pop-up programs under OS/2. You'll doubtless come up with other ideas. Pop ups are exceptions to the way that OS/2 typically handles the console. Only one pop-up program can be activated at a time: if another process issues a pop-up call, it will be blocked until the first one relinquishes the screen. While a pop-up program is active, the user cannot switch to another process or screen group. In this sense, the pop-up program gains temporary ownership of the foreground screen. The pop-up facility of OS/2 should only be used by a process that needs to gain temporary control of the physical console (that is, the screen, the keyboard, or the mouse). Since other processes cannot be switched into the foreground during a pop up, the pop-up program must be efficient, do its job, and end the pop-up session quickly. A good example of how the OS/2 pop-up facility is used is the OS/2 Hard Error handler. You can observe its use of the pop-up facility by issuing a DIR command on a disk drive while the drive door is open. When a pop-up program begins, the screen is automatically placed in 80 by 25 text mode. The screen is restored to the previous mode when the pop-up program ends. The VIO service routines for activating a pop-up program are VioPopUp and VioEndPopUp. The function prototypes for these two routines are shown in Figure 6. Note that only a subset of VIO services are available during the pop-up call (shown in Figure 7). An example of code that activates a pop-up program can be found in the graphics function in the sample program listing. Using the VIO API Any discussion of the VIO Application Programming Interface (API) ought to begin with a reference to Ray Duncan's article, "Character-Oriented Display Services Using OS/2's VIO Subsystem," MSJ (Vol. 2, No. 4). It contains an excellent overview of the API and discusses most of the services that VIO provides. This article will now pick up where Duncan's article left off and, in doing so, will walk through the sample program listing presented here. The sample is a modified and enhanced version of HELLO0.C, which was presented in the preceding article in this series. This discussion will provide insight into the practical application of VIO services. A New HELLO.C Recall that HELLO0.C was a multithreaded version of HELLO.C. It was designed to illustrate the use of threads and semaphores to control and serialize access to resources. Although technically sound, it was visually boring, so adding a variety of colors to it is a good idea. In addition, the current version of HELLO1.C (The actual code listing is available on MSJ's bulletin boards and is not included here due to space constraints--Ed.) has been restructured and modularized to allow different sample functions to be plugged in with ease. Probably the first thing you'll notice in HELLO1.C is that the macro INCL_SUB has been defined at the beginning of the program listing. As such, the program automatically includes the function prototypes, macros, and type definitions that will be needed to use VIO functions via the system of header files and defines that were discussed in the earlier article. Next, note that every VIO function requires a device handle as its last argument. In non-PM versions of OS/2, this handle is always zero. A special set of VIO calls under PM (called advanced VIO, or AVIO) use this argument. The argument is represented by a macro, VIOHDL, in the listings here, making it easy to search for and replace references to it later. Finally, in fitting with the convention used by all OS/2 API functions, notice that VIO services that return an error code will return a nonzero unsigned value when an error occurs. You can always assume that a VIO service was successful when it returns a zero value. Also note that one of the most commonly used VIO routines, VioWrtTTy, is now called VioWrtTTY in OS/2 Version 1.1, which has been released since this series was initiated. The Startup Function As you see from the listing, main has been simplified to include only a handful of functions. The first of these, startup, processes the optional command-line arguments. As in HELLO0.C, these arguments are the number of milliseconds used in calls to DosSleep, which allow you to vary the sleep time between thread events. If you set these values too low, the program will hog CPU time and radically slow down OS/2 and other programs. These arguments are discussed in more detail below. Startup performs several other services for the program. It initiates a keyboard thread (discussed below). Then it calls the Screen function to save the current video display for restoration when the program terminates. Screen takes a single argument, which determines if the screen should be saved or restored. It assumes that the calling program's screen group is in text mode and calls VioGetBuf to retrieve the address of the screen group's LVB and the size of the buffer. Thus Screen can allocate storage to hold the LVB contents and copy the contents of the LVB to the new storage. To restore the display when the program terminates, Screen copies the contents of the storage buffer back to the LVB and calls VioShowBuf to update the physical display. This process is further discussed in the sidebar, "Direct Screen I/O and Graphics under VIO". Startup also calls the Cursor function to hide the cursor and save the cursor position. Like Screen, the Cursor function takes a single argument, whose bits determine whether Cursor should hide, save, or restore the cursor (that is, position it and make it visible). It uses VioGetCurType to retrieve the cursor character attribute and VioSetCurType to change the attribute (and hide the cursor). It calls VioGetCurPos to save the current cursor position and VioSetCurPos to reposition the cursor. Note that the Screen and Cursor functions rely on the VIOCURSORINFO data type. Startup also resets the screen rows to the highest number possible. It does this by calling VioGetMode to get the current video mode information (into a VIOMODEINFO object type). Then it sets the row parameter to either 50, 43, or 25 lines and calls VioSetMode until it is successful. This lets the program run at 50 lines on a VGA monitor, 43 lines on an EGA monitor, and 25 lines otherwise. The original row count is saved so the termination function can restore it before the program exits. After startup, main calls the initialization function shown in the listing. This code is virtually identical to the code used by HELLO0.C in the preceding article and initializes the FRAME structures. Flickering Frames The flickering frames of HELLO0.C that greeted you with "Hello, World from thread" are now encapsulated in the flicker_frames function. The code for this function is very similar to that in the original program. The function will create about two dozen threads; the exact number will vary with the number of screen rows. Each thread carries the responsibility of displaying or clearing its frame on cue from its semaphore, which is stored in its FRAME data type. And each thread shares the code found in the hello_thread function. Hello_thread now calls VioWrtCharStrAtt to do its screen I/O. This is a VIO service routine that writes a string at a specified row and column location with a specific attribute. Note that an attribute byte for each frame (and thus each frame thread) has been added to the FRAME data type and is set and manipulated by flicker_frames. Thus, whenever each frame appears it has a different foreground and background color. The flicker_frames function masks off the blinking bit in the attribute so that the frames do not blink. In addition, the function increments the FRAME attribute byte whenever the foreground and background colors match, thereby ensuring that the foreground and background contrast. In order to accommodate the changes to the FRAME type, the program now includes a filler byte. This ensures that the threadstack member is aligned on an even-numbered address. The _beginthread library routine will not successfully create a thread whose stack falls on an odd-numbered address. The operator initiates the termination of the flicker_frames function by pressing the Esc key. This activates the keyboard thread, which is contained in the keyboard_thread function mentioned above. This thread blocks on keyboard input and continues to block until the Esc key is pressed. Then it blocks on a semaphore, doneSem, until flicker_frames clears the semaphore. Clearing the semaphore allows keyboard_thread to begin the termination sequence and flicker_frames to postpone the sequence until it has completed a round of activating the frame threads. Once the semaphore clears, keyboard_thread continues and sets the done flag, thereby notifying flicker_frames that it can terminate the frame threads. Flicker_frames will cause each thread to run one final time, clear its box (if it is not already cleared), and terminate. As each frame thread terminates, it clears its own frame semaphore to notify flicker_frames that it is terminating. In the meantime, flicker_frames calls the WaitForThreadDeath function, which blocks on each thread's semaphore, and returns to the flicker_frames when all the threads have terminated. Then flicker_frames returns to main. Chasing Frames New to HELLO1.C are chasing frames, which demonstrate an animated use of the VIO scrolling routines. Encapsulated in the chasing_frames function, the chasing frames are a series of Hello, World frames that are launched from the upper-right-hand corner of the screen and traverse the screen perimeter counterclockwise. Each successive circuit takes place one frame height and width inside the last, so that the frames gradually make their way to the center of the screen. Another frame follows after a period of time (which is set by one of the command-line sleeptime arguments). The VIO scrolling func tions, VioScrollLf, VioScrollDn, VioScrollRt, and VioScrollUp, not only cause the frames to chase each other, they also leave a trail of each frame's color behind. Each chasing frame is managed by its own thread, thereby giving a visual confirmation to the reality of independent threads of execution. The threads share the code found in the box_thread function. Again the keyboard_thread function plays a role in terminating these threads. After servicing flicker_frames, the keyboard thread goes into a loop, blocking on keyboard input and ignoring all but the Esc key. When the Esc key is finally pressed, the keyboard thread will again set the done flag and terminate. This in turn notifies chasing_frames not to start any additional frames. It also notifies those frame threads that have already started to terminate themselves. If a frame thread is in the middle of scrolling around the screen, its sleeptime will fall to zero and it will race to the upper-right-hand corner of its current circuit. Then it will clear its own semaphore and terminate. As flicker_frames did before it, chasing_frames calls WaitForThreadDeath to wait until all the frame threads have died; it then returns to main. There is one catch to the way the keyboard thread works. The user must press the Esc key before the last chasing frame completes its final circuit. Otherwise, the keyboard thread will continue to block until the Esc key is pressed. The next section of the program assumes that the keyboard is available, so the keyboard thread must have terminated by that time. Unfortunately, there is no kernel routine for one thread to kill another. In any case, it would have made keyboard_thread even more complex to cause it to terminate some other way. The Video Configuration The next function called from main is displayconfig. This function calls get_user_name to prompt the user for his or her name. To do this, get_user_name displays a multicolored prompt, and below it, a highlighted input field delimited with two markers. It uses VioWrtCellStr, which writes a series of character-attribute cell combinations from those stored in the cellstr array to create the multicolored prompt. Then it calls VioWrtNCell, which can replicate a single cell, to create the field and markers. Finally, it restores the cursor, sets it to the beginning of the field, and calls the KBD routine, KbdStringIn, to read the user's name. After hiding the cursor, it returns the number of characters read by KbdStringIn. When displayconfig returns from get_user_name, it knows how long the user's name is, but it does not have the name itself. So it calls VioReadCharStr to read the characters directly from the screen. Next, it retrieves the video configuration with VioGetConfig, prints that information on the screen, and returns to main. Background Colors The togglebackground function illustrates the use of two other VIO services: VioReadCellStr and VioWrtNAttr. The former reads one or more cells from the screen. The Background function, which is called by togglebackground, uses it to retrieve the attribute of the character in the upper-left-hand corner of the screen. It then increments or decrements the attribute value (depending on the argument passed to it) and calls VioWrtNAttr to flood the screen with this attribute. Thus, togglebackground can prompt the user to use the arrow keys to increment or decrement the background color. Note that exactly which color it uses the first time depends on the color of the character in the upper-left-hand corner. This may have been set by one of the chasing frames. The Esc key is used to terminate the togglebackground function and return to main. An additional message (discussed below) is printed, warning the user to press only the Esc key while in graphics mode. Generating Graphics The last facet of VIO that HELLO1.C illustrates is how to generate graphics and create a pop-up program under OS/2. The graphics function changes the video mode to CGA graphics with a 320 by 200 resolution and writes a graphical Hi in magenta on a white field. The function creates a separate thread of execution, which saves and restores the screen before and after a screen switch (see the sidebar, "Direct Screen I/O and Graphics under VIO"). Finally, it waits for the user to press the Esc key to terminate. If, however, the user presses any key (other than Alt-Esc or Ctrl-Esc), a pop up will be generated that informs the user to press the Esc key. The graphics function works by first retrieving the current video mode information and changing the video mode parameters. It sets them to indicate a video mode of non-monochrome graphics, with four colors, 40 by 25 characters, and 320 by 200 pixels. Then it calls VioSetMode to change the video mode, followed by VioGetPhysBuf to retrieve a selector for the CGA video buffer. It then locks the screen with a call to VioScrLock. The function continues by creating a new thread that executes the SaveRestoreScreen function discussed below. It calls clear_graphics_screen to clear the screen and writes a large Hi in the middle of the screen. The screen write uses two loops to traverse a bitmap of character 1s and 0s (stored in the hi_bitmap array) and calls the putpixel function to write a magenta pixel whenever a 1 is encountered in the bitmap. You can get an idea of what this will look like by staring at the hi_bitmap array at the beginning of the listing and unfocusing your eyes slightly. Last of all, graphics calls VioScrUnLock to unlock the screen and goes into a loop. In the loop it calls KbdCharIn to block on keyboard input. If the Esc key is pressed, the function breaks out of the loop and returns to main. Otherwise, it calls VioPopUp to create a blank 80 by 25 text mode screen, prints a message, and waits for a single keystroke. Upon receiving the keystroke, the function calls VioEndPopUp to end the pop up and loop back to KbdCharIn. The graphics function illustrates two important points: how to generate graphics under OS/2 and how to save and restore the screen in graphics mode. When you run the program, try some screen switches and press the wrong key to generate the pop up. You will find that the warning message (at the end of togglebackground) not to press anything but Esc during the graphics display is a taunt to trick the user into generating the pop up. The SaveRestoreScreen function is called by OS/2 each time the wrong key is pressed to save or restore the graphics image and mode. SaveRestoreScreen starts by calling VioGetMode to save the current video mode information. Following that, it goes into a loop and immediately calls VioSavRedrawWait. This call registers the thread with OS/2, causing the thread to block until a screen switch of some kind occurs. At this point, it doesn't matter whether the user switches sessions (using Alt-Esc), brings up the Session Manager (with Ctrl-Esc), or presses an inappropriate key that causes the graphics function to generate a pop up. If you want a screen switch to takes place, OS/2 will activate the SaveRestoreScreen thread by returning from VioSavRedrawWait. Once SaveRestoreScreen returns from VioSavRedrawWait, it quickly saves the screen by copying the contents of the video screen to a buffer. If a screen restore has to take place, the function restores the screen by resetting the video mode and copying the buffer contents back to the video screen. The graphics function uses two support functions. The first, clear_graphics_screen, simply floods the pixels in the video buffer with FFH, turning them white. The second, putpixel, converts pixel row-column coordinates into pixel offsets and stuffs the color bits passed into the appropriate part of a byte into the video buffer. Terminating the Program The termination function completes HELLO1.C. It resets the screen rows to the original number (restoring the original video mode at the same time), calls Screen to restore the screen to its original state, and calls Cursor to restore the cursor. Finally, it calls DosExit to terminate the program. Here's a summary of how to run the program. The program goes through five stages: flickering frames, chasing frames, the name prompt, the configuration display, and graphics. The flickering frames will run indefinitely until Esc is pressed. The chasing frames will continue until the frames are exhausted, but for best results, terminate them before the last frame makes its way to the center. Since a chasing frame normally appears before its predecessor moves off the top line, it should be easy to identify. The number of frames is set in the initialization function and will vary on different video hardware. Once you have entered your name at the name prompt, you can use the Up and Down arrow keys to change the background color. You can continue indefinitely changing the background until you press Esc. When you press Esc, the graphics display will terminate. If you press any other keys, it will generate the pop-up screen. Try recompiling the program with the SaveRestoreScreen thread commented out. The graphics screen will be trashed to some degree if you switch screens without this thread operating. Finally, you may find it interesting to alter the sleeptime variables in the program. Three of these can be altered on the command line. The command line defaults are HELLO1 1 1500 1 which places a minimum of 1 millisecond between the activation of each flickering frame, 1500 milliseconds (1.5 seconds) between each chasing frame, and a 1 millisecond pause when a chasing frame returns to the upper-right-hand corner of its circuit. The most interesting results occur when using 0 instead of 1 and a low number (like 100) instead of 1500. Keep in mind, however, that 0 will cause the program to steal a lot of CPU time from other processes. But it's fun to watch the results. This concludes our examination of the OS/2 VIO subsystem. Additional functions and services, which are not essential to a typical OS/2 application but should be mentioned, are listed and briefly discussed in Figure 8. You are now ready to tackle VIO. In fact, you can even write some sophisticated applications. With a good feeling for VIO, you'll find the rest of the OS/2 API no more difficult than gaining knowledge of how and when to use the different services. This will become even more apparent in the next article in this series, which switches from visual output to keyboard and mouse control, and will explore the KBD and MOU subsystems. Direct Screen I/O and Graphics Under VIO The OS/2 VIO subsystem does not offer any facilities for a process to access the physical screen directly. This can pose problems if you are porting DOS applications that perform direct screen writes or generate graphics or if you are writing an OS/2 application that generates graphics without the use of Presentation Manager (PM). Fortunately, OS/2 offers two indirect means for accessing the physical screen, depending on the needs of the application. Direct Screen Writes in Text Mode OS/2 systems provide a logical video buffer (LVB) for each screen group, and all VIO services update the LVB. When a screen group is in the background, screen updates can take place without disrupting the screen of the foreground process. When a screen group is in the foreground, OS/2 duplicates VIO updates of the LVB in the physical video buffer (PVB). You can easily modify a DOS program that performs text mode direct screen writes to run under OS/2. To do this, however, it's essential that you change the code that updates the PC's video buffer to write to the LVB instead. First, you must call VioGetBuf to retrieve the address of the LVB of your program's screen group. Since the format of the LVB is identical to a PC's text mode video buffer, the direct screen write code remains largely intact--only the destination changes. Note that there is no need to include code that checks for horizontal retrace or that manipulates the video hardware. OS/2 will take care of these details for you. When you are finished updating the LVB, your program should call VioShowBuf to update the PVB from the LVB. VioShowBuf can update all or a portion of the LVB to the PVB. This indirect screen write process remains the same whether your program is in the foreground or background: OS/2 will only honor calls to VioShowBuf when the program is in the foreground and will ignore such calls when a program is in the background. OS/2 will, however, update the PVB from the LVB once the program is switched to the foreground again. Note that although your program only needs to call VioGetBuf once, when it begins, your program can (and should) call VioShowBuf as often as is necessary. VioShowBuf will do nothing while a process is in a background screen group. This is not a problem since OS/2 will update the PVB upon a screen switch. I should mention that VioShowBuf is very fast. I modified the Magma Systems ME Editor to perform screen paging by writing to the LVB and occasionally calling VioShowBuf. This improved the speed of screen page updates and made the calls to VioShowBuf appear every bit as fast as direct screen writes under DOS. VIO function calls in a foreground process automatically update both buffers, since they notify OS/2 that the PVB must be updated. You can see this for yourself by writing a piece of code that first writes some text directly to the LVB, then calls a VIO service routine to write additional text, which overwrites part of the text written in the first step. This updates the LVB and causes OS/2 to update the PVB. Finally, have the code call VioShowBuf or manually switch the program into the background and back again. Until the PVB is updated, only the output of step 2 will appear on the screen. The text written in step 2 that overlays the text written in step 1 will, however, survive the screen update after the call to VioShowBuf. This also illustrates that, in text mode, the LVB always contains the current visual state of an application. Going Around VIO to Generate Graphics Although it's relatively simple to perform direct screen writes in text mode, it's not as easy to work with graphics. Part of the problem is that you can't use the LVB, since it's only available for direct screen writes in text mode. Therefore, if your program is going to generate graphic images, you'll need to access the PVB directly. Fortunately, VIO has a mechanism that allows an application to step around it and manipulate the PVB directly. This makes it easier to port graphics applications from DOS and enables you to write OS/2 graphics applications without the complexity of PM programming. There are a few caveats to be aware of when writing to the PVB. First, a routine can only access one adapter and video mode at a time. To support several adapters in a graphics routine, you'll have to write code that can address each, making the code somewhat hardware dependent. Next, images drawn in the PVB by an application are lost when a screen switch takes place. As I said earlier, you can't use the LVB in graphics mode. Further, there is no built-in mechanism for saving the contents of the PVB when a screen switch takes place. Since it's not built in, you'll have to provide this mechanism yourself. If your program manipulates any of the video hardware, you'll also have to save that information when a screen switch takes place. Finally, an application that accesses the physical screen must run in its own screen group, so it will never run in a PM window. When run in the PM environment, PM gives the program its own full-screen window. If you want it to run in a PM window, you'll have to rewrite the program to take advantage of the PM graphic facilities. With these warnings in mind, there are two problems to solve when writing a program that generates graphics in a VIO context. How do you use the mechanism for obtaining access and writing to the physical screen? And how do you provide a facility to save and restore the screen before and after a screen switch? The Direct Screen Write Process First, consider how to access and write to the physical screen buffer. Your program should call VioSetMode to ensure that the display is in the correct graphics mode. Then you must obtain the address of the PVB by calling VioGetPhysBuf, which will return a selector that corresponds to the address of the physical buffer. When you're ready to write to the buffer, you must use VioScrLock to lock the screen so a background application will not switch screens during the update. Next, your program should perform the direct screen write itself. This should be fast, efficient code, since a screen lock temporarily hangs the system and prevents a screen switch from taking place. Finally, your program must call VioScrUnLock to unlock the screen. Note that once you've retrieved a selector for the physical buffer address, the remaining steps must be taken every time your program is going to access the display hardware and write to the screen. These steps are essential to inhibit screen switches (which will disrupt the display) and to prevent other processes from writing to the display at the same time. VioScrLock is also a fast, effective way to determine whether or not your program is in the foreground, since the caller can either choose to block if the screen is not available or to return immediately. If the screen is unavailable, the program can do other processing until it is. Probably the most effective way to use VioScrLock is to place the direct screen write code in a function that's executed by a separate thread. You can use a semaphore to tell the thread when to perform a screen update, and the thread can block on VioScrLock until the screen can be accessed. This leaves the main thread free to continue with other work and lets you structure the code so that a graphics-intensive I/O can be written quickly, in its entirety, without a great deal of overhead. VioScrLock is so effective in locking the screen that nothing can switch the screen while it's locked. It protects the system from the application and protects the application from the system. Even pop ups (including the hard error handler) cannot be activated during a screen lock. As a safety valve, OS/2 will cancel the lock and perform a screen switch after 30 seconds if a program or the user has requested it. If your graphics application is still drawing an image when the screen switch takes place, the picture will be disrupted. Saving/Restoring the Video Display and State The second problem is to see how an application that manipulates the video hardware can save and restore the entire video state. The contents of the display buffer, the video mode, the palettes, the cursor, and so on, will all be lost if a screen switch takes place. VIO cannot perform this service for you, since a graphics bitmap might require a great deal of memory to save and restore the display. Further, some video hardware adapters have write-only registers that cannot be saved for later restoration--only your program will know whether or not it changed a video register. Thus, OS/2 can only notify a process when a screen switch is about to take place. It becomes the application's job to save and restore the screen when so notified. This is accomplished by creating a thread whose job it is to save and restore the screen. You can register the thread function with OS/2; then, just before the screen switch takes place, OS/2 will activate the thread. This lets you construct a thread to do one job and do it well--to save or restore the screen and video mode at the appropriate time. Thus, the thread's structure, minus the details of saving/restoring the display and video mode, is quite simple. The thread enters a loop and immediately calls VioSaveRedrawWait. This function will cause the thread to block until OS/2 indicates that a screen switch is about to take place. When this happens OS/2 will cause VioSaveRedrawWait to return, at which time the thread must perform the screen save or restoration. Returning to the top of the loop, the thread again calls VioSaveRedrawWait, which notifies OS/2 that the screen switch can continue. The thread will continue to block until the next screen switch for this process occurs. Note that upon its return, VioSaveRedrawWait will indicate whether a screen save or restore should transpire. And, as with VioScrLock, the system is vulnerable while the save/restore thread is active, so the thread function must be as efficient as possible. Finally, note that a similar function, VioModeWait, is provided if your program only needs to save the video mode and not the display. VioModeWait is used in the same fashion as VioSaveRedrawWait. Please keep in mind that the discussion presented here pertains to graphics applications that need to circumvent VIO or PM. It's not an issue with ordinary text applications. There's nothing to stop you from using VioGetPhysBuf to write to the screen in text mode, if you choose. The efficiency you gain will, however, be offset by the code overhead needed to save and restore the screen. That's why the VioGetBuf/VioShowBuf combination is provided for text applications. Investigating the Debugging Registers of the Intel 386 Microprocessor Marion Hansen and Nick Stuecklen The more complex a program and the microprocessor executing it, the bigger the task of debugging. In very complex systems, external debuggers simply cannot do the whole job. Fortunately, debugging sophisticated software created for the 386 hardware environment is made easier by the fact that the Intel(R) 386 microprocessor (hereafter referred to as mp) itself provides a substantial set of internal debugging support features. In this article we will take a look at these 386 features and explore how today's state-of-the-art debuggers use them. In the Past In the past, PC-based debuggers could only set code breakpoints, not data access breakpoints, and setting those code breakpoints was restricted to random access memory (RAM). Data monitoring utilities could be used, but they merely attach themselves to the timer interrupt and periodically examine and display the specified region of memory. Typically, a debugger worked by overwriting the first byte of the specified instruction, which preempted the ability to set breakpoints in read only memory (ROM). If you wished to set a breakpoint at an instruction, an old-style debugger usually saved the first byte of that instruction and then overwrote the byte with a 0CCH opcode (INT 3). When the microprocessor executed the 0CCH opcode, it generated an INT 3, and the debugger, which was monitoring INT 3, was invoked. The debugger displayed the breakpointed instruction and the register state, then waited for the next command. Today Fortunately the introduction of the 386 mp changed all that. With its six debug registers, the 386 mp provides built-in debugging support to let you set breakpoint addresses and define when breakpoints will occur. The four debug address registers are individually programmable and individually enabled or disabled through the debug control register. The debug status register maintains debug status information. In addition to supporting the usual break on instructions, the 386 mp also supports data access breakpoints. Data breakpoints are a very useful debugging tool (besides being an exceptional capability for a microprocessor). A data breakpoint occurs at the exact moment that data residing at a particular address is read or written. Using data breakpoints, you can immediately locate the instruction responsible for overwriting a data structure. The 386 mp lets you set breakpoint addresses in ROM as well as RAM. You can set a 386 debug exception when any of the following conditions occur: ■ an execution of the breakpoint instruction ■ an execution of every instruction (single-stepping) ■ an execution of any instruction at a given address ■ a read or write of a byte, word, or doubleword at any specified address ■ an attempt to change a debug register ■ a task switch to a specific task (protected mode only) Some Limitations Even with all the above capabilities, the 386 debug registers can't always do the job of hardware-assisted debuggers. For example, while many in-circuit emulators (ICEs) can maintain breakpoints across processor resets, a 386 mp reset clears the debug registers, effectively destroying any previously set breakpoints. Another limitation is that many ICEs can set breakpoints on I/O space accesses, but the 386 debug registers can't distinguish between memory and I/O space accesses. All data breakpoints are associated with the memory space only. You need an ICE to trap I/O space accesses, or you can use an I/O permission bitmap in virtual 8086 mode. You can use a hardware-assisted debugger such as an ICE to debug a debugger, but the only way to debug a 386-based debugger using the 386 debug registers is to ensure that the debugger uses only the INT 3 breakpoint. When triggered, the 386 debug registers generate an INT 1; therefore, a debugger relying only on INT 1 can debug a debugger relying only on INT 3. Further, because you need stable hardware to use the 386 debug registers, debugging in harsh, unknown system environments is easier with a full-scale ICE or other hardware-assisted debugger. System developers usually don't have the functional keyboard, display, disk, and operating system that is needed to load and run a debugger relying only on the 386 debug registers; they must rely on an ICE when debugging preliminary software and device drivers. You do, however, need more than just the 386 debug registers to create an acceptable software debugger. Programmers expect today's debuggers to provide recognition of symbolics, fast disassemblies, and the ability to handle multiple object module formats (OMFs). Another desirable feature is a Boolean comparison layer above the debug exception handler. Debuggers that take advantage of the 386 debug features are on the horizon. Combining the 386 debug features with what is available on today's debuggers will produce a powerful development tool. In the meantime, the example at the end of this article is part of a working program that exploits the 386 debug features to create a useful debugging tool. Built-in Features The 386 mp provides several built-in debugging features: ■ Four breakpoints. You can specify four addresses that the CPU will automatically monitor. ■ Arm or disarm the breakpoints. You can selectively enable and disable various debug conditions that are associated with the four debug addresses. ■ Data and instruction breakpoints. You can set breakpoints on data accesses as well as on instruction executions. ■ Singlestepping. You can step through the program one instruction at a time. Let's look at how the 386 debug features are implemented. Along the way, we will develop some debugger software that will amplify these features. Debug Registers The 386 mp has six registers to control debug features. Figure 1 shows the format of the debug registers. You can access them by using variants of the MOV instruction. A debug register can be either the source or the destination operand. In protected mode, the MOV instructions that access the debug registers can only be executed at privilege level zero. Trying to read or write the debug registers from any other privilege level causes a general protection exception. Under a protected mode operating system (such as OS/2 or XENIX(R) systems), the debug registers are considered privileged resources, and only privileged tasks can access them. Debug address registers (DR0-DR3). You can set a breakpoint address in each of the four debug address registers. Each register contains a linear address used to identify a breakpoint. (In contrast, addresses are presented as a segment/offset pair in real mode.) Debug control register (DR7). The debug control register lets you define, enable, and disable debug conditions. It specifies the conditions under which the 386 recognizes a breakpoint, the breakpoint type (local or global), and the breakpoint length field. The low order 8 bits of DR7 enable or disable the four breakpoints. Each breakpoint has 2 enable bits. The L bit enables local breakpoints, and the G bit enables global ones. The real mode debugging program shown later in this article sets and clears both bits. For each address in register DR0 through DR3, the corresponding fields R/W0 through R/W3 (in register DR7) specify the type of action that can cause a breakpoint. The 386 interprets these bits as follows: Bits Meaning 00 break on instruction execution only 01 break on data writes only 10 undefined 11 break on data reads and writes but not on instruction fetches The breakpoint length field (LENn) is associated primarily with data breakpoints. The length of a data breakpoint can be a byte, a word, or a doubleword. Specifying only the starting address of a data item is not enough to match a breakpoint condition because data items can be of three different lengths (8, 16, and 32 bits). The length field adds flexibility by selecting a range of address accesses which can trigger a breakpoint. You can specify the following lengths: Bits Meaning 00 1-byte length 01 2-byte length (word) 10 undefined 11 4-byte length (doubleword) Because instruction breakpoints should uniquely specify the byte-granular starting address of the intended instruction, instruction breakpoints always specify a length field of 1 byte. If RWn is 00--an instruction execution breakpoint--LENn should also be 00 (1 byte). Any other length is undefined. When the LE (local) or GE (global) bit is set, the 386 mp slows execution so data breakpoints can be reported on the precise instruction that caused them. Keep in mind that while an instruction execution breakpoint occurs before the specified instruction is executed, a data access (read or write) breakpoint occurs after the specified data is read or written. Debug status register (DR6). The debugger uses the debug status register to determine what debug conditions have occurred. The 386 mp sets status bits, and the debugger reads them. When the microprocessor detects a debug exception, it sets one or more of the status register's 4 low-order bits, B0 through B3, before entering the debug exception handler. Bn is set if the condition described in the address registers (DRn) and the control register (LENn and R/Wn) occurs. The BS bit, associated with the TF (trap flag) bit of the 386 EFLAGS register, is set if the debug handler is entered, since a single-step exception occurred. The single-step trap is the highest-priority debug exception. When BS is set, any of the other debug status bits may also have been set by the 386 mp. This means that a single-step trap can occur at the same time an instruction or data breakpoint occurs. The BT and BS bits are used only when the microprocessor is in protected mode. The BT bit is associated with the T bit (debug trap bit) of the task state segment, or TSS. The TSS is a data structure defined by the 386 system architecture; it is available only in protected mode. Each task has its own TSS, which holds the state of the task's virtual processor. The 386 mp sets the BT bit before entering the debug handler if a task switch has occurred and if the T bit of the new TSS is set. There is no corresponding bit in DR7 that enables and disables this trap; the T bit of the TSS is the only enabling bit. The ICE-386 is Intel's in-circuit emulator for the 386 mp. When the ICE-386 is attached, it has priority over the 386 debugger. The BD bit is set if the next instruction will read or write one of the debug registers and ICE-386 is also using the debug registers. The 386 mp only clears the bits of DR6 when the microprocessor is reset. To avoid confusion in identifying the cause of the next debug exception, the debug handler should move zeros to DR6 immediately before returning. Setting Breakpoints Each of the four breakpoints is defined by its linear address (DRn) and its length (LENn). The LEN field lets you specify a 1-, 2-, or 4-byte field. The 386 mp requires that 2-byte fields be aligned on word boundaries (addresses that are multiples of two), and 4-byte fields must be aligned on doubleword boundaries (addresses that are multiples of four). You will get unexpected results if code or data breakpoint addresses are not properly aligned. You can set a data breakpoint for a misaligned field longer than 1 byte by using two or more debug address registers to hold the entire address. Each entry must be properly aligned, and the entries must span the length of the field. For example, when setting three breakpoints for a doubleword address that starts on an odd-byte boundary, the first address identifies the first byte of the doubleword, the second one identifies the next two bytes, and the third identifies the last byte. Figure 2 shows you how to align breakpoint addresses for the address of a doubleword starting on an odd-byte boundary. A memory access triggers a data read or write breakpoint when it occurs within a defined breakpoint field (as determined by a breakpoint address register and its corresponding LEN field). Figure 3 contains examples showing when breakpoints will and will not occur. Instruction breakpoint addresses are always specified as 1 byte (LEN=00). Other values for instruction breakpoint addresses are undefined. The 386 recognizes an instruction breakpoint only when the breakpoint address points to the first byte of an instruction. Therefore, if the instruction has prefixes, then the breakpoint address must point to the first prefix byte. Debug Exceptions Breakpoints set on instructions cause faults; all other debug conditions cause traps. (Faults break before executing the instruction at the specified address. Traps report a data access breakpoint after executing the instruction that accesses the given memory item.) The debug exception can report faults and traps at the same time. The following describes the four classes of debug exceptions. Instruction execution breakpoint. An instruction execution breakpoint is a fault, so the 386 mp reports an instruction execution breakpoint before it executes the instruction at the given address. Data access breakpoint. A data access breakpoint exception is a trap. That is, the processor reports a data access breakpoint after executing the instruction that accesses the given memory item. When using data breakpoints, you should set the DR7's LE bit, GE bit, or both. If either LE or GE is set, any data breakpoint trap is reported exactly after completion of the instruction that accessed the specified memory item. This exact reporting is accomplished by forcing the 386 execution unit to wait for completion of data operand transfers before beginning execution of the next instruction. If neither GE nor LE is set, data breakpoints may not be reported until one instruction after the data is accessed or they may not be reported at all. This is because the 386 mp normally overlaps instruction execution with memory transfers to such a degree that execution of the next instruction may begin before memory transfers for the previous instruction are completed. If a debugger is creating a data write breakpoint, it should save the original data contents before setting the breakpoint. Because data breakpoints are traps, a write into a breakpoint location is completed before the trap condition is reported. The handler can report the saved (original) value after the breakpoint has been triggered. The data in the debug registers can be used to address the new value written by the instruction that triggered the breakpoint. Single-step trap. This debug condition occurs at the end of an instruction if the trap flag (TF) of the flag's register held the value 1 at the beginning of that instruction. Note that the exception does not occur at the end of an instruction that sets TF. For example, if POPF is used to set TF, a single-step trap does not occur until after the instruction that follows POPF. The interrupt priorities in hardware guarantee that if an external interrupt occurs, single stepping stops. When an external and a single-step interrupt occur together, the single-step interrupt is processed first. This clears the TF bit. After saving the return address or the switch tasks, the external interrupt input is examined before the first instruction of the single-step handler executes. If the external interrupt is still pending, it is then serviced. The external interrupt handler is not single stepped. In order to single step an external interrupt handler, single step an INT n instruction that refers to the interrupt handler. Task switch breakpoint. In protected mode, a breakpoint occurs after switching to a new task if the new TSS's T bit is set. The breakpoint occurs after control passes to the new task but before the first instruction is executed. The exception handler can detect a task switch by examining the BT bit of the debug status register (DR6). Interrupts Both the 386 and earlier microprocessors have two interrupt vectors dedicated to debugging. Interrupt 1 is reserved for the single-step instruction trap, interrupt 3 for instruction breakpoints. In addition, the 386 mp generates an interrupt 1 on any debug register trigger. Interrupt 1. The handler for interrupt 1 is usually a debugger or part of a debugging system. Figure 4 shows the seven breakpoint conditions that can cause an interrupt 1. The debugger can check flags in DR6 and DR7 to determine what condition caused the exception and what other conditions might have occurred. Interrupt 3. This exception is caused by execution of the breakpoint instruction INT 3. Typically, a pre-386 debugger prepared a breakpoint by substituting the opcode of the 1-byte breakpoint instruction for the first opcode byte of the instruction to be trapped. Prior to the 386 machine, microprocessors used the breakpoint exception extensively for trapping execution of specific instructions. The 386 mp solves this need more conveniently by using the debug registers and interrupt 1. However, the breakpoint exception is still useful for debugging debuggers because the breakpoint exception can vector to an exception handler that is different from that used by the debugger. The breakpoint exception can also be useful when you need to set more breakpoints than allowed by the four debug registers. Sample Debugger Program You don't need to scrap your old-style debugger to take advantage of the 386's built-in debugging features. With a little help from a friendly debugging assistant (such as the working program fragment shown later in this article), you can continue to use your current debugger. By enabling or disabling data breakpoints with a pop-up routine, the debugging assistant shown supplements the setting of instruction breakpoints by old-style debuggers. You still have all the benefits of your older debugger (symbolics and disassemblies, for example) plus the powerful built-in debugging capabilities of the 386 mp. The 386-based debugging assistant runs in the background as an adjunct to a traditional, pre-386 debugger. The debugging assistant is a terminate-and-stay-resident (TSR) program that pops up either when you press the SYSREQ key or when a debug exception occurs. The pop-up screen describes the 386 registers and identifies which of the four individually programmable breakpoints occurred. Our debugging assistant has several features built into it. A human interface displays all the registers you're interested in and lets you easily enter breakpoint addresses, enable/disable breakpoints, and define breakpoint conditions. Figure 5 shows a model of this screen, which is divided into the following five option fields: ■ BREAKPOINT contains the four 386 debug registers and their options. ■ COMPARE shows the Boolean comparison options that are available. ■ SPECIAL REGS contains the 386 EFLAGS register in its hexadecimal representation, the debug control register (DR7), and the debug status register (DR6). ■ REGISTER SET displays the values in the most frequently referenced 386 registers. ■ EFLAGS presents the EFLAGS register decoded into an English format. You can change most of the values displayed on the screen by either editing or toggling each respective field. A Boolean comparison layer decides if the exception meets the criteria you specified. The debugging assistant has Boolean comparison logic that is armed and disarmed separately by toggling the switch (sw) field in COMPARE. You can also toggle the Boolean (bool) field in COMPARE between: < less than < = less than or equal to = equal to < > not equal to > greater than > = greater than or equal to The value to be used in the comparison logic is specified by editing the value field in COMPARE. An interrupt service routine handles exceptions. The debugging assistant program has two interrupt service routines (ISRs). One ISR handles the SYSREQ key and the other handles debug exception interrupts. Part of the code in Figure 6 contains an interrupt service routine. Common code is called by the interrupt service routines. In the debugging assistant program, both ISRs call a common block of code that copies the 386 registers into a large data structure. The common code then passes the address of the data structure to the high-level language program that controls the human interface. After the registers are modified (through the human interface), the high-level language program returns control to the common code. The common code then copies the edited register images back into the 386 registers and "goes to sleep" until the next debug exception or until the SYSREQ key is pressed. Figure 6 contains the common code, written in assembly language. Some of the assembly language mnemonics may be unfamiliar. Because of the absence of 386 assemblers, we hand-assembled certain 386-specific instructions. Most of them were relatively easy to code because they only needed a prefix byte. The prefix byte specifies that the following instruction will use 32-bit operands, rather than 16-bit pre-386 instructions. For example, inserting the prefix byte 066h ahead of mov ax,bx forms an instruction which the 386 mp interprets as: mov eax,ebx Figure 7 shows the flow of the debugging assistant program. A thorough understanding of the 386 debug support features presented in this article will provide a good starting point for tackling difficult debugging chores. It makes it possible to build special debugging utilities that focus on specific problems that might not be handled by a generic debugger. We are not, of course, suggesting that the reader should actually build a debugger. But under certain circumstances a debugging assistant such as the one presented here will decrease debugging time plus give you the satisfaction of both fully exploiting the debugging capabilities and better understanding the internal workings of the most advanced microprocessor on the market. Strategies for Building and Using OS/2 Run-Time Dynamic-Link Libraries Ross M. Greenberg Your process has died. Later, when you run the BACKTRACE function in your debug DLL, you find that you TOOK the ax with a strncmp but tried to THROW with a strcmp. And with a quick correction the bug is history. A Debug DLL What is a debug DLL anyway, and how can you create one? The Dynamic-Link Library (DLL) capabilities of OS/2 systems actually come in two "flavors." The first, called static linking, was described fully in "Design Concepts and Considerations in Building an OS/2 Dynamic-Link Library," MSJ (Vol. 3, No. 3). Briefly stated, the static linking implementation of DLLs lets an EXE function be called into memory from another file, a DLL. When the EXE program loads, the necessary functions are loaded into memory from the DLL, linkage references to the functions are resolved, and the EXE starts to run. Only functions referenced at link time are loaded into memory, and the operating system can discard and reload them as needed. Importantly, only one copy of any given function needs to be in memory and it is then shared by all sessions requiring it. There are various advantages to using the Dynamic-Link capabilities of OS/2 systems when developing code: smaller run times; the ability to truly share a single copy of your developed routines between programs (when properly configured, only a single copy of your routines will get loaded into memory regardless of how many processes use them); the ability to distribute different versions of your program packaged with different DLLs to serve different markets; and so on. DLLs were such a good concept that a majority of OS/2 is, in reality, a collection of DLLs. Different OEMs, when porting OS/2 over to their own unique hardware environment, need only replace individual DLLs as they continue with their port effort. Each software author will most likely find a different aspect of the DLL concept that fits their needs; DLLs are that flexible. Although a little difficult to use initially, I've found that most of my OS/2 code uses DLLs more and more--and, more and more, it uses routines already written as I create my own libraries, with a resolution granularity that's definable on a routine-by-routine basis. However, loading a DLL like this doesn't let you load functions as you might wish--and all functions you reference in the link stage are loaded into memory. If you happen to have a very large, infrequently used function, it will still be read from disk, loaded into memory, and later (perhaps) discarded by the operating system if there's a memory crunch. Dynamic Linking OS/2 provides a second flavor to its Dynamic-Link Library Package: the ability to have "run-time linking." In essence, this lets you load functions (called procedures in DLL parlance) as needed. And when necessary, you can (in essence) discard them just as easily. You can even mix the two different types of DLLs, so that the operating system takes care of the "usual" cases and you take care of the unusual ones. Necessary Functions There are only five new function calls you need to learn to take advantage of this feature. But there are several strategies you will need to consider as you create and build your DLLs. The five new functions are: DosLoadModule DosGetProcAddr DosFreeModule DosGet ModHandle DosGetModName In discussing these functions I'm going to begin using the C notation provided in the most recent set of manuals from the Microsoft(R) Software Development Kit (SDK); assembly language programmers, be forewarned. The new definitions and typedefs allow for more machine independent code, although they might be a bit difficult to understand at first. A bit of sound advice here: it is essential that the very first thing you do is to make a complete hard-copy list of all the OS/2 header files. Study them now and you will save yourself lots of digging for the information later. Future versions of OS/2 systems will most likely allow the use of different addressing schemes, so machine independence is actually a valuable concept, even at this stage. To begin with we have DosLoadModule, defined (prototyped) in BSEDOS.H: USHORT DosLoadModule( bomb_ptr, bomb_ptr_len, mod_name_ptr, mod_handle_ptr) PSZ bomb_ptr; USHORT bomb_ptr_len; PSZ mod_name_ptr; PHMODULE mod_handle_ptr; The bomb_ptr field is a character array, of length bomb_ptr_len. The operating system will fill in this array with the null-terminated name of the DLL file that caused the error in loading. Normally this null-terminated string would be equivalent to the actual module name, mod_name_ptr. But since a DLL can call other DLLs, there is a possibility that some DLL further down the "tree" might have caused the problem. If the function returns a zero, it loaded successfully. If the module was already loaded by some other process, the reference count on the module is incremented. The module will remain loaded as long as the reference count is greater than zero. Only segments of the DLL marked as "preload" in the DLL's DEF file will be automatically loaded. The module name itself must be the name of a DLL in your LIBPATH or the function will fail. The module handle must be preserved for all other calls of the run-time load package; think of it as a file handle if you wish. Once the module can be referenced by a handle, routines within the DLL can now be accessed--if their address is known. USHORT DosGetProcAddr( mod_handle, function_name, function_ptr) HMODULE mod_handle; PSZ function_name; PPFN function_ptr; This function will return the address in the DLL (specified in mod_handle) of the named function into the function pointer, function_addr. Once returned, it may be called via dereferencing as with any other function pointer. Functions with the same name can be referenced in different DLLs since the handle to the DLL itself would be different. The name of the function, however, can also be an ordinal number. Each function within a DLL can have a public name associated with it. For efficiency, you can remove the function names from the DLLs and only allow reference through the ordinal number. As an example, DOSCALLS.DLL does not have the function names readily available, and functions contained therein must be done by ordinal number. An ordinal number is of the form "#xyz"--it must start with a pound sign, and the remaining part of the string (that is, xyz) is the ASCII representation of the ordinal number of the function you wish to reference. OS/2 does not provide a means of discarding a function you no longer have any use for. But the operating system will discard the function if it needs the memory the function occupies. If you no longer reference the function, then it will not be reloaded into memory. On the other hand, when you no longer need a DLL module, you may discard it. USHORT DosFreeModule( mod_handle) HMODULE mod_handle; This will decrement the reference count of the module itself. If the reference count returns to zero, then all memory allocated to the module will be deallocated and discarded as required. Even if the reference count of the module is not zero, however, the local process's references to the DLL are no longer valid; calls to function addresses returned by DosGetProcAddr will fail with a protection violation. When a program exits, it returns all of its resources to the operating system, so it is not necessary to call DosFreeModule when you exit your program--although it is good programming practice. Two complementary functions exist, for determining whether the process has already loaded a module and for determining the name of a module based on its handle. USHORT DosGetModHandle( mod_ptr, mod_handle_ptr) PSZ mod_ptr; PHMODULE mod_handle_ptr; This function will return the handle associated with a named DLL module. It's useful if you want to double-check whether a given module is already loaded. Since the handle itself is local to a given process, if you need to use this call, you should double-check your error processing--how could you forget a handle? There are some specific reasons for using this function in a large program. Potentially, some other member of your programming team could have released the module, and then reloaded it later. This would usually cause it to get a different handle, which might cause you a problem if the handle is in a local, static variable. You can find a way around that problem in the debug routine in the sample program. DosGetModName, which is the best function, is defined as follows: USHORT DosGetModName( mod_handle, name_len, name_ptr) HMODULE mod_handle; USHORT name_len; PCHAR name_ptr; This function simply returns the DLL name associated with a given handle into the character array of name_ptr. If the name of the DLL can't fit into the character array, then an error condition is returned. This is useful in determining the name of a currently loaded DLL. Again, take a look at the debug function in the sample program for an idea of how to use it. DLLs Call DLLs One of the more interesting aspects of DLLs has largely been ignored until now; the ability of a DLL to utilize another DLL almost infinitely. I use this technique as an efficient way of dealing with two distinct DLLs utilized via the run-time load mechanisms. There is a decent enough reason for it. You needn't change any code in an applications program to take advantage of things like extended commands and functions; it becomes transparent where the actual DLL "lives," and the total separation of the two DLLs--through a third, common DLL--allows easy modification of one without having to recompile or maintain the other. FNDFILE The simple application I use to demonstrate the run-time loading capabilities of OS/2, FNDFILE (see Figures 1, 2, and 3), uses the OS/2 function call, DosFindFirst. This function lets you retrieve structures containing information about files with names matching a given pattern. You can retrieve information on as many files matching the pattern as you like, so long as you provide a buffer large enough to hold the returned information. One interesting little quirk in DosFindFirst is that--although it is documented to fill a buffer with the contents of a structure, called FILEFINDBUF--it cheats a little. Basically the structure is a bunch of dates and times, a couple of longs (to indicate filesize), a character count, and an array of 13 characters, which holds the filename. The character array contains only enough bytes to satisfy the need for a null-terminated string. The next structure starts immediately after the null byte, so a simple pointer-to-structure increment doesn't work, since that increments the pointer by exactly one sizeof(FILEFINDBUF). Therefore, when examining the code in the show routine, please forgive the ugliness of the increment to the pointer. An interesting concept used in OS/2 is that of a "directory-search handle." This lets you consider the DosFindFirst function a resource which can be allocated and deallocated like any other resource. For example you could set up a number of initial requests to find a different matching filename pattern (say, the same pattern in different directories) and to then call functions, passing the directory-search handle as a parameter, which in turn call DosFindNext using those handles to find the next file on a pattern-by-pattern basis. Calling DosFindFirst with a directory-search handle already in use causes closing and then reuse of that handle. Other parameters in the DosFindFirst function let you include matching "special" files in your request, including directories, hidden files, read-only files, etc. By examining the returned attribute for each of the files, you can determine what type of special file you're currently examining. The DosFindFirst function is useful enough that most of us will have some need of it. Yet it's also sufficiently complex that we'll all make mistakes when we first use it. Additionally it makes a perfect test case for a debug DLL. The structure of the FNDFILE program is a bit convoluted, so some explanation is in order. The main routine lets you enter the parameters of your choice via a simple keyword parsing if-then-else clause. If you enter the "GO" command, you call the do_find function. Other "action" keywords let you turn debugging on or off, show your current parameter list, and show the current return buffer contents. The do_find function is called with parameters passed exactly as DosFindFirst expects them; it makes it easier to do the call later. When the find program is first invoked, the debug function is called with the flag turned off, and debug itself (in FNDFILE3) loads the normal DosFindFirst function from the appropriate DLL, DOSCALLS. You cannot, however, just load the DosFindFirst function as you'd wish. The function name isn't immediately accessible from the DOSCALLS library. Since the name is "private," I was forced to do a hex dump of the OS2.LIB file (or the DOSCALLS.LIB file, depending on your version of OS/2) and to find the so-called "ordinal number" of the DosFindFirst function. An ordinal number is basically a simple numeric representation of the function number itself in the library, and is a more efficient way of calling important functions than forcing the kernel to do a strncmp on a function name. The hex dump (see Figure 4) shows 64 as the ordinal number for DosFindFirst. To indicate that you're using an ordinal number, you must specify the number as an ASCII null-terminated string, with a '#' prefixed to it as the function name in the DosLoadProcAddr function. Before you can call the DosLoadProcAddr function, the appropriate DLL must be opened up and a handle, returned by the operating system, preserved for future reference. Use of the DosGetModHandle checks whether the program has already loaded the DLL; if so, DosLoadModule, the function which returns the handle address, is not called. If, however, the DLL is not open, this implies that the other DLL is open (for all calls to debug but the first). Therefore a call is made to DosGetModName, simply to show the name of the DLL already loaded, and the open DLL is closed via the DosFreeModule call. The DosLoadProcAddr function returns the actual address of the requested function, whether called with a function name (which should be in uppercase) or an ASCII-based ordinal number. Simply by calling this function via the normal C mechanisms, you can call any one of a number of functions, as long as the parameters match. One of the functions with matching parameters is the do_list function. This is in FNDFILE2 (see Figure 2). This function simply prints out all of your parameters, saving the ones the call might change, then calls the real DosFindFirst function. This implies that the function is already loaded when you first invoke the FNDFILE program. If you want to avoid this "preloading," you can also have the do_list function call all the appropriate run-time load functions for its own copy of the DosFindFirst pointer--but this is only demonstration code, in any case. The call to DosFindFirst is done simply to get back the status code, which is expected and will be processed by the do_find function. The number of files to be found on subsequent calls is reset, as is the original handle. The important thing here to realize is that the do_find function calls either do_list or DosFindFirst without really knowing which. So in a transparent manner, you can create your program with full run-time loading of your difficult functions, debug them completely, then let the actual programming effort continue. An obvious extension of this technique would let you provide your end-users with a full debug DLL of all your functions and all the OS/2 system calls you use. Then when you get that inevitable tech support phone call, you can just have the user turn on debug mode and modem you a copy of the voluminous output the debug DLL produces. Run-time loading is one of the treasures in OS/2 which, with enough exploration, you'll find extraordinarily valuable as you produce some of the larger programs OS/2 makes possible. In fact, you can prompt for a DLL to be loaded, and load it on the spot! The name doesn't have to be in the code. Even the simple debug facility demonstrated here dwarfs the capabilities of other debugging methods in other operating systems. Figure 1 [FNDFILE.C] #define INCL_DOS #define INCL_ERRORS #include <os2.h> #include <stdio.h> #include <stdlib.h> #include <string.h> USHORT far _loadds pascal debug(USHORT); USHORT far _loadds pascal do_find(PSZ, PHDIR, USHORT, PFILEFINDBUF, USHORT, PUSHORT, ULONG); USHORT far _loadds pascal do_list(PSZ, PHDIR, USHORT, PFILEFINDBUF, USHORT, PUSHORT, ULONG); void do_usage(void); void main(void); void show(PFILEFINDBUF f_ptr, USHORT cnt) { while (cnt--) { printf("Len is: %2d. Filename is:%s\n", (int)(f_ptr->cchName), f_ptr->achName ); f_ptr = (PFILEFINDBUF) ((char far *)f_ptr + 24 + (int)(f_ptr->cchName)); } } void do_usage() { printf("\n\nexit - to exit\n"); printf("file=<filename> - enter name of message file\n"); printf("hand=<val> - 1 = default, ffff = create new\handle\n"); printf("attrb=<attrb> - Attributes: see pg 98 of Ref Manual\n"); printf("len=<buf_len> - length of output buffer to\ use.Allocated\n"); printf("cnt=<cnt> - Maximum number of files to return\n"); printf("debug=on|off - turn debug mode on or off\n"); printf("show - show buffer contents...\n"); printf("list - show current parameter settings\n"); printf("go - call DosFindFirst()\n"); } void main() { CHAR tmp_buf[256]; CHAR name[64]; USHORT max_len = 0; char *buf_ptr = (char *)NULL; HDIR handle = 1; USHORT attrb = 0; USHORT num_files = 0; if(!debug(FALSE)) { printf("Error in initial call to debug\n"); exit(1); } *name = (CHAR)NULL; while(TRUE) { printf(">"); gets(tmp_buf); strlwr(tmp_buf); if(!strncmp(tmp_buf, "exit", 4)) exit(1); else if(!strncmp(tmp_buf, "file=", 5)) strcpy(name, tmp_buf + 5); else if(!strncmp(tmp_buf, "len=", 4)) { if(buf_ptr) free(buf_ptr); max_len = atoi(tmp_buf + 4); buf_ptr = calloc(max_len, 1); } else if(!strncmp(tmp_buf, "cnt=", 4)) num_files = atoi(tmp_buf + 4); else if(!strncmp(tmp_buf, "hand=", 5)) sscanf(tmp_buf + 5, "%x", &handle); else if(!strncmp(tmp_buf, "attrb=", 6)) sscanf(tmp_buf + 6, "%x", &attrb); else if(!strncmp(tmp_buf, "list", 4)) do_list(name, (PHDIR)&handle, attrb, (PFILEFINDBUF)buf_ptr, max_len, (PUSHORT)&num_files, (ULONG)0); else if(!strncmp(tmp_buf, "debug=", 6)) { if(!debug(!strncmp(tmp_buf + 6, "on", 2))) { printf("Error in subsequent call to debug\n"); exit(1); } } else if(!strncmp(tmp_buf, "show", 4)) show((PFILEFINDBUF)buf_ptr, num_files); else if(!strncmp(tmp_buf, "go", 2)) do_find(name, (PHDIR)&handle, attrb, (PFILEFINDBUF)buf_ptr, max_len, (PUSHORT)&num_files, (ULONG)0); else if(*tmp_buf == '?') do_usage(); else printf("?Huh?\n"); } } [FNDFILE.DEF] IMPORTS FNDFILE3.do_find IMPORTS FNDFILE3.debug IMPORTS FNDFILE2.do_list Figure 2 [FNDFILE2.C] #define INCL_BASE #define INCL_ERRORS #define INCL_DOS #include <os2.h> #include <stdio.h> #include <stdlib.h> #include <string.h> void show(PFILEFINDBUF f_ptr, USHORT cnt); USHORT far _loadds pascal do_list(PSZ name_ptr, PHDIR hptr, USHORT attrb, PFILEFINDBUF buf_ptr, USHORT buf_len, PUSHORT num_ptr, ULONG reserved) { USHORT stat; HDIR save_handle = *hptr; USHORT save_num = *num_ptr; printf("File name: %s\n", name_ptr); printf("Buffer address is %lx\n", buf_ptr); printf("Buffer length: %d\n", buf_len); printf("File count: %d, address: %ld\n", *num_ptr, num_ptr); printf("Handle is: %d\n", *hptr); printf("Attribute: %d\n", attrb); stat = DosFindFirst(name_ptr, hptr, attrb, buf_ptr, buf_len, num_ptr, reserved); printf("Return status was:%d, cnt was:%d\n", stat, *num_ptr); *hptr = save_handle; *num_ptr = save_num; return(stat); } [FNDFILE2.DEF] LIBRARY FNDFILE2 EXPORTS do_list Figure 3 [FNDFILE3.C] #define INCL_BASE #define INCL_ERRORS #define INCL_DOS #include <os2.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #define FAIL_BUF_LEN 128 PSZ mod_name[] = {"doscalls", "fndfile2"}; PSZ routine_name[] = {"#64", "DO_LIST"}; USHORT (pascal far *routine)(PSZ, PHDIR, USHORT, PFILEFINDBUF, USHORT, PUSHORT, ULONG); USHORT far _loadds pascal do_find(PSZ name_ptr, PHDIR hptr, USHORT attrb, PFILEFINDBUF buf_ptr, USHORT buf_len, PUSHORT num_ptr, ULONG reserved) { USHORT stat; if(!(stat = routine(name_ptr, hptr, attrb, buf_ptr, buf_len, num_ptr, reserved))) { printf("Good return. Files found = %d\n", *num_ptr); } else { switch(stat) { case ERROR_BUFFER_OVERFLOW: printf("Buffer Overflow - Increase Buffer Size\n"); break; case ERROR_DRIVE_LOCKED: printf("Drive Locked\n"); break; case ERROR_FILE_NOT_FOUND: printf("File: %s not found\n", name_ptr); break; case ERROR_INVALID_HANDLE: printf("Invalid handle: %d\n", *hptr); break; case ERROR_INVALID_PARAMETER: printf("Invalid Parameter\n"); break; case ERROR_NO_MORE_FILES: printf("Ran out of files\n"); break; case ERROR_NO_MORE_SEARCH_HANDLES: printf("Can;t allocate another Search Handle\n"); break; case ERROR_NOT_DOS_DISK: printf("Not a DOS Disk\n"); break; case ERROR_PATH_NOT_FOUND: printf("I can't locate that Path\n"); break; default: printf("Unknown error in FindFirst: %d\n", stat); break; } } return(stat); } far _loadds pascal debug(USHORT debug_flag) { USHORT stat; CHAR fail_buf[FAIL_BUF_LEN]; static HMODULE handle = 0; HMODULE tmp_handle; CHAR tmp_buf[128]; printf("Debug is: %s\n", debug_flag ? "On" : "Off"); /* already a DLL loaded? */ if(handle) { /* some DLL already loaded. Requested DLL? */ stat = DosGetModHandle(mod_name[debug_flag], &tmp_handle); /* if error, or a handle mismatch, then it isn't * the requested DLL */ if(stat || tmp_handle != handle) { /* Get name of the DLL currently loaded */ if(stat = DosGetModName(handle, 128, tmp_buf)) { printf("Couldn't retrieve loaded DLL Name. Error code is: %d\n", stat); return(FALSE); } else printf("Currently Loaded DLL is: %s\n", tmp_buf); /* free the already loaded module, whatever * it is */ DosFreeModule(handle); } else { /* current handle is for requested DLL. * Simply return */ printf("DLL (%s) already loaded\n", mod_name[debug_flag]); return(TRUE); } } /* wrong DLL is now freed */ /* try to load the requested DLL, and get the entry * points */ if(stat = DosLoadModule(fail_buf, FAIL_BUF_LEN, mod_name[debug_flag], &handle)) { printf("Couldn't load: %s (stat is :%x)\n", mod_name[debug_flag], stat); printf("DLL problem was in: %s\n", fail_buf); return(FALSE); } else printf("Module handle is: %d\n", handle); /* Now get the entry point for the requested routine */ if (stat = DosGetProcAddr(handle, routine_name[debug_flag], &routine)) { printf("Couldn't get routine: %s (stat is :%d)\n", routine_name[debug_flag], stat); return(FALSE); } else printf("Routine address is: %lx\n", routine); /* module loaded, entry point returned, so we return */ return(TRUE); } [FNDFILE3.DEF] LIBRARY FNDFILE3 EXPORTS do_find EXPORTS debug IMPORTS FNDFILE2.do_list Figure 4 Partial Dump of \PMSDK\LIB\OS2.LIB 0FC0: 00 00 8A 02 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0FD0: 80 0F 00 0D 44 4F 53 43 41 4C 4C 53 30 30 30 36 ....DOSCALLS0006 0FE0: 33 16 88 1D 00 00 A0 01 01 0C 44 4F 53 46 49 4E 3.........DOSFIN 0FF0: 44 46 49 52 53 54 08 44 4F 53 43 41 4C 4C 53 40 DFIRST.DOSCALLS@ 1000: 00 00 8A 02 00 00 00 00 00 00 00 00 00 00 00 00 ................ 1010: 80 0F 00 0D 44 4F 53 43 41 4C 4C 53 30 30 30 36 ....DOSCALLS0006 1020: 34 15 88 1C 00 00 A0 01 01 0B 44 4F 53 46 49 4E 4.........DOSFIN 1030: 44 4E 45 58 54 08 44 4F 53 43 41 4C 4C 53 41 00 DNEXT.DOSCALLSA. 1040: 00 8A 02 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ How the 8259A Programmable Interrupt Controller Manages External I/O Devices Jim Kyle and Chip Rabinowitz Unlike software interrupts, which are service requests initiated by a program, hardware interrupts occur in response to electrical signals received from a peripheral device such as a serial port or a disk controller, or they are generated internally by the microprocessor itself. Hardware interrupts, whether external or internal to the microprocessor, are given prioritized servicing by the Intel(R) CPU architecture. The 8086 family of microprocessors (which includes the 8088, 8086, 80186, 80286, and 80386) reserves the first 1024 bytes of memory (addresses 0000:0000H through 0000:03FFH) for a table of 256 interrupt vectors, each a 4-byte far pointer to a specific interrupt service routine (ISR) that is carried out when the corresponding interrupt is processed. The design of the 8086 family requires certain of these interrupt vectors to be used for specific functions (see Figure 1). Although Intel actually reserves the first 32 interrupts, IBM, in the original PC, redefined usage of Interrupts 05H to 1FH. Most, but not all, of these reserved vectors are used by software, rather than hardware, interrupts; the redefined IBM uses are listed in Figure 2. Nestled in the middle of Figure 2 are the eight hardware interrupt vectors (08-0FH) IBM implemented in the original PC design. These eight vectors provide the maskable interrupts for the IBM(R) PC-family and close compatibles. Additional IRQ lines built into the IBM PC/AT(R) are discussed under The IRQ Levels below. The conflicting uses of the interrupts listed in Figures 1 and 2 have created compatibility problems as the 8086 family of microprocessors has developed. For complete compatibility with IBM equipment, the IBM usage must be followed even when it conflicts with the chip design. For example, a BOUND error occurs if an array index exceeds the specified upper and lower limits (bounds) of the array, causing an Interrupt 05H to be generated. But the 80286 processor used in all AT-class computers will, if a BOUND error occurs, send the contents of the display to the printer, because IBM uses Interrupt 05H for the Print Screen function. Hardware Interrupt Categories The 8086 family of microprocessors can handle three types of hardware interrupts. First are the internal, microprocessor-generated exception interrupts (refer to Figure 1). Second is the nonmaskable interrupt, or NMI (Interrupt 02H), which is generated when the NMI line (pin 17 on the 8088 and 8086, pin 59 on the 80286, pin B8 on the 80386) goes high (active). In the IBM PC family (except the PCjr and the Convertible), the nonmaskable interrupt is designated for memory parity errors. Third are the maskable interrupts, which are usually generated by external devices. Maskable interrupts are sent to the main processor through a chip called the 8259A Programmable Interrupt Controller (PIC). When it receives an interrupt request, the PIC signals the microprocessor that an interrupt needs service by driving the interrupt request (INTR) line of the main processor to high voltage level. This article focuses on the maskable interrupts and the 8259A because it is through the PIC that external I/O devices (disk drives, serial communication ports, and so forth) gain access to the interrupt system. Interrupt Priorities The Intel microprocessors have a built-in priority system for handling interrupts that occur simultaneously. Priority goes to the internal instruction exception interrupts, such as Divide by Zero and Invalid Opcode, because priority is determined by the interrupt number: Interrupt 00H takes priority over all others, whereas the last possible interrupt, 0FFH, would, if present, never be allowed to break in while another interrupt was being serviced. However, if interrupt service is enabled (the microprocessor's interrupt flag is set), any hardware interrupt takes priority over any software interrupt (INT instruction). The priority sequencing by interrupt number must not be confused with the priority resolution performed by hardware external to the microprocessor. The numeric priority discussed here applies only to interrupts generated within the 8086 family of microprocessor chips and is totally independent of system interrupt priorities established for components external to the microprocessor itself. Interrupt Service Routines For the most part, programmers need not write hardware-specific program routines to service the hardware interrupts. The IBM PC BIOS routines, together with MS-DOS services, are usually sufficient. In some cases, however, the MS-DOS operating system and the ROM BIOS do not provide enough assistance to ensure adequate performance of a program. Most notable in this category is communications software, for which programmers usually must access the 8259A and the 8250 Universal Asynchronous Receiver and Transmitter (UART) directly. Two major characteristics distinguish maskable interrupts from all other events that can occur in the system: they are totally unpredictable, and they are highly volatile. In general, a hardware interrupt occurs when a peripheral device requires the complete attention of the system and data will be irretrievably lost unless the system responds rapidly. All things are relative, however, and this is especially true of the speed required to service an interrupt request. For example, assume that two interrupt requests occur at essentially the same time. One is from a serial communications port receiving data at 300 bps; the other is from a serial port receiving data at 9600 bps. Data from the first serial port will not change for at least 30 milliseconds, but the second serial port must be serviced within one millisecond to avoid data loss. Unpredictability Because maskable interrupts generally originate in response to external physical events, such as the receipt of a byte of data over a communications line, the exact time at which such an interrupt will occur cannot be predicted. Even the timer interrupt request, which by default occurs approximately 18.2 times per second, cannot be predicted by any program that happens to be executing when the interrupt request occurs. Because of this unpredictability, the system must, if it allows any interrupts to be recognized, be prepared to service all maskable interrupt requests. Conversely, if interrupts cannot be serviced, they must all be disabled. The 8086 family of microprocessors provides the Set Interrupt Flag (STI) instruction to enable maskable interrupt response and the Clear Interrupt Flag (CLI) instruction to disable it. The interrupt flag is also cleared automatically when a hardware interrupt response begins; the interrupt handler should execute STI as quickly as possible to allow higher priority interrupts to be serviced. Volatility As noted earlier, a maskable interrupt request must normally be serviced immediately to prevent loss of data, but the concept of immediacy is relative to the data transfer rate of the device requesting the interrupt. The rule is that the currently available unit of data must be processed (at least to the point of being stored in a buffer) before the next such item can arrive. Except for such devices as disk drives, which always require immediate response, interrupts for devices that receive data are normally much more critical than interrupts for devices that transmit data. The problems imposed by data volatility during hardware interrupt service are solved by establishing service priorities for interrupts generated outside the microprocessor chip itself. Devices with the slowest transfer rates are assigned lower interrupt service priorities, and the most time-critical devices are assigned the highest priority of interrupt service. Maskable Interrupts The microprocessor handles all interrupts (maskable, nonmaskable, and software) by pushing the contents of the flags register onto the stack, disabling the interrupt flag, and pushing the current contents of the CS:IP registers onto the stack. The microprocessor then takes the interrupt number from the data bus, multiplies it by 4 (the size of each vector in bytes), and uses the result as an offset into the interrupt vector table located in the bottom 1Kb (segment 0000H) of system RAM. The 4-byte address at that location is then used as the new CS:IP value (see Figure 3). External devices are assigned dedicated interrupt request lines (IRQs) associated with the 8259A. See the subsection titled "The IRQ Levels" below. When a device requires attention, it sends a signal to the PIC via its IRQ line. The PIC, which functions as an "executive secretary" for the external devices, operates as shown in Figure 4. It evaluates the service request and, if appropriate, causes the microprocessor's INTR line to go high. The microprocessor then checks whether interrupts are enabled, that is, whether the interrupt flag is set. If they are, the flags are pushed onto the stack, the interrupt flag is disabled, and CS:IP is pushed onto the stack. The microprocessor acknowledges the interrupt request by signaling the 8259A via the interrupt acknowledge (INTA) line. The 8259A then places the interrupt number on the data bus. The microprocessor gets the interrupt number from the data bus and services the interrupt. Before issuing the IRET instruction, the interrupt service routine must issue an end-of-interrupt (EOI) sequence to the 8259A so that other interrupts can be processed. This is done by sending 20H to port 20H. (The similarity of numbers is pure coincidence.) The 8259A (see Figure 5) has a number of internal components, many of them under software control. Only the default settings for the IBM PC family are covered here. Three registers influence the servicing of maskable interrupts: the interrupt request register (IRR), the in-service register (ISR), and the interrupt mask register (IMR). The IRR is used to keep track of the devices requesting attention. When a device causes its IRQ line to go high, to signal the 8259A that it needs service, a bit is set in the IRR that corresponds to the interrupt level of the device. The ISR specifies the interrupt levels that are currently being serviced; an ISR bit is set when an interrupt has been acknowledged by the CPU (via INTA) and the interrupt number has been placed on the data bus. The ISR bit associated with a particular IRQ remains set until an EOI sequence is received. The IMR is a read/write register (at port 21H) that masks (disables) specific interrupts. When a bit is set in this register, the corresponding IRQ line is masked and no servicing for it is performed until the bit is cleared. Thus, a particular IRQ can be disabled while all others continue to be serviced. The fourth major block in Figure 5, labeled Priority resolver, is a complex logical circuit that forms the heart of the 8259A. This component combines the statuses of the IMR, the ISR, and the IRR to determine which, if any, pending interrupt request should be serviced and then causes the microprocessor's INTR line to go high. The priority resolver can be programmed in a number of modes, although only the mode used in the IBM PC and close compatibles is described here. The IRQ Levels When two or more unserviced hardware interrupts are pending, the 8259A determines which should be serviced first. The standard mode of operation for the PIC is the fully nested mode, in which IRQ lines are prioritized in a fixed sequence. Only IRQ lines with higher priority than the one currently being serviced are permitted to generate new interrupts. The highest priority is IRQ0, and the lowest is IRQ7. Thus, if an Interrupt 09H (signaled by IRQ1) is being serviced, only an Interrupt 08H (signaled by IRQ0) can break in. All other interrupt requests are delayed until the Interrupt 09H service routine is completed and has issued an EOI sequence. Eight-level Designs The IBM PC and PC/XT (and port-compatible computers) have eight IRQ lines to the PIC chip--IRQ0 through IRQ7. These lines are mapped into interrupt vectors for Interrupts 08H through 0FH (that is, 8 + IRQ level). These eight IRQ lines and their associated interrupts are listed in Figure 6. Sixteen-level Designs In the IBM PC/AT, 8 more IRQ levels have been added by using a second 8259A PIC (the "slave") and a cascade effect, which gives 16 priority levels. The cascade effect is accomplished by connecting the INT line of the slave to the IRQ2 line of the first, or master, 8259A instead of to the microprocessor. When a device connected to one of the slave's IRQ lines makes an interrupt request, the INT line of the slave goes high and causes the IRQ2 line of the master 8259A to go high, which, in turn, causes the INT line of the master to go high and thus interrupts the microprocessor. The microprocessor, ignorant of the second 8259A's presence, simply generates an interrupt acknowledge signal on receipt of the interrupt from the master 8259A. This signal initializes both 8259A and also causes the master to turn control over to the slave. The slave then completes the interrupt request. On the IBM PC/AT, the eight additional IRQ lines are mapped to Interrupts 70H through 77H (see Figure 7). Because the eight additional lines are effectively connected to the master 8259A's IRQ2 line, they take priority over the master's IRQ3 through IRQ7 events. The cascade effect is graphically represented in Figure 8. Programming for the Hardware Interrupts Any program that modifies an interrupt vector must restore the vector to its original condition before returning control to DOS (or to its parent process). Any program that totally replaces an existing hardware interrupt handler with one of its own must perform all the handshaking and terminating actions of the original: reenable interrupt service, signal EOI to the interrupt controller, and so forth. Failure to follow these rules has led to many hours of programmer frustration. When an existing interrupt handler is completely replaced with a new, customized routine, the existing vector must be saved so it can be restored later. Although it is possible to modify the 4-byte vector by directly addressing the vector table in low RAM (and many published programs have followed this practice), any program that does so runs the risk of causing system failure when the program is used with multitasking or multiuser enhancements or with future versions of DOS. The only technique that can be recommended for either obtaining the existing vector values or changing them is to use the MS-DOS functions provided for this purpose: Interrupt 21H Functions 25H (Set Interrupt Vector) and 35H (Get Interrupt Vector). After the existing vector has been saved, it can be replaced with a far pointer to the replacement routine. The new routine must end with an IRET instruction. It should also take care to preserve all microprocessor registers and conditions at entry and restore them before returning. Replacement Handler Suppose a program performs many mathematical calculations of random values. To prevent abnormal termination of the program by the default MS-DOS Interrupt 00H handler when a DIV or IDIV instruction is attempted and the divisor is zero, a programmer might want to replace the Interrupt 00H (Divide by Zero) routine with one that informs the user of what has happened and then continues operation without abnormal termination. The COM program DIVZERO.ASM (see Figure 9) does just that. Supplementary Handlers In many cases, a custom interrupt handler augments, rather than replaces, the existing routine. The added routine might process some data before passing the data to the existing routine, or it might do the processing afterward. These cases require slightly different coding for the handler. If the added routine is to process data before the existing handler does, the routine need only jump to the original handler after completing its processing. This jump can be done indirectly, with the same pointer used to save the original content of the vector for restoration at exit. For example, a replacement Interrupt 08H handler that merely increments an internal flag at each timer tick can look something like the code in Figure 10. The added handler must preserve all registers and machine conditions, except for those machine conditions that it will modify, such as the value of myflag in the example (and the flags register, which is saved by the interrupt action), and it must restore those registers and conditions before performing the jump to the original handler. A more complex situation arises when a replacement handler does some processing after the original routine executes, especially if the replacement handler is not reentrant. To allow for this processing, the replacement handler must prevent nested interrupts, so that even if the old handler (which is chained to the replacement handler by a CALL instruction) issues an EOI, the replacement handler will not be interrupted during postprocessing. For example, instead of using the preceding Interrupt 08H example routine, the programmer could use the code shown in Figure 11 to implement myflag as a semaphore and use the XCHG instruction to test it. Note that an interrupt handler of this type must simulate the original call to the interrupt routine by first doing a PUSHF, followed by a far CALL via the saved pointer to execute the original handler routine. The flags register pushed onto the stack is restored by the IRET of the original handler. Upon return from the original code, the new routine can preserve the machine state and do its own processing, finally returning to the caller by means of its own IRET. The flags inside the new routine need not be preserved, as they are automatically restored by the IRET instruction. Because of the nature of interrupt servicing, the service routine should not depend on any information in the flags register, nor can it return any information in the flags register. Note also that the previous handler (invoked by the indirect CALL) will almost certainly have dismissed the interrupt by sending an EOI to the 8259A PIC. Thus, the machine state is not the same as in the first myint8 example. To remove the new vector and restore the original, the program simply replaces the new vector (in the vector table) with the saved copy. If the substituted routine is part of an application program, the original vector must be restored for every possible method of exiting from the program (including Control-Break, Control-C, and critical-error Abort exits). Failure to observe this requirement invariably results in system failure. Even though the system failure might be delayed for some time after the exit from the offending program, as soon as some subsequent program overlays the interrupt handler code the crash is imminent. Summary Hardware interrupt handler routines, although not strictly a part of DOS, form an integral part of many MS-DOS programs and are tightly constrained by MS-DOS requirements. Routines of this type play important roles in the functioning of the IBM personal computers, and, with proper design and programming, significantly enhance product reliability and performance. In some instances, no other practical method exists for meeting performance requirements. Figure 1 Interrupt Number Definition 00H Divide by zero 01H Single step 02H Nonmaskable interrupt (NMI) 03H Breakpoint trap 04H Overflow trap 05H BOUND range exceeded (see note 1) 06H Invalid opcode (see note 1) 07H Coprocessor not available (see note 2) 08H Double-fault exception (see note 2) 09H Coprocessor segment overrun (see note 2) 0AH Invalid task state segment (see note 2) 0BH Segment not present (see note 2) 0CH Stack exception (see note 2) 0DH General protection exception (see note 2) 0EH Page fault (see note 3) 0FH (Reserved) 10H Coprocessor error (see note 2) Note 1: The 80186, 80286, and 80386 microprocessors only. Note 2: The 80286 and 80386 microprocessors only. Note 3: The 80386 microprocessor only. Figure 2 Interrupt Number Definition 05H Print screen 06H Unused 07H Unused 08H Hardware IRQ0 (timer-tick) (see note 1) 09H Hardware IRQ1 (keyboard) 0AH Hardware IRQ2 (reserved) (see note 2) 0BH Hardware IRQ3 (COM2) 0CH Hardware IRQ4 (COM1) 0DH Hardware IRQ5 (fixed disk) 0EH Hardware IRQ6 (floppy disk) 0FH Hardware IRQ7 (printer) 10H Video service 11H Equipment information 12H Memory size 13H Disk I/O service 14H Serial-port service 15H Cassette/network service 16H Keyboard service 17H Printer service 18H ROM BASIC 19H Restart system 1AH Get/Set time/date 1BH Control-Break (user defined) 1CH Timer tick (user defined) 1DH Video parameter pointer 1EH Disk parameter pointer 1FH Graphics character table Note 1: IRQ = Interrupt request line. Note 2: See figures 7 and 8. Figure 6 IRQ Line Interrupt Description IRQ0 08H Timer tick, 18.2 times per second IRQ1 09H Keyboard service required IRQ2 0AH I/O channel (unused on IBM PC/XT) IRQ3 0BH COM1 service required IRQ4 0CH COM2 service required IRQ5 0DH Fixed-disk service required IRQ6 0EH Floppy-disk service required IRQ7 0FH Data request from parallel printer (see note 1) Note 1: This request cannot be reliably generated by older versions of the IBM Monochrome/Printer Adapter and compatibles. Printer drivers that depend on this signal for operation with these cards are subject to failure. Figure 7 IRQ Line Interrupt Description IRQ0 08H Timer tick, 18.2 times per second IRQ1 09H Keyboard service required IRQ2 0AH INT from slave 8259A: IRQ8 70H Real-time clock service IRQ9 71H Software redirected to IRQ2 IRQ10 72H Reserved IRQ11 73H Reserved IRQ12 74H Reserved IRQ13 75H Numeric coprocessor IRQ14 76H Fixed-disk controller IRQ15 77H Reserved IRQ3 0BH COM2 service required IRQ4 0CH COM1 service required IRQ5 0DH Data request from LPT2 IRQ6 0EH Floppy-disk service required IRQ7 0FH Data request from LPT1 Figure 9 name divzero title 'DIVZERO - Interrupt 00H Handler' ; ;DIVZERO.ASM: Demonstration Interrupt 00H Handler ;This code is specific to 80286 and 80386 microprocessors. ;To assemble, link, and convert to a COM file: ; ; MASM DIVZERO ; LINK DIVZERO ; EXE2BIN DIVZERO.EXE DIVZERO.COM ; DEL DIVZERO.EXE ; cr equ 0dh ; ASCII carriage return lf equ 0ah ; ASCII linefeed eos equ '$' ; end of string marker _TEXT segment word public 'CODE' assume cs:_TEXT,ds:_TEXT,es:_TEXT,ss:_TEXT org 100h entry: jmp start ; skip over data area intmsg db 'Divide by Zero Occurred!',cr,lf,eos divmsg db 'Dividing ' ; message used by demo par1 db '0000h' ; dividend goes here db ' by ' par2 db '00h' ; divisor goes here db ' equals ' par3 db '00h' ; quotient here db 'remainder' par4 db '00h' ; and remainder here db cr,lf,eos oldint0 dd ? ; save old Int00H vector intflag db 0 ; nonzero if divide by ; zero interrupt occurred oldip dw 0 ; save old IP value ; ;The routine 'int0' is the actual divide by zero interrupt handler. ;It gains control whenever a divide by zero or overflow occurs. Its ;action is to set a flag and then increment the instruction pointer ;saved on the stack so that the failing divide will not be reexecuted ;after the IRET. ; ;In this particular case we can call MS-DOS to display a message during ;interrupt handling because the application triggers the interrupt ;intentionally. Thus, it is known that MS-DOS or other interrupt ;handlers are not in control at the point of interrupt. ; int0: pop cs:oldip ;capture instruction pointer push ax push bx push cx push dx push di push si push ds push es push cs ; set DS = CS pop ds mov ah,09h ; print error message mov dx,offset _TEXT:intmsg int 21h add oldip,2 ; bypass instruction causing ; divide by zero error mov intflag,1 ; set divide by 0 flag pop es ; restore all registers pop ds pop si pop di pop dx pop cx pop bx pop ax push cs:oldip ; restore instruction pointer iret ; return from interrupt ; ;The code beginning at 'start' is the application program. It alters ;the vector for Interrupt 00H to point to the new handler, carries ;out some divide operations (including one that will trigger an ;interrupt) for demonstration purposes, restores the original ;contents of the Interrupt 00H vector, and then terminates. ; start: mov ax,3500h ; get current contents int 21h ; ofInt 00H vector ; save segment:offset ; of previous Int 00H handler mov word ptr oldint0,bx mov word ptr oldint0+2,es ; install new handler ... mov dx,offset int0 ; DS:DX = handler address mov ax,2500h ; call MS-DOS to set int 21h ; Int 00H vector ; now our handler is active, ; carry out some test divides. mov ax,20h ; test divide mov bx,1 ; divide by 1 call divide mov ax,1234h ; test divide mov bx,5eh ; divide by 5EH call divide mov ax,5678h ; test divide mov bx,7fh ; divide by 127 call divide mov ax,20h ; test divide mov bx,0 ; divide by 0 call divide ; (triggers interrupt) ; demonstration complete, ; restore old handler lds dx,oldint0 ; DS:DX = handler address mov ax,2500h ; call MS-DOS to set int 21h ; Int 00H vector mov ax,4c00h ; final exit to MS-DOS int 21h ; with return code = 0 ; ;The routine 'divide' carries out a trial division, displaying the ;arguments and the results. It is called with AX = dividend and ;BL = divisor. ; divide proc near push ax ; save arguments push bx mov di,offset par1 ; convert dividend to call wtoa ; ASCII for display mov ax,bx ; convert divisor to mov di,offset par2 ; ASCII for display call btoa pop bx ; restore arguments pop ax div bl ; perform the division cmp intflag,0 ; divide by zero detected? jne nodiv ; yes, skip display push ax ; no, convert quotient to mov di,offset par3 ; ASCII for display call btoa pop ax ; convert remainder to xchg ah,al ; ASCII for display mov di,offset par4 call btoa mov ah,09h ; show arguments, results mov dx,offset divmsg int 21h nodiv: mov intflag,0 ; clear divide by 0 flag ret ; and return to caller divide endp wtoa proc near ; convert word to hex ASCII ; call with AX = binary value ; DI = addr for string ; returns AX, CX, DI destroyed push ax ; save original value mov al,ah call btoa ; convert upper byte add di,2 ; increment output address pop ax call btoa ; convert lower byte ret ; return to caller wtoa endp btoa proc near ; convert byte to hex ASCII ; call with AL = binary value ; DI = addr to store string ; returns AX, CX destroyed mov ah,al ; save lower nibble mov cx,4 ; shift right 4 positions shr al,cl ; to get upper nibble call ascii ; convert 4 bits to ASCII mov [di],al ; store in output string mov al,ah ; get back lower nibble and al,0fh ; blank out upper one call ascii ; convert 4 bits to ASCII mov [di+1],al ; store in output string ret ; back to caller btoa endp ascii proc near ; convert AL bits 0-3 to ; ASCII (0...9,A...F) add al,'0' ; and return digit in AL cmp al,'9' jle ascii2 add al,'A'-'9'-1 ; "fudge factor" for A-F ascii2 ret ; return to caller ascii endp _TEXT ends end entry Figure 10 ■ ■ ■ myflag dw ? ; variable to be incremented ; on each timer-tick interrupt oldint8 dd ? ; contains address of previous ; timer-tick interrupt handler ■ ■ ; get the previous contents ■ ; of the Interrupt 08H vector... mov ax,3508h ; AH = 35H (Get Interrupt Vector) int 21h ; AL = Interrupt number (08H) mov word ptr oldint8,bx ; save the address of the mov word ptr oldint8+2,es ; previous Int 08H Handler mov dx,seg myint8 ; put address of the new mov ds,dx ; interrupt handler into DS:DX mov dx,offset myint8 ; and call MS-DOS to set vector mov ax,2508h ; AH = 25H (Set Interrupt; Vector) int 21h ; AL = Interrupt number (08H) ■ ■ ■ myint8: ; this is the new handler ; for Interrupt 08H inc cs:myflag ; increment variable on each ; timer-tick interrupt jmp dword ptr cs:[oldint8] ; then chain to the ; previous interrupt handler Figure 11 myint8: ; this is the new handler ; for Interrupt 08H mov ax,1 ; test and set interrupt- xchg cs:myflag,ax ; handling-in-progress ; semaphore push ax ; save the semaphore pushf ; simulate interrupt, ; allowing the previous call dword ptrcs:oldint8 ; handler for the ; Interrupt 08H ; vector to run pop ax ; get the semaphore back or ax,ax ; is our interrupt handler ; already running? jnz myint8x ; yes, skip this one ■ ; now perform our interrupt ■ ; processing here... ■ mov cs:myflag,0 ; clear the interrupt- ; handling-in-progress ; flag myint8x: iret ; return from interrupt Advanced Techniques for Using Structures and Unions In Your C Code Greg Comeau Structures, unions, and typedefs, constructs essential to organizing data in your programs, are often misunderstood and poorly used. In "Organizing Data in Your C Programs with Structures, Unions, and Typedefs," MSJ (Vol. 4, No. 2), I attempted to clear up some of the mysteries surrounding the use of the constructs. In this article, I build on that discussion by examining pointers to structures and then move on to some of the finer points of multilevel structure access, memory allocation, and arrays. The previous article may have convinced some of you to use typedef more often in your programs. However, when doing so, you should be aware of one particular problem; structures that contain references to themselves in the form of a pointer can begin to produce mysterious syntax errors. Figure 1 shows an example of such a situation. The problem here is that the typedef for the identifier s1 has not yet been completed on line 2; therefore the compiler cannot acknowledge the existence of the s1 typedef or even the existence of any name for s1 and must deduce that an invalid type is being used. The case shown in Figure 2 is a different situation but a similar problem. Two typedefs are set up; however, they each refer to the other (in circular fashion). In this instance, the first typedef always gives an error since the second one does not yet exist. Is there a solution to these situations? There are a few; however, some are wrong and some are messy. Without a clear syntactic way to remedy this, many programmers are tempted to change the s1 * and/or the s2 * into char * (yes, either one since the typedefs may actually be found in #include files whose order of occurrence in your source file is not determinable) and then, when using or assigning to s1ptr or s2ptr, will cast it into s1 * or s2 *, respectively. This is a mess and a sure way to get into trouble with a different compiler or hardware. Casting into s1 * or s2 * is a sure bet that something will go wrong sooner or later since the cast will keep the compiler from saying anything (that is, a cast is a directive to tell the compiler that you've decided that you know what you're doing with a mismatched type. Suffice it to say for now that even though a cast implies portability, it in no way guarantees it). The description of the problem having been stated, let's discuss possible ways to go about solving it. We'll begin by breaking the problem down into various steps. As we move along I will introduce some other concepts that you might not be aware of along the way. Let me just say in passing that Pascal fans will be glad to hear that the solution is to use a forward reference. We will go through several variations of this technique. Figure 1 1 typedef struct { 2 s1 *s1ptr; 3 char s1data[100]; 4 } s1; 5 6 main() 7 { 8 s1 s1instance; 9 10 /* ... */ 11 } Figure 2 1 typedef struct { 2 s2 *s2ptr; 3 char s1data[100]; 4 } s1; 5 6 typedef struct { 7 s1 *s1ptr; 8 char s2data[100]; 9 } s2; 10 11 main() 12 { 13 s1 s1instance; 14 s2 s2instance; 15 16 /* ... */ 17 } Forward References Let's make the problem simpler for a moment by removing the typedef from the examples (Figure 3). At this point it should be obvious that the ability to reference a pointer to s1 or s2 in the code sample does not exist. In other words, s1 and s2 are clearly not types. However, what if we were to add a structure tag to each declaration first so that the structure shape can be referenced instead (Figure 4)? The key here is that now s2ptr and s1ptr must not deal with a "type of;" they only serve as references to some structure_tag. Note, however, that though a forward reference through a pointer declaration is legitimate, an instance of the structure is not. If we look at Figure 5, the declaration of anotherptr is fine, since the pointer does not yet have to know what the shape of the structure it points to (the shape of the struct anothertag) looks like. In C, this is one way that an incomplete type can exist. Of course, if you wish to use anotherptr beyond having the pointer assigned to it, you must first define the structure to which it points. Similarly, the declaration of anotherinstance will fail, since I was attempting to include a whole instance of anothertag within sometag. This can't happen because the compiler has no way to determine sometag's members and therefore it is sizeless. The compiler can't delay this either since sometag would not have a valid size and shape until anothertag had a size and shape, thus creating an error. The problem of incomplete types in C is an important one to be aware of. It actually goes well beyond the discussion presented here and is not related to structures only. In general, you can always refer to an incomplete type but can never use it as a single entity, that is, as a unit by itself, since it is not completely defined. Another popular place where incomplete types are commonly used is with external arrays, for example, in a declaration such as extern int array[ ];. Here you can index and get the address of the array as normal; however, you cannot perform some operations such as sizeof(array) since the dimension of the array may not be in the same source file of such a declaration. This happens because array only serves as a declaration and not a definition. Figure 3 1 struct { 2 s2 *s2ptr; 3 char s1data[100]; 4 } s1; 5 6 struct { 7 s1 *s1ptr; 8 char s2data[100]; 9 } s2; 10 11 main() 12 { 13 s1 s1instance; 14 s2 s2instance; 15 16 /* ... */ 17 } Figure 4 1 struct s1_tag { 2 struct s2_tag *s2ptr; 3 char s1data[100]; 4 }; 5 6 struct s2_tag { 7 struct s1_tag *s1ptr; 8 char s2data[100]; 9 }; 10 11 main() 12 { 13 struct s1_tag s1instance; 14 struct s2_tag s2instance; 15 16 /* ... */ 17 } Figure 5 1 struct atag { 2 struct anothertag *anotherptr; 3 /* reference to another tag before 4 it comes into scope is fine */ 5 }; 6 7 struct sometag { 8 struct anothertag anotherinstance; 9 /* This is an error since a 10 description of anothertag 11 is not in scope */ 12 }; 13 14 main() 15 { 16 /* ... */ 17 } Typedef/Structure Tags Generally speaking, when you are considering the use of #define and typedef, it is usually safe to conclude that typedef is the better choice. Here I have introduced yet another major source of confusion when using structs by using structure tags. However, do not get the mistaken impression that I'm saying tags are better than typedefs, since this is not necessarily the case. I've only used tags as a stepping stone. For instance, now that forward references are clearer, let's turn back to the original problem and solve that using typedefs. There are two common ways to set this up (see Figures 6 and 7)--both are equivalent. In the first, two typedefs are set up that refer to structure tags (and since a typedef only serves as a synonym, we only care that it matches a structure tag that can be defined at a later time). This brings the two typedefs into scope and lets you use them from that point onward, simply using the names s1 and s2 as if they were types. This is true even when using them within the definitions of the structure tags that they were based upon, as shown in Figure 6. The second method (Figure 7) allows us to produce the same result using a different method. Actually it's the reverse of the previous procedure: instead of typedefing the names and creating the structure tags last, we'll create the structure tags as we're typedefing by using structure tags internally. Figure 6 1 typedef struct s2_tag s2; 2 typedef struct s1_tag s1; 3 4 struct s1_tag { 5 s2 *s2ptr; 6 char s1data[100]; 7 }; 8 9 struct s2_tag { 10 s1 *s1ptr; 11 char s2data[100]; 12 }; 13 14 main() 15 { 16 struct s1_tag s1instance; 17 struct s2_tag s2instance; 18 19 /* ... */ 20 } Figure 7 1 typedef struct s1_tag { 2 struct s2_tag *s2ptr; 3 char s1data[100]; 4 }; 5 6 typedef struct s2_tag { 7 struct s1_tag *s1ptr; 8 char s2data[100]; 9 }; 10 11 main() 12 { 13 struct s1_tag s1instance; 14 struct s2_tag s2instance; 15 16 /* ... */ 17 } Passing/Returning Structs I'm assuming that the reader knows how to pass structures to functions. It should be sufficient to say that you should almost always pass a pointer to a structure or the address of a structure (&struct_var, which is mappable to a pointer to a structure) as the argument to a function instead of the actual structure itself. In other words, pass the structure by reference, not by value. The latter case can require an unreasonable amount of stack space and, if you are not careful, can cause symptoms that include random crashes, lockups, invalid pointers, and other such problems. Returning a structure should also be through a pointer, mostly for the sake of efficiency. This involves three areas: returning a complete structure from the function, returning a pointer to a structure, and the case where just some of the structure's members are accessed after function(s) return them. Returning a Structure When returning a complete structure from the function, consider what the compiler must go through. A typical compiler might copy the structure into a static area set aside by the compiler and linker. This area can be thought of as a union of every structure in your program. This of course means that it must be large enough to hold any given structure. It also needs to be addressable without any idiosyncrasies, and must be static with suitable alignment qualities. This copying alleviates much of the game playing the generated object code goes through since there is usually a standard function calling and return convention that the compiler writer will try to make the code adhere to. There are, however, problems. Consider the program in Figure 8, specifically the call to func on line 25. This should work as if f1 and f2 return base types, since you are, after all, allowed to return structures from functions. Note the series of events that must occur--f1 will return a struct ps1 by copying it into a static area; next that static area might be copied onto the stack, which is accomplished on some systems by generating lots of "move long word" instructions instead of a loop. The same will be performed for f2's return value. The end result is reached by a slow and tedious process. But that's only half the problem. If you pass the structures returned from f1 and f2 by their addresses as shown in line 26 (you can do this since a structure is an aggregate and not a simple scalar), you may find that your compiler does not work as expected. Both f1 and f2 might return the same address (remember our friend provided by the linker), thus the addresses and strings printed out by func2 could be the same. Figure 8 1 struct ps1 { 2 char *p; 3 char array[1024 - sizeof(char *)]; 4 } v1; 5 6 struct ps2 { 7 char *p; 8 char array[512 - sizeof(char *)]; 9 } v2; 10 11 struct ps1 f1() 12 { 13 v1.p = "hi there"; 14 return (v1); 15 } 16 17 struct ps2 f2() 18 { 19 v2.p = "HI THERE"; 20 return (v2); 21 } 22 23 main() 24 { 25 func(f1(), f2()); 26 func2(&f1(), &f2()); /* This doesn't mean the 27 address of f1 and f2! 28 It means the address 29 of the structures they 30 return--Remember 31 precedence! */ 32 } 33 34 func(struct ps1 v1, struct ps2 v2) 35 36 37 { 38 printf("%s\n", v1.p); 39 printf("%s\n", v2.p); 40 } 41 func2(struct ps1 *v1, struct ps2 *v2) 42 43 44 { 45 printf("%ld\n", v1); /* print out the addrs 46 that v1 and v2 point 47 to, these */ 48 printf("%ld\n", v2); /* may also be printed 49 with a %p instead of 50 %ld */ 51 printf("%s\n", v1->p); 52 printf("%s\n", v2->p); 53 } Returning a Pointer The second case is quite simple: don't return a pointer to a structure that's automatic. Figure 9 shows an example of such misuse. Notice that func returns &temp, which is perfectly valid C but not valid logic. When func exits, the temp structure--not the temp tag, which has file scope in this example--will terminate since it is local to a function and nonstatic. Since temp has terminated, accessing it will result in undefined behavior and most likely a program crash. Note that changing the declaration to static struct temp temp; will not cause any problems. Because temp is now static it will have a permanent existence during the life of your program. In this case it doesn't matter if the function that houses temp is currently executing or not; the fact is that temp is addressable since it's static. Additionally, whether temp is in scope or not is irrelevant here. Scope relates only to identifiers. It does not deal with concepts such as variables--for instance setting a pointer to an absolute position and accessing it--or things like addresses. Of course you will only be able to obtain an identifier's address while it is in scope; but once you have the address of an object, during its lifetime it's yours to do with as you please (within reason of course). Figure 9 1 struct temp { 2 int members; 3 /* ... */ 4 }; 5 6 struct temp * 7 func() 8 { 9 struct temp temp; /* yes, structure tags *and* 10 structures can have the 11 same name */ 12 13 return (&temp); 14 } 15 16 main() 17 { 18 struct temp *temp; 19 20 temp = func(); 21 /* <expressions using temp->???> */ 22 } 23 Member Access The last scenario is one in which a function returns a structure, but you're only interested in accessing a few of its members. Think about this for a moment. Should you access the members by returning the structure and be hit with all the copying or should you take the easy route by returning a pointer to the structure? In the case of an operating system call, you'll rarely want a pointer to one of the internal data structures of the operating system unless you're writing a device driver or a very specialized application that requires some special knowledge. In another situation you might find you're calling a C library function instead of a system call, and are most likely accessing something in your "process's space." But the arguments to the particular library you are using are usually a given and neither can nor should be changed. Routines may accept an argument that's a pointer to a structure (one that you've properly allocated based on some #include file) and completely avoid a return value, which would have been a structure. The routine itself will set the structure's members as might be appropriate. This will prevent the situation I've presented and is a viable way to code some of your programs. In addition, you might find routines of your own that use large structures, and instead of passing whole entities back and forth, it is often worth creating another smaller structure which is a subset of the larger one and manipulating that instead. Depending upon your logic and program design, you will also find it worthwhile to return a pointer to a structure even if only one of two members is going to be used. You only have to apply the arrow operator (->) to the returned pointer to access the member in question, rather than dealing with the whole structure. Remember that you may be calling a function containing an instance of the structure it's returning a pointer to as an internal static identifier. However, the consequence is that calling the function twice could destroy the previous value of the structure, so the calling function must account for this and copy all the data it needs before calling the previously called function again. This presents another situation in which either the caller of the function or the function itself dynamically allocated the structure. You must therefore be careful to control the allocation of the structure and be just as careful to make sure that you free it properly. Structure Access Figure 10 contains both structures with pointers to other structures and instances of the other structures. Chances are that you will never encounter this specific situation. Nevertheless I've included it to provide some further insights into structures in general. Most of you can no doubt construct a simple structure member reference as in line 21 of Figure 10. Some of you can even create a multistructure or multilevel reference as in line 23--but without certainty that it is correct (it is). Anything beyond this, however (meaning those nasty creatures we call pointers), is unfamiliar territory. To understand line 23, you must be aware that s3 contains an instance (that is, an occurrence) of an s2tag structure (an s2tag structure named s2inst), which in turn contains an instance to an s1tag structure (s1inst), which of course contains an instance of an integer identifier named s1var. To use each of these instances, you simply create a structure access to the member, as shown in line 21. Since the operator precedence of the dot (.) operator presents no problem and its associativity is naturally oriented to be left to right, the setup is simply structure1.<...>.structureN which line 23 shows. Remember that this all takes place within s3 because s3 contains an s2tag and s2tag contains an s1tag. Under a different case, such as in lines 25-28, the code will use references to s3tag, s2tag, and s1tag by way of ps3, ps2, and ps1, which go beyond the limits of s3 to gain access to other variables such as s2 and s1. On line 25, ps3 is set equal to the address of s3. Note the use of &s3 instead of s3 since we want to obtain the address of s3 and not an assignment to the pointer of the actual contents of the s3 structure. Once this is done, we can reference the members of the structure (s3 in this case) that ps3 points to by using the -> operator. It happens that we want to assign to ps2, which is another structure pointer (a pointer to an s2tag). This assignment will take place just as smoothly as the ps3 assignment. The evaluation of the arrow operator will take place in exactly the same order of precedence as the dot operator. Again, this will occur with left to right associativity. Our knowledge of line 23 coupled with this discussion should make the interpretation of lines 27 and 28 very easy. Briefly then, line 27 uses the fact that ps3->ps2 references an s2tag, which contains a reference to an s1tag. This allows ps3->ps2->ps1 to be assigned to an identifier such as s1, which has an s1tag size and shape. Line 28 uses ps3->ps2->ps1, which is a pointer to an s1tag and therefore a reference to a member such as s1var is possible as well. Every pointer used in lines 25-27 needed to be initialized. You could not simply have coded the access to s1var in line 28 without the other assignments. That would not be valid logic since every pointer in this example must access a memory location in order to be used. Be careful here since this constraint is something that you as the programmer must take care to enforce. The compiler doesn't care, for instance, that you may have coded line 28 without lines 25 through 27. You may actually have performed the structure pointer assignments in those lines in another part of the program based on if/else logic and not necessarily right before line 28. In general the compiler has no easy way of determining this. The moral is to make sure all your assignments and pointers are set up properly since the compiler is not going to give you a warning or error message for failing to initialize them correctly. Problems of this nature will typically begin to crop up during the execution of your program; more often than not they will be sporadic and very hard to debug. If you provide the extra ounce of prevention during coding, you will avoid many of these situations. Line 29 really means less than you might think it does. If you take a closer look at the code, you will see that the execution of line 27 allowed line 28 to gain access to s1.s1var. However, this may work properly even if ps3 and/or ps3->ps2 are not valid pointers. Or I should say it appears to work properly--can you see the problem here? If we work under the assumption that line 25 was accidentally deleted from the source file, would the program continue to work properly? Perhaps. If it did continue to work, should it have? No. As explained above, it would most certainly compile so that's not the concern here. It would most likely execute under DOS as though nothing were wrong. However, most versions of the XENIX(R) operating system, as well as OS/2 systems, would produce a general protection fault because of the invalid memory access. This would occur because ps3 is an external (that is, an external defined in the same source file with no initializer) variable and would therefore be implicitly initialized to zero. If this is the case, then line 26 would be indexing ps2 off a pointer that points at memory location zero. That assignment would then be assigning something to a memory location of 0 + sizeof(int), which may map into memory location 2 and then be treated as the location of ps3->ps2, which we all know is wrong. However, if an access to that location at that moment in time does not stomp on something it shouldn't (and in many cases this is not as clear as the scenario that I'm describing), execution will continue with no apparent damage. Microsoft(R) C Optimizing Compiler versions 5.0 and later usually protect against the case above with their infamous R6001 run-time error message upon program termination; however, this is not something you should particularly depend upon, and it shouldn't be relied on to help solve your problems. Furthermore, the R6001 error functions only when your program writes in low core. Anything beyond that certain magic number and you're on your own. Therefore, make sure that your pointers are always legitimate regardless of whether or not your program executes properly. Clearly looks can be deceiving in this case. An excellent example of this scenario is when you merge together different stubs to your program and suddenly something seems to be going wild. Most likely this is due to invalid pointers pointing to locations that cannot be touched without causing a problem. This would occur because your program modules' variables and functions will most likely be in different memory locations as well as in a larger program. When this happens, errors that didn't show up in the stubs will begin to appear. Given the preceding information, we're now ready to tackle some of the other derivations in Figure 10. For instance, if we wanted to access s1inst indirectly through the ps2 pointer, we might use code similar to line 31. If we read this left to right (since both -> and . have equal precedence and left to right associativity), you'll notice that the part referring to ps2.s1inst is in error. Since ps2 is a pointer, constructing anything with ps2. (note the dot) is not going to work because the dot operator requires that the left operand have a structure type. This structure type must be either an identifier reflecting a structure name, or a dereference of a parenthesized pointer to structure type as in ps=s; (*ps).m;. The proper way to do this is shown on line 32, which is the same as line 27, only instead of referencing another pointer (ps1), we're accessing a real structure (s1inst). You may see another solution to this problem since you should know from your knowledge of C that a structure reference such as pointer->member is translatable into (*pointer).member. However, how do we implement it in this case? Line 33 seems like a good possibility to resolve this problem (let's not even consider the syntax of line 34) but it does not go far enough. Unfortunately, compiling this will not clue you in any better since most compilers will simply report a syntax error. This isn't immediately intuitive--at least not until you see exactly what's going on behind the scenes. You need to ask yourself: What exactly is ps2? We know it's a pointer, and we think we know its name but in fact its name is not ps2. If it were, you would be able to say something like ps2 = 0; and that is clearly unreasonable (compile it to prove it), since there is no identifier with a name consisting solely of ps2. Note that there is one named s3.ps2 and another that can reference it through pointer notation, as in ps3->ps2. Therefore, these are two names we must refer to in this particular program when accessing ps2. Tying this back to the (*pointer).member notation discussion above, the correct syntax for line 33 is shown in line 37 since ps3->ps2 is the proper pointer reference to ps2. This in no way infers line 38 is equivalent to line 37. As discussed in the previous article, this line would produce an error because of the precedence of the * operator. As a final note, make sure that you understand that lines 32 and 37 do not access the same s1var as in line 38. Again, recall that an s3tag contains a pointer to an s2tag and an instance of an s2tag--they are not the same thing. You could point the s2tag pointer (ps2) to the s2tag instance (s2inst), but if you had wanted to do that, you'd need to code lines 39-42. Figure 10 1 struct s1tag { 2 int s1var; 3 } s1; 4 5 struct s2tag { 6 int s2var; 7 struct s1tag *ps1; 8 struct s1tag s1inst; 9 } s2; 10 11 struct s3tag { 12 int s3var; 13 struct s2tag *ps2; 14 struct s2tag s2inst; 15 } s3; 16 17 struct s3tag *ps3; 18 19 main() 20 { 21 s1.s1var = 99; 22 23 s3.s2inst.s1inst.s1var = 5; 24 25 ps3 = &s3; 26 ps3->ps2 = &s2; 27 ps3->ps2->ps1 = &s1; 28 ps3->ps2->ps1->s1var = -99; 29 printf("s1var=%d\n", s1.s1var); 30 31 /* ps3->ps2.s1inst.s1var = 11; */ 32 ps3->ps2->s1inst.s1var = 22; 33 /* ps3->(*ps2).s1inst.s1var = 33; */ 34 /* ps3->*ps2.s1inst.s1var = 44; */ 35 /* ps2 = 0; */ 36 /* might as well have called this 'abccba' */ 37 (*ps3->ps2).s1inst.s1var = 55; 38 /* *ps3->ps2.s1inst.s1var = 66; */ 39 printf("s3.s2inst.s1inst.s1var=%d\n", 40 s3.s2inst.s1inst.s1var); 41 42 ps3->ps2 = &ps3->s2inst; 43 ps3->ps2->s1inst.s1var = 22; 44 printf("s3.s2inst.s1inst.s1var=%d\n", s3.s2inst.s1inst.s1var); 45 } Multilevel Structure Access Involving Functions There are many situations in which you may call functions that return structures, or pointers to structures and the use of temporary variables becomes a nuisance. Consider the function example1 in Figure 11. Is it clear that all of the lines from 27 through 30 will print out 12345? Line 27 ought to be self-explanatory as a reference to s1.s1var, which has been statically initialized to the value 12345. Line 28 involves a structure analysis exactly like any other, with the same precedence and associativity of operators involved. Do you care that there is a ( ) involved? No, because you memorized the top line of the Operator Precedence/Associativity chart. Therefore since s1returner returns an s1tag type and explicitly returns s1, why not just treat it like line 27? It's simply a structure.member reference. Line 29 presents a function that returns a pointer to an s1tag, but nothing is that different here even though it does involve a function. If it returns a pointer, access it as a pointer->member reference. Of course line 30 is just a familiar pointer->member case being mapped into (*pointer).member. These variable references are all rather natural. The alternative is to code something along the lines of the statements shown in the example2 function. As you can see, this makes things longer and more tedious than necessary and in this program needn't be used. This point hits home when you realize that you may have a second structure involved, such as s2, and as in the previous section, there are pointers and references to all sorts of variables. This would result in code such as that found in example3 and example4 (or even more complex situations). Figure 11 1 struct s1tag { 2 int s1var; 3 } s1 = { 12345 }; 4 5 struct s2tag { 6 int s2var; 7 struct s1tag *ps1; 8 struct s1tag s1inst; 9 } s2; 10 11 struct s2tag *ps2; 12 13 struct s1tag 14 s1returner() 15 { 16 return (s1); 17 } 18 19 struct s1tag * 20 ps1returner() 21 { 22 return (&s1); 23 } 24 25 void example1() 26 { 27 printf("%d\n", s1.s1var); 28 printf("%d\n", s1returner().s1var); 29 printf("%d\n", ps1returner()->s1var); 30 printf("%d\n", (*ps1returner()).s1var); 31 printf("%d\n", (ps1returner())->s1var); 32 /* printf("%d\n", (ps1returner()).s1var); */ 33 } 34 35 void example2() 36 { 37 struct s1tag s1holder; 38 struct s1tag *ps1holder; 39 40 printf("%d\n", s1.s1var); 41 s1holder = s1returner(); 42 printf("%d\n", s1holder.s1var); 43 ps1holder = ps1returner(); 44 printf("%d\n", ps1holder->s1var); 45 printf("%d\n", (*ps1holder).s1var); 46 } 47 48 struct s2tag 49 s2returner() 50 { 51 return (s2); 52 } 53 54 struct s2tag * 55 ps2returner() 56 { 57 return (&s2); 58 } 59 60 void example3() 61 { 62 printf("%d\n", s2.s1inst.s1var); 63 printf("%d\n", s2returner().s1inst.s1var); 64 printf("%d\n", ps2returner()->s1inst.s1var); 65 } 66 67 void example4() 68 { 69 printf("%d\n", ps2returner()->ps1->s1var); 70 } 71 72 main() 73 { 74 example1(); 75 example2(); 76 77 s2.s1inst.s1var = 54321; 78 example3(); 79 80 s2.ps1 = &s1; 81 example4(); 82 } Structures and Malloc There is one other important point to consider. Typically, one must deal with packets of information. In other words, you may find that you're being fed some group of data that is prefixed with control or status information and is then followed by the actual data being described. This would normally occur when dealing with communications, but it needn't occur only there. The problem this presents to the C programmer is that the information part of the packet is finite and predictable, whereas the data portion may be variable in length. For instance, you may find that the following structure is sufficient for describing the control information: struct apacket { int packethead; int packetcontrol1; int packetcontrol2; char data[???]; }; But how large do you make the dimension of data[ ]? If you make it too small, you could lose data. If you make it too large, you could waste critical space in your program or machine state. Make it reasonably large, and you may not know if that size will be too small for the future--then what do you do? You could add another field that encodes the size of the data length, then derives some unions and associated code to perform something like the codedrecord example in the previous article. I'm sure we can all agree, however, that that would be a big mess. The best answer is to avoid the constraints altogether and work with what you have been given. In other words, since you know what the members of the structure that represent the control information are, why not use that to your advantage? This, along with the ability to allocate memory dynamically through standard library routines, such as alloc, calloc, and malloc, is all you need. A first attempt at a solution might result in a structure template and sample code such as: struct apacket { int packethead; int packetcontrol1; int packetcontrol2; int packetsize; char data[0]; }; < other code > struct apacket apacket; struct apacket *ppacket; ppacket = (struct apacket *)malloc( sizeof(struct apacket) + apacket.packetsize); Unfortunately, C does not allow for zero-length data items to occur, even in structure tags. (Note that some compilers allow this, but they are clearly in error. This is not a portable construct and should be completely avoided.) Many of you will be quick to point out that making data into a 1-dimensional array with a bound of 1 (for example, char data[1]) should work without a problem (which is true). Note that the 1-byte length will need to be subtracted from the value that's being passed to malloc. This length can be presented as sizeof(char), sizeof(char [1]), or simply the constant 1 since in this case they all represent and refer to the same thing. By the way, because sizeof can accept a derived type as its argument, the second sizeof says, "give me the size of an array of x characters, where x is 1." An equal yet slightly less messy approach is to make use of the offsetof macro. Under this circumstance, the structure tag would still contain a char data[1], however instead of ppacket = (struct apacket *)malloc( sizeof(struct apacket) - sizeof(char[1]) + apacket.packetsize); you would code: ppacket = (struct apacket *)malloc( offsetof(apacket, data) + apacket.packetsize); since the offset of data within apacket would represent the length of the previous fields (in our case all the fields) in apacket. I feel this is a superior method. With offsetof it's perfectly clear: get the size of the structure and add it to the size desired for the data. Once you have a pointer of the correct length, you may copy the structure members as appropriate. However, before leaving this subject, let me once again refer you to the March article and encourage you to reread the sections dealing with structure holes and structure packing since you may find that you will need to set up your structures in the same "manner" as the one(s) which you are copying it from. This scheme of creating dynamic structure images is, as most things, not without problems and requires some careful thought before you use it. For instance, it should be clear that if you use this scheme, every reference to the packet's members will be through a pointer. This is due to the variable length data that must remain as a single unit. It's important to note that if you do not need a variable length structure, you should think about organizing your data in a less elaborate way. For example, you could change the apacket structure tag layout so that data is not an array but a character pointer. Allowing for this circumstance will usually be far more natural, since apacket instances must be set up (using a char *data; construct) and then referenced. Note that now all the structure members can be accessed directly with the dot operator. The close relationship between arrays and pointers allows data to be referenced in array notation, if desired, as follows: struct apacket somepacket; somepacket.data = (char *) malloc( somepacket.packetsize); <...> somepacket.data[i] = <...>; <...> *somepacket.data = <...>; /* Note precedence with no parens! */ <...> The only ramification of this example is that you will need to free the memory block associated with somepacket.data when you no longer need the data (for dynamically allocated data, the programmer controls the lifetime of the memory block). Arrays Since we are on the subject of dynamic memory, it's worth dwelling a bit on arrays and structures. First, look at line 21 of Figure 12. By now I would hope that you wouldn't have much of a problem interpreting it. Nor should you have a problem deciphering lines 24-26, where a pointer to a structure is set equal to an element of an array (which is therefore using only one structure). The use of arrays (and pointers) implies that their usage and idiosyncrasies remain the same regardless of whether they are used inside structures or as structures (for instance, the syntax and semantics of passing an array to a function does not change because the array might consist of structures rather than a base type like int). I'd also like to direct your attention to the use of HBOUND that occurs on line 20. It's actually very simple (as lines 17-18 demonstrate) and is very handy when dealing with a good many array situations. Finally, if you needed to use and access s1 strictly via pointers, an alternate to lines 20-22 can be found in lines 29-35. The latter lines are surely more cryptic; however, they do remove the indexing notation, which would be advantageous if you were to reference other elements of the structure or even repeated occurrences of the same element. Returning to HBOUND, you should study the constructs on lines 29 and 32-33. The code is checking to see if the pointer has gone beyond the end of the array by checking it against an array dimension that is one greater than the array. In other words, ps1 will be checked against &s1[10], which is one greater than nine (remember that in C arrays start at element 0). Similarly, cp will be checked against &ps1->charray[20]. Note in particular how all of this occurs without having to explicitly use any constants in either of the two nested loops that occur in this program. You should strive for this type of construction in your own programs, whether you are dealing with structures or not. Figure 12 1 #define HBOUND(array) (sizeof(array) / 2 sizeof(array[0])) 3 4 struct s1tag { 5 char charray[20]; 6 }; 7 8 struct s1tag s1[10]; 9 struct s1tag *ps1; 10 11 main() 12 { 13 int i; 14 int j; 15 char *cp; 16 17 printf("%d/%d=%d\n", sizeof(s1), 18 sizeof(s1[0]), HBOUND(s1)); 19 20 for (i = 0; i < HBOUND(s1); i++) 21 for (j = 0; j < sizeof(s1[0].charray); j++) 22 s1[i].charray[j] = '\0'; 23 24 ps1 = &s1[5]; 25 ps1->charray[2] = 5; /* Note that this is ASCII 5 */ 26 (*ps1).charray[2] = 5; /* not '5' */ 27 28 printf("%d\n", sizeof(ps1->charray)); 29 for (ps1 = &s1[0]; ps1 < &s1[HBOUND(s1)]; 30 ps1++) { 31 cp = ps1->charray; 32 while (cp < &ps1->charray[sizeof( 33 ps1->charray)]) 34 *cp++ = '\0'; 35 } 36 } Summary Although this article has focused specifically on structures and the different aspects of having pointers to structures, accessing struct members, avoiding memory conflicts with arrays, and memory allocation, most of these concepts apply to unions as well. With the information presented here, added to the insights presented in the previous article, you now have a solid base of knowledge concerning the use of structures. When you add to this the insights into unions, typedefs, and C declarations that you have been storing up from the past several issues, you are ready for some serious C programming. ────────────────────────────────────────────────────────────────────────────

Volume 4 - Number 4

──────────────────────────────────────────────────────────────────────────── Circumventing DOS Program Memory Constraints with an Overlay Manager Dan Mick In the current state of computing, both the PC end user and the programmer have an incredible array of software tools at their disposal. Thousands of terminate-and-stay-resident (TSR) utilities add timesaving, useful features to MS-DOS(R) and PC-DOS operating system machines, while integrated environments and multitasking shells extend the PC's power. But all these tools use memory, sometimes a good deal of it. Therefore, application programmers need to be certain their code is well designed, using small utility modules to decrease duplicated routines. Although such optimization can save a lot of code space, sometimes it isn't enough. In such cases, an overlay manager may be just the thing an application needs. The overlay is a fairly common concept for data space; the C programmer will be familiar with malloc and free to manage dynamic memory, as will the Pascal programmer with New and Free, Mark and Release. Overlaying may be thought of as a kind of dynamic code memory allocation. The basic concept of the overlay manager is that not every routine in a program needs to be loaded into memory at the same time. In a word processing program, for example, the printing output routines for a document will probably never be active while that document is being edited. Overlays make it possible for the printer routines to stay on disk until needed, at which point the printing code can be put in memory and the editing code can be put on disk (to be read later or to be discarded). This way, the word processor uses less memory. Another example is the printer driver code. Since only one printer is used at a time, only that printer driver is loaded at a time. Using Overlays The only justification for overlaying code is the decrease in resident code size, since speed will suffer as a result of disk loading times. But decreasing code size is important. No matter what the overlay scheme, the programmer's job is much easier if the code need not be modified greatly; in particular, the application should be designed as a "flat" application to aid in development and debugging, with the overlay management added late in the development cycle. (Debugging an overlaid program can really give you headaches.) Also, the decisions concerning how the program is overlaid are best delayed until late in the development cycle, since the complexity and size of routines may make a difference. If it's easy to add overlay management, it can be added late in the cycle without requiring a lot of editing to existing working code. TSRs can make life easier, as programmers and users realize. Editors, calendars, alarms, datebooks, help and documentation, communications, even games are nice to have available at the touch of a key, but they come at the expense of precious memory. It's not difficult to have two or three favorite utilities and find out there's no longer room for much editing or compilation on the system. Overlaying TSRs is a natural, since they're dormant most of the time; it's only when the TSR is active that any code is needed. At that point, the memory could be allocated from DOS and the code loaded; it could then be released when the TSR is deactivated. It's nice to be able to have some TSRs loaded while running other, more complex programs in which all the code is not needed. A word processor could be a desktop-publishing-style program that takes up 500Kb, leaving no room for anything else to be loaded. But if it were overlaid, it might use only 300Kb, leaving room for whatever else the user might desire. The speed of the application will suffer--but only when moving from one overlay to another. With good planning, the programmer can ensure that the swapping time is minimal, or perhaps even unnoticeable, buried in the time it takes a user to read the screen output from the last command. Even if the delay is noticeable, the program's smaller memory size may be worth the speed tradeoff. To alleviate some of the speed loss, the overlaid code can be stored in LIM EMS memory, allowing it to load nearly instantly. Expanded memory is ideal for this application, as it's not easy to actually run code from the EMS window (at least with older versions of EMS), but it's certainly easy to copy pages to and from the EMS window in the overlay manager. A disk cache in EMS can, however, serve the same purpose with far less effort. Early Approaches Programmers have been using different approaches to implement overlays for awhile. One early approach was built-in language statements, such as OVERLAY FUNCTION and OVERLAY SUBROUTINE. Mainframe FORTRAN compilers often had built-in statements for the programmer to direct the overlaying process. Another early approach was roll-your-own overlaying, as practiced by WordStar(R) (CP/M(R) and DOS) and Lotus(R) 1-2-3(R). The main disadvantages to the above two approaches are code complexity and the lack of transparency to the programmer. Extra work means extra places to introduce bugs, and overlay-system bugs can be hard to find. Other approaches to overlay management leave the application programmer to do what he or she will, and the operating system to do the work of swapping code. One approach, used on some early mainframes, is whole-process swapping, which allowed multiple applications to share the computer's resources by swapping the entire program to secondary storage and gave control of the machine to the next program in line. Demand-paged or segmented virtual-memory management, used more often today, break the code space into smaller units than processes or programs, called pages (or segments), and swap parts of processes rather than the entire process. This allows finer granularity in swapping code space than whole-process swapping, therefore using memory more efficiently and involving less disk I/O. There are some disadvantages, however, to allowing the operating system to do the swapping work. For one thing, each process may suffer more memory-management overhead during its execution since all the pages for the entire process may not be present at once. In addition, the operating system has to manage the swapping for all processes, so it must approach things in the general sense, trying to manage memory well for an "average" application--which, of course, doesn't exist. The application itself will know more about which code needs to be coresident and which does not. Moreover, the operating system makes its swapping decisions at times unrelated to program execution. The program may know, for instance, that it is now done with the initialization code forever. The operating system has no way of telling that the initialization code is used only once. Current Approaches There are several current solutions available for PC programmers who want to overlay their applications. Phoenix Technologies, Ltd., sells a product called PLINK that works with Microsoft(R) OBJ files, allowing incredible flexibility in overlaying both code and data. Also, PLINK is a nonstandard linker; this may or may not be a bad thing, but it certainly increases the number of manuals on your desk. Microsoft C and Microsoft FORTRAN optimizing compilers provide overlaying capability through the Microsoft Overlay Linker (LINK) and the overlay managers supplied with the language run-time libraries. But the Microsoft Macro Assembler (MASM) programmer cannot use LINK's overlaying features without those languages. Also, the Microsoft overlay manager and linker do not support languages that Microsoft doesn't offer, such as C++, Modula-2, and PL/I, or even Microsoft QuickBASIC. LINK LINK does provide overlaying capabilities that programmers can make use of without developing in Microsoft C or FORTRAN. LINK provides the most basic form of overlaying. One section of the program is defined as the overlay area, and there may be several different code overlay segments (but fewer than 256, because the selector is 1 byte). Each overlay segment is made up of one or more modules (object files) that live, one at a time, in the overlay area. The overlay manager is expected to install itself as an interrupt routine (the linker changes the entry point of the program to a location in the overlay manager so the manager has a chance to initialize itself before the main program code is called). The linker then translates all overlay routine calls to software interrupts, as described below, to allow the overlay manager to load the overlay code from disk before the control transfer takes place. There is only one overlay segment present in the overlay area at once, but it may involve object code from more than one module. For example, the command line LINK main+(p1+p2)+(p3) ; causes LINK to make MAIN.EXE, which consists of a resident portion main, and two overlay segments, one made up of code from modules p1 and p2, and one made up of code from module p3. An important point to note is that in order for this to work, modules p1, p2, and p3 need to have class names ending in "CODE", and each needs to be a different segment than the segment defined in MAIN. In the examples shown here, MASM's .LARGE directive and separate source files take care of this, but it's something to be careful of. The far code model must be used for the overlay manager; that is, code must be located at far addresses and use far calls to transfer control. This enables the overlay linker to replace the far call (5 bytes) with a software interrupt instruction (2 bytes), an overlay number (1 byte), and an offset into the overlay area for the call (when more than one entry point into the overlay is provided). For example, a CALL MY_ROUTINE might be replaced with INT 3FH (which is the default overlay-interrupt-handler number), DB 01 (signifying the first overlay), and DW 0100 (representing an offset of 100H into the beginning of the overlay area as the call destination). LINK replaces only those calls that refer to overlaid code. Interestingly, if you examine an overlay-linked program with SYMDEB, the overlay calls are disassembled as INT 3F; <overlay number> <overlay offset> just as described. SYMDEB is at least partially overlay aware. The Microsoft CodeView(R) debugger (referred to herein as CodeView) even has hooks so that local symbols for overlay procedures are properly mapped in. Note that this method of calling the overlay manager will not properly handle functions that are called through pointers. If you attempt to use the simple overlay manager presented here, and call an overlaid function through a function pointer, LINK will not replace the call with an INT 3FH and the overlay manager will never have the chance to load the routine. Needless to say, this will have disastrous effects. LINK also structures the EXE file differently for overlaid executables. In short, there is a separate EXE header for each overlay segment, with fields specific to the overlay routine that follows it and a code-segment-only data block. I'll describe this in more detail below. Global Variables LINK inserts several global variables for the overlay manager's use. I'll list here the ones I've examined. ■ OVERLAY_AREA, OVERLAY_END. Two segment names defined by the linker at the very end of the CODE-class segments. OVERLAY_AREA is defined at the beginning of the overlay area. The end of the overlay area (first available paragraph after the largest overlay) is marked by OVERLAY_END. ■ $$CGSN. A variable containing a count of the number of separate overlay segments. It's not used in our overlay manager, which simply tries to find the requested overlay until there are no more in the file. ■ $$COVL. Contains the same value as $$CGSN. $$COVL is a limit on the number of overlays, whereas $$CGSN is used as a size parameter for the $$MPGSNBASE array. ■ $$EXENAM. The name of the executable file--base name and extension (which will be EXE). It's not used in my overlay manager, since the full executable name is available from DOS in the environment built for the program. ■ $$INTNO. The software interrupt number that is assigned for overlay manager operations. The /OVERLAYINTERRUPT switch on the LINK command line allows this to be changed from its default of 3FH. Since the calls will use this number, the overlay manager should use it to pick the interrupt vector to take over. ■ $$MAIN. The original program entry point. This is the entry point that would be used if the program were not overlaid. Once the program is overlaid, the entry is changed to $$OVLINIT (see below). The overlay manager initialization code can branch to $$MAIN once it has initialized itself and taken over the $$INTNO interrupt vector. ■ $$MPGSNBASE. An array of $$CGSN words, each containing the segment address of an overlay. $$MPGSNBASE[0] contains the segment address of the resident section of the program (that is, the PSP address + 10H), and the entries 1-$$CGSN contain OVERLAY_SEGMENT. $$MPGSNBASE is used to find the segment address of the requested overlay segment. ■ $$MPGSNOVL. An array of $$CGSN bytes, each one of which contains an overlay number corresponding to an entry in $$MPGSNBASE. $MPGSNOVL[0], the entry for the resident section, is null and each entry thereafter is equal to its index. ■ $$MPOVLLFA. A cache for overlay file offsets in the Microsoft overlay manager. There are as many doubleword entries as overlays, and $$MPOVLLFA[n] is either zero (as initialized) or set to the location in the file for the overlay numbered n. Although it could speed up large applications significantly, I decided not to use this in my overlay manager. For small tests the time spent searching the executable is minimal. ■ $$OVLBASE. A symbol equal to the address of OVERLAY_SEGMENT. This seems to be a conveniently initialized, alternate way of finding out where to load the overlay. I used its value instead of OVERLAY_SEGMENT; either would be appropriate. ■ $$OVLINIT. The address of the new entry point of the program after overlaying. The overlay manager should define its entry point as $$OVLINIT and perform a branch to $$MAIN after initialization. LINK sets $$MAIN to the original code's entry point. EXE File Structure As mentioned above, the EXE file from an overlay linker session is structured a bit differently from a normal DOS executable file. Each overlay segment (those modules that are contained within one set of parentheses make up an overlay segment) has its own EXE header. The header looks just like the normal EXE header, omitting some data (which is used only by the DOS loader) that applies only to the program as a whole. Initial stack settings, entry point, and so on, appear only in the initial header. The overlay number, however, changes in each subsequent header, and the length fields change as needed in order to find the next section. See the file EXEHDR.H for a full description of the structure. Of course, as with any LINK session, it's a good idea to create a MAP file with the /M option and examine the result. Overlay Manager It's possible, knowing the hooks that LINK provides, to write a simple overlay manager for languages that do not include support, particularly MASM. OVRMGR.ASM (see Figure 1) and RDOVLY.C (see Figure 2) make up an overlay manager that can be used with MASM programs and non-Microsoft languages. OVRMGR.ASM takes care of the overlay manager initialization and also contains the shell of the interrupt handler code. RDOVLY.C does all the dirty work. It actually reads the EXE file and reads in the overlay code at the address determined by OVRMGR.ASM. C code is much easier to write, especially at this level, and certainly easier to debug. Also, the C run-time facilities for file handling take a lot of load off the programmer for development effort. For those of you who have a Microsoft C compiler, the C function was translated into an assembly file, RDOVLY.ASM (see Figure 3), and the pertinent C run-time functions were added as assembly definitions in SUPPORT.ASM (see Figure 4). The code is commented and should be self-explanatory, but the following offers a rundown. OVRMGR assumes control at $$OVLINIT and proceeds to install ovly_int as the new interrupt service routine for the INT $$INTNO handler, saving the old vector for completeness. It's not restored by the code in its present form. It would be simple enough to define an add-on to your language's exit clean-up routines that restores the vector from old_ovint, where it's stored. Once the initialization is complete, OVRMGR performs a far jump to $$MAIN, the original program entry point. When an overlay is called, OVRMGR again assumes control at ovly_int. First, the handler checks to see if it's already in the middle of an overlay call; if it is, it prints an error message and terminates the program with error code 41H. (That is, this example manager does not allow an overlay to call another overlay or itself.) If the overlay manager is not currently active, the overlay number and offset are obtained from the original code stream; the segment address for the overlay area is obtained from the $$MPGSNBASE array, and read_overlay_section is called to read in the overlay. On return, if there was an error reading the file, OVRMGR exits with error code 42H; otherwise, the overlay may be called. I will go into more detail on this below. The read_overlay_section routine is reasonably straightforward. First, the executable file is opened, using the executable_name variable that was set by the original program. The variable executable_name is an ASCIIZ string (a C-style string) containing the full pathname of the executable file; this is easily obtained from a C program and is located just after the environment passed to any process. Many languages have argument-string arrays like C's argv[ ] array, so I won't dwell on how to create executable_name. It must, however, be defined by the program that uses the overlay manager. The example assembly main routine, _main, calls a procedure named fill_exe_name to initialize executable_name (it's allocated in OVRMGR.ASM). Be careful if you try to run this under SYMDEB. I found out, much to my chagrin, that SYMDEB eats the path specifiers from the front of the executable name stored in the environment. When running OVASM.EXE from the DOS command line, the whole path is placed in the environment. After opening the file that was named by executable_name, read_overlay_section determines the file size for later use. The lseek function is used to find the file size, in contrast to the expected method of using the file length or tell functions. Once I decided to prototype in C, I tried to limit the number of run-time calls made (since I needed to program them in assembly language anyway). Following this, the file pointer position is returned to the beginning of the file, and read_overlay_section begins an endless loop to search for the requested overlay. If there is an error at any point during the search, read_overlay_section( ) prints an error message using the custom routine errputs, closes the executable, and returns with a nonzero error code (in this case, 1). Errors include read errors, unrecognizable information in the executable file, and overlay not found. Presence of the file that is named in executable_name is assumed. If the file is not present (for example, if it's on a floppy that's been removed), however, the read of the executable header will fail, returning an error. When read_overlay_section finds the requested overlay, it reads that section of the file into the area pointed to by the ovarea parameter. Then it must relocate the code. Each segment reference in the code must be offset by the program load address. This is the same relocation performed by COMMAND.COM when it loads the program initially, but only for the root section--the overlay manager is responsible for relocating the loaded overlay. In order to offset each reference in the program by the load address, $$OVLINIT sets the variable pspaddr for use during relocation. The program load address is always the address of the PSP plus 10H, since the PSP is 100H bytes long. The variable, pspaddr, and the relocation table located in the file header are used by read_overlay_section in order to locate the overlay. The read_overlay_section handle does the relocation in the while (eh.num_relocs) loop; it continues until all relocation entries in the table have been used. Each relocation fix-up is done by adding the segment part of the relocation entry to the segment part of the load address (pspaddr + 0x10). The resultant far pointer is the address of the target word of the relocation. The pointer points into a code at a segment reference that must be fixed up. Once the pointer is formed, read_overlay_section adds the program load address (pspaddr + 0x10) to this target word, changing 0-relative segment references to actual absolute segment references. Notice the buffering of relocation entries; it is one of the reasons I decided to write the routine in C. Once the relocation is finished, the executable file is closed and control returns to OVRMGR.ASM with a return code of 0. (If there was an error in the load, as explained above, the return code would have been 1 and ovly_int would print an error message and die.) If there was no error, the address of return_from_ov is pushed on the stack so that the RETF from the overlaid routine will return to return_from_ov (thus explaining the choice of label). Next, the address of the overlaid routine itself is pushed onto the stack and a RETF is executed, causing a jump to the overlaid, relocated routine. I did this mainly because I'm already pushing things; a jump through a far pointer would have been just as easy. When the overlaid routine is done, it executes a RETF (remember that we are working strictly in far code). Control transfers to return_from_ov, which cleans up, resets the flag that indicates that we're no longer in the overlay manager, and returns control to the mainline program, just after the call to the overlaid routine. SUPPORT.ASM contains the assembly functions that supplant the C run-time functions used in RDOVLY.ASM: errputs, myopen, myclose, mylseek, and myread. OVRMGR.ASM contains the overlay initialization and interrupt handler and RDOVLY.C translates to RDOVLY.ASM, completing the simple overlay management package. Test Code Several test routines are provided with the overlay manager, both in assembly language and in C. The files OV.C, P1.C, and P2.C (see Figure 5) comprise a simple test, and OVASM.ASM, P1ASM.ASM, and P2ASM.ASM (see Figure 6) are similar test routines in assembly language. If you have Microsoft C Version 5.1 and MASM Version 5.1, you can remake the entire package in C or assembly language using MAKE and the sample make files, OV and OVASM (see Figure 7). Of course MASM and LINK must be available in your DOS executable path, you must have a version of LINK that supports overlaying, and so on. The RDOVLY.C source file, which eventually becomes the RDOVLY.ASM assembly language source, is processed by a small utility called SMERGE (see SMERGE.C listed in Figure 8), which merges the C source into the /Fa assembly language output from Microsoft C. SMERGE also removes an annoying external definition, an EXTRN _acrtused:ABS, which is issued by the compiler. Reasonably portable source for SMERGE.C is provided for those with C compilers. Limitations The code given here isn't sufficient for a really full-featured overlay manager. For one thing, the error handling could be improved. For instance, there is currently no provision for handling the case in which a floppy disk containing the executable file is removed during program execution. Granted, most PC users have hard disks, and the programs that will benefit from overlay management often live on those hard disks, but a specific error handler could be added for a really bulletproof package. The Microsoft overlay manager, for instance, requests either the floppy disk or a new pathname for the executable file. A more serious limitation is that only one overlay can be active at a time. This is due to the way information is stored in the overlay manager and the way returns are processed. It would be a fairly simple matter to modify OVRMGR.ASM to support an arbitrary number of overlays. The Microsoft C overlay manager supports up to 128 nested overlay calls. I'll leave this as an exercise for the investigative and adventurous reader. Microsoft notes, in the MASM documentation, that CodeView is now compatible with the overlay manager; the manual even mentions that overlaying may be necessary to get a large application to run under CodeView. In fact, the Microsoft overlay manager contains special hooks into CodeView and DEBUG so that special calls are made to both in order to map in new symbols. This overlay manager clearly can't support undocumented features, but I've found that debugging is still possible--it's just not as convenient--by using a symbolic debugger's capabilities for loading symbol files after the program has been loaded. Just rip out the sections of the MAP file that relate to the overlaid routines. Since data segments aren't overlaid, the problem usually isn't too limiting; data labels are the things I most often want to see in the disassembly. But it's worth noting that debugging an overlaid program can be interesting. Using overlay management offers one solution to the problem of limited code memory. This article has offered some insight into programming techniques for limited code memory by discussing Microsoft's solution for far code program development in addition to providing MASM programmers with a way to use it. While this particular overlay manager may not be useful to you, a modification along the lines suggested above might be useful. Overlay management is definitely an area worth exploring. Programming Assembly Language Routines in C Programming in assembly language has undeniable advantages; the low-level control, raw speed, and small code size just can't be equaled by a high-level language. But assembly programming can be terrible drudgery, prototyping in assembly can be painful (even for experienced assembly language programmers), and routines that involve array access, looping with complex termination conditions, or file I/O can involve lots of error-prone code. For these reasons, I decided to write the initialization and shell of the overlay interrupt handler in MASM and use Microsoft C for the main body of the routine. The result is the function read_overlay_section contained in the file RDOVLY.C. Since I also wanted to present an overlay manager for assembly language programmers, I proceeded to rewrite C portions of the routines in assembly--until it occurred to me that the Microsoft C /Fa option (to generate a MASM-compatible assembly language output file) could write the code for me, with some help. The first thing I had to do was rewrite the run-time routines I needed from the C run-time library in assembly language. This was easy with a bit of advance planning. For instance, since I was aware of how easily I could write an lseek function using DOS function call 42H, I used that function instead of the C function tell to get the current file position. I also knew I would need lseek in other places, and the proper form of lseek returns the same value as tell. To make the rewriting job easier, I limited file I/O to level 1 I/O, which is most like DOS file I/O. The small routines in SUPPORT.ASM are those that take the place of the C run-time routines as well as a special print to stderr function (errputs). Switches for the Microsoft C compiler driver, CL, are contained in both example make files. /c instructs CL to compile the file but doesn't go on to the link step, and /Fa and /Fonul cause the assembly language output to appear while sending any OBJ output to the NUL device, effectively discarding it. /AL causes the compiler to use large-model coding; that is, far pointers for data and code. LINK requires this when overlaying code. /Gs suppresses generation of calls to _chkstk, the stack-overflow-check routine, since this is a hidden run-time routine that isn't duplicated in my assembly code. /Zl suppresses the default library search, usually written to the OBJ file. This is probably not needed here, since I'm generating ASM files directly, but I'm careful. Finally, since I planned on including the assembly output, I used /Od to suppress all optimizations, so the output code would be clearer. Since the ASM output contains references to the original C source line, I decided to merge the C source and the compiler-generated assembly source to make it easier to see what's happening in the assembly output. SMERGE.C is a program written to automate the merging process, replacing comments of the form ; Line <n> with the actual nth line of the C source. When I discovered that CL inserts an extra data declaration for a variable _acrtused (whose purpose I never discovered), I decided to remove that declaration with SMERGE as well. Thus, CL and SMERGE together make a clearer, commented assembler file. I didn't hand optimize anything in the result. It's possible that performance could be improved by doing so, and certainly more mnemonic labels could be added by hand. A first glance at the code generated by CL indicated that it wasn't unclear, so I decided to leave well enough alone. The experiment worked well with RDOVLY.C. I was able to write a functional overlay manager in an afternoon, tweak its performance easily by using C, and then write an assembly-language-only version in just a few more hours. Had I started writing in assembly language, changing the overlay section reader's error handling and buffering of relocation records would have been much more difficult. But C is especially suited for such design modification and is close enough kin to assembly to generate reasonably efficient output. Give prototyping in C a serious try; you'll like it. Figure 1 .model LARGE extrn $$INTNO:byte extrn $$MPGSNBASE:word extrn $$MPGSNOVL:byte extrn $$MPOVLLFA:word extrn $$OVLBASE:byte extrn $$MAIN:far extrn _read_overlay_section:far extrn _errputs:far .data _pspaddr dw ? ;for relocation with read_overlay_section old_ovint dw ? ;old overlay interrupt offset dw ? ;and segment _executable_name db 80 dup (0) ;space for full pathname cantmap db 'Can''t map overlay!...exiting',13,10,0 reentry db 'Already mapping an overlay!...exiting',13,10,0 .code public $$OVLINIT public _executable_name public _pspaddr public ax_save,bx_save,cx_save,es_save,ret_ip,ret_cs public req_ov,ov_ofs,ov_seg,in_ovly_mgr,ovly_int public return_from_ov ax_save dw ? ;save area for used registers bx_save dw ? cx_save dw ? es_save dw ? ret_ip dw ? ;original return address ret_cs dw ? ;(from call to overlaid routine) req_ov db ? ;requested overlay number ov_ofs dw ? ;address to call after mapping ov_seg dw ? in_ovly_mgr db 0 ;flag to indicate already mapping $$OVLINIT label far push ax push bx push dx push es push ds ;need to use ds:dx mov ax,DGROUP mov ds,ax mov ax,es ;initialize _pspaddr mov _pspaddr,ax mov al,[$$INTNO] ;get interrupt number mov ah,35h ;get overlay number interrupt vector int 21h mov old_ovint,bx mov old_ovint+2,es mov bx,cs mov ds,bx mov dx,offset ovly_int ;new interrupt mov ah,25h int 21h ;install new int handler pop ds pop es pop dx pop bx pop ax jmp $$MAIN ;go to mainline code ;** ;** INTERRUPT HANDLER ;** ovly_int proc far cmp cs:in_ovly_mgr,0 ;are we not here already? je ok ;right ; Error if we're already in overlay manager! mov ax,offset reentry ;error message push ds push ax call _errputs add sp,4 mov ax,4c41h ;exit with error code 41h int 21h ok: mov cs:in_ovly_mgr,1 ;note that we're here mov cs:ax_save,ax mov cs:bx_save,bx mov cs:cx_save,cx mov ax,es mov cs:es_save,ax pop bx ;bx = return ip push bx add bx,3 ;adjust for extra stuff mov cs:[ret_ip],bx ;save it pop bx ;get original return address back pop ax ;return cs mov cs:[ret_cs],ax ;save it mov es,ax ;es:bx -> bytes after INT pop ax ;flags, discard xor ah,ah mov al,byte ptr es:[bx] ;get requested overlay number inc bx ;move to next item mov byte ptr cs:req_ov,al ;save it mov cx,word ptr es:[bx] ;get offset in overlay segment mov cs:[ov_ofs],cx ;save it push ax ;p3 for C function mov bx,ax shl bx,1 ;* 2 add bx,offset $$MPGSNBASE ;add to base of ;segment table mov bx,[bx] mov ov_seg,bx push bx ;push the segment for p2 mov ax,0 ;p2, low word (0) push ax call _read_overlay_section ;call the pup! add sp,6 ;get rid of parms cmp ax,0 ;error return? je no_error ;no ; Error if can't map this overlay! mov ax,offset cantmap ;error message push ds push ax call _errputs add sp,4 mov ax,4c42h ;exit with error code 42h int 21h no_error: push cs mov ax,offset return_from_ov ;trick up return ;to our code push ax mov ax,seg $$OVLBASE ;set up call to overlay code push ax mov ax,ov_ofs push ax retf ;jump to overlay code... return_from_ov: ;..and come back here mov ax,ret_cs ;restore original return address push ax mov ax,ret_ip push ax mov ax,cs:es_save ;and original reg values mov es,ax mov ax,cs:ax_save mov bx,cs:bx_save mov cx,cs:cx_save mov cs:in_ovly_mgr,0 ;we're not here anymore ret ovly_int endp end Figure 2 #include "exehdr.h" /* externally-defined full pathname of executable */ #define min(a,b) ((a) < (b)) ? (a) : (b) #define max(a,b) ((a) < (b)) ? (a) : (b) #define OPENMODE_RO 0 #define MK_FP(a,b) ((void far *)( ((unsigned long)(a) << 16) | (unsigned)(b) )) extern char executable_name[]; extern int pspaddr; void errputs(char far *s); long mylseek(int handle, long offset, int type); int myread(int handle, void far *buf, int len); int myopen(char far *name, int mode); int myclose(int handle); int read_overlay_section( void far *ovarea, /* buffer into which to read him */ int requested_overlay_number /* number of overlay to read */ ) #define RELOC_BUF_SIZE 32 { struct reloc_entry_ reloc_buf[RELOC_BUF_SIZE]; struct reloc_entry_ *reloc_ptr; int reloc_chunk; unsigned far *target_ptr; struct exehdr_ eh; int i; long startpos, nextpos, filesize; long sectionsize, imagesize; int executable_handle; /* open executable */ executable_handle = myopen(executable_name,OPENMODE_RO); /* determine file size */ filesize = mylseek(executable_handle,0L,2); /* search from beginning of file */ mylseek(executable_handle,0L,0); while (1) { /* use mylseek() to avoid calling runtime functions */ startpos = mylseek(executable_handle,0L,1); if (myread(executable_handle,&eh,sizeof(eh)) != sizeof(eh)) { errputs("Something's wrong...can't read EXE header\r\n"); myclose(executable_handle); return(1); } if (eh.M_sig != 'M' || eh.Z_sig != 'Z') { errputs("Found non-EXE signature!\r\n"); myclose(executable_handle); return(1); } if (eh.remain_len == 0) sectionsize = (long)eh.page_len * 512; else sectionsize = (long)(eh.page_len - 1) * 512 + eh.remain_len; if (eh.overlay_number == requested_overlay_number) { /* found ours...load and fix up */ /* move to executable image */ mylseek(executable_handle, startpos + eh.hsize * 16, 0); myread(executable_handle, ovarea, (int)(sectionsize - eh.hsize * 16)); /* fix up relocations in the loaded overlay */ mylseek(executable_handle,startpos + (long)eh.first_reloc,0); while (eh.num_relocs) { reloc_chunk = min(RELOC_BUF_SIZE,eh.num_relocs); myread(executable_handle,reloc_buf,reloc_chunk * sizeof(struct reloc_entry_)); eh.num_relocs -= reloc_chunk; for (i = 0; i < reloc_chunk; i++) { reloc_ptr = reloc_buf + i; target_ptr = MK_FP(pspaddr + 0x10 + reloc_ptr->segment, reloc_ptr->offset); *target_ptr += pspaddr + 0x10; } } myclose(executable_handle); return 0; } else { nextpos = startpos + sectionsize; /* round up to 512-byte bound */ nextpos = (nextpos + 511L) & ~511L; if (nextpos >= filesize) { myclose(executable_handle); return 1; } mylseek(executable_handle,nextpos,0); } } myclose(executable_handle); return 1; } Figure 4 .model LARGE,C .code public errputs errputs proc far C uses ax bx cx di ds es, string:ptr les di,string ;es:di -> string push di ;save original offset mov al,0 ;terminator byte mov cx,0100h ;length to examine for null repne scasb ;look for it pop ax ;ax = orig pointer sub di,ax dec di ;di = length mov cx,di mov bx,2 ;stderr mov dx,ax ;buffer address mov h,40h ;write to file int 21h ret errputs endp mylseek proc far C uses bx cx,handle:WORD,ofs:DWORD,whence:WORD public mylseek mov ax,whence mov ah,42h mov bx,handle push es les dx,ofs mov cx,es pop es int 21h ;result left in dx:ax, nicely jnc lseekdone mov ax,-1 lseekdone: ret mylseek endp myread proc far C uses bx ds dx, handle:WORD, buf:PTR,len:WORD public myread mov ah,3fH mov bx,handle mov cx,len lds dx,buf int 21h ;result nicely in ax for return jnc readdone mov ax,-1 readdone: ret myread endp myopen proc far C uses ds dx, fname:PTR, mode:WORD public myopen mov ax,mode mov ah,3dH lds dx,fname int 21h ;result nicely in ax for return jnc opendone mov ax,-1 opendone: ret myopen endp myclose proc far C uses bx, handle:WORD public myclose mov ah,3eH mov bx,handle int 21h ;result nicely in ax for return ret myclose endp end Figure 5 OV.C #include <stdio.h> #include <string.h> void p1(void); void p2(void); extern char executable_name; void main(int argc, char **argv) { int i; strcpy(&executable_name,argv[0]); argc = argc; printf("This is main\n"); for (i = 0; i < 1000; i++) { p1(); p2(); } } P1.C #include <stdio.h> void p1() { printf("In p1, whose address is %Fp\n",p1); printf("\t 3 * 3 + 9 = %d\n",3*3+9); printf("\t 4 * 4 / 5 = %d\n",4*4/5); } P2.C #include <stdio.h> void p2() { printf("In p2, whose address is %Fp\n",p2); printf("\t 10 %% 3 = %d\n",10 % 3); } Figure 6 OVASM.ASM extrn _errputs:far, p1:far, p2:far extrn _executable_name:byte _text segment public 'CODE' _text ends _data segment public 'DATA' _data ends public fill_exe_name,ENVSEG,main_msg, public call_loop,envstr_loop public fname_loop ENVSEG equ 2CH ;loc'n of environment pointer DGROUP group _data,_stack _text segment assume cs:_text,ds:DGROUP,ss:DGROUP _main proc far mov ax,DGROUP mov ds,ax call far ptr fill_exe_name lea ax,main_msg push ds push ax call far ptr _errputs add sp,4 mov cx,1000 call_loop: call far ptr p1 call far ptr p2 loop call_loop mov ax,4c00h int 21h _main endp fill_exe_name proc far push es push di push ds push si push ax mov ax,es mov ds,ax mov ax,DGROUP:ENVSEG ;point at env mov ds,ax mov si,0 mov ax,seg _executable_name ;point at dest string mov es,ax lea di,DGROUP:_executable_name envstr_loop: lodsb ;get first env cmp al,0 ;end of string? jnz envstr_loop ;no, keep looking cmp byte ptr ds:[si],0 ;end of env? jnz envstr_loop ;no, get another string add si,3 ;yes: go past, 2 more fname_loop: movsb ;move a byte cmp byte ptr ds:[si],0 ;end of name? jnz fname_loop ;no, keep it up mov byte ptr es:[di],0 ;store zero terminator pop ax pop si pop ds pop di pop es ret fill_exe_name endp _text ends _data segment main_msg db 'This is main...',13,10,0 _data ends _stack segment stack 'STACK' dw 200 dup (?) _stack ends end _main P1ASM.ASM extrn _errputs:far .model LARGE .data p1_msg db 'This is p1...',13,10,0 .code p1 proc far public p1 lea ax,p1_msg push ds push ax call _errputs add sp,4 ret p1 endp end P2ASM.ASM extrn _errputs:far .model LARGE .data p2_msg db 'This is p2...',13,10,0 .code p2 proc far public p2 lea ax,p2_msg push ds push ax call _errputs add sp,4 ret p2 endp end Figure 7 OV ov.obj: ov.c cl /c /AL /Fm ov.c p1.obj: p1.c cl /c /AL /Fm p1.c p2.obj: p2.c cl /c /AL /Fm p2.c ovrmgr.obj: ovrmgr.asm masm /MX ovrmgr.asm; rdovly.asm : rdovly.c exehdr.h # /c compile only # /Fa create .ASM output # /Fonul don't create object (leave that for MASM) # /Al large model # /Gs no stack checking # /Zl no default library search # /Od no optimization (for code clarity) cl /c /Fa /Fonul /Od /AL /Gs /Zl rdovly.c smerge rdovly.asm rdovly.c rdovly.mrg del rdovly.asm ren rdovly.mrg rdovly.asm rdovly.obj : rdovly.asm masm /mx rdovly; support.obj : support.asm masm /mx support; # Note - the ov.exe : ... line cannot break but had to be broken # here to fit on page ov.exe : ov.obj support.obj rdovly.obj ovrmgr.obj p1.obj p2.obj ovrmgr.obj link /m/li ov+support+rdovly+(p1)+(p2)+ovrmgr,ov,ov.map; OVASM ovasm.obj: ovasm.asm masm /mx ovasm.asm; p1asm.obj: p1asm.asm masm /mx p1asm; p2asm.obj: p2asm.asm masm /mx p2asm; ovrmgr.obj: ovrmgr.asm masm /MX ovrmgr.asm; rdovly.asm : rdovly.c exehdr.h # /c compile only # /Fa create .ASM output # /Fonul don't create object (leave that for MASM) # /Al large model # /Gs no stack checking # /Zl no default library search # /Od no optimization (for code clarity) cl /c /Fa /Fonul /Od /AL /Gs /Zl rdovly.c smerge rdovly.asm rdovly.c rdovly.mrg del rdovly.asm ren rdovly.mrg rdovly.asm rdovly.obj : rdovly.asm masm /mx rdovly; support.obj : support.asm masm /mx support; # Note - the ov.exe : ... and link lines cannot break but had # to be broken here to fit on page ovasm.exe : ovasm.obj support.obj rdovly.obj ovrmgr.obj p1asm.obj p2asm.obj ovrmgr.obj link /m/li ovasm+support+rdovly+(p1asm)+(p2asm)+ ovrmgr,ovasm,ovasm.map; Figure 8 /* smerge - merge source and assembly files */ #include <stdio.h> main(argc,argv) int argc; char *argv[]; { FILE *csrc, *asmsrc, *mergefile; char buffer[128]; long sline, line, totslines; if (argc < 4) { printf("usage: smerge asmsrc csrc mergefile\n"); exit(1); } sline = 0; asmsrc = fopen(argv[1],"r"); csrc = fopen(argv[2],"r"); mergefile = fopen(argv[3],"w"); setvbuf(csrc,NULL,_IOFBF,8192); setvbuf(asmsrc,NULL,_IOFBF,8192); setvbuf(mergefile,NULL,_IOFBF,8192); totslines = 0; while (fgets(buffer,128,csrc) != NULL) totslines++; fseek(csrc,0L,SEEK_SET); while ((fgets(buffer,128,asmsrc)) != NULL) { /* special hack for MSC -> assembly ... kill the "__acrtused" extrn */ if (strstr(buffer,"__acrtused") != NULL) continue; if (strncmp(buffer,"; Line",6) == 0) { sscanf(buffer,"; Line %ld",&line); if (line <= totslines) { while (sline < line) { fgets(buffer+1,128,csrc); fputs(buffer,mergefile); ++sline; } } } else fputs(buffer,mergefile); } fclose(asmsrc); fclose(csrc); fclose(mergefile); } Extended Memory Specification 2.x: Taking Advantage of the 80286 Protected Mode Chip Anderson The IBM PC and compatibles support three types of memory--conventional, expanded, and extended. Expanded memory has been widely supported by programmers for a number of years; extended memory, on the other hand, has been largely ignored. Now a new industry standard, known as the Extended Memory Specification (referred to herein as XMS), has made that memory more readily accessible to all programmers working in the DOS1 environment. This article takes a look at the evolution of extended memory and examines how XMS provides more memory for DOS applications. Extended Memory Issues DOS programmers have been slow to take advantage of extended memory for two primary reasons. The first is that until recently the number of machines that would not support extended memory (that is, 8088- and 8086-based machines) was far greater than the number that would. Most programmers, therefore, had to ignore the additional protected-mode capabilities of the 80286, since most copies of programs were purchased to run on the 8088 or 8086 machines. But earlier in 1989, the sales of 286- and 386-based machines, many of which contained one or more megabytes of extended memory, began to surpass those of 8086- and 8088-based machines. Due to the popularity of the 286/386 machines, developers will certainly write programs to take advantage of extended memory. The second reason programmers were reluctant to provide access to extended memory was due to the lack of an accepted standard for accessing it. A standard for using extended memory must define methods that will allow programs to reserve part of the available extended memory for their exclusive use (allocation), copy data to and from extended memory (transfer), and release the reserved memory when finished (deallocation). Two conflicting pseudostandards for accessing extended memory have evolved over time. The IBM VDISK.SYS, a RAM disk program released with DOS Version 3.0, allocates extended memory using the VDISK method. Programs such as Microsoft(R) RAMDRIVE.SYS use an alternate scheme known as the INT 15h method. Examining the details of how these two methods implement the allocation, transfer, and deallocation of extended memory demonstrates why using extended memory has remained difficult. Allocation VDISK.SYS was the first program to use extended memory and assumed that no other programs would use it. VDISK stores a data structure called a VDISK Header just above 1Mb. The VDISK Header indicates the amount of memory used by the first memory disk. If two VDISKs are installed, the second instance will install its header just above the end of the first disk. This process is called a bottom-up allocation scheme (see Figure 1). The INT 15h method approaches allocation from the opposite direction. When a DOS program uses INT 15h, Function 88h, the ROM BIOS returns the amount of extended memory in the computer. Using the INT 15h method, a DOS program reserves a portion of that memory by intercepting or hooking all subsequent INT 15h calls. After hooking INT 15h, the program gets control whenever another program executes INT 15h. This means that the first program can "lie" to all subsequent programs about the amount of extended memory that is installed. A consequence of this is that by returning a smaller amount, the first program guarantees that it has exclusive access to the remaining memory. A PC/AT(R) with 2Mb of RAM, for example, returns 1408Kb of memory when INT 15h, Function 88h is executed (since 2048Kb - 640Kb = 1408Kb). When a 1Mb RAMDrive is installed, it hooks INT 15h so that all subsequent calls to INT 15h, Function 88h, return 384 (since 1408Kb - 1024Kb = 384Kb). If any other program uses INT 15h, Function 88h, it is fooled into thinking that the machine only contains 1Mb of RAM. RAMDrive is then free to store its data in the memory between 1408Kb and 2048Kb. If two RAMDrives are installed, the second instance also hooks INT 15h and has it tell other programs that even less memory is installed. Notice that the INT 15h method allocates memory in a top-down manner (see Figure 2). Since these methods allocate extended memory in different directions, they are incompatible. Another problem is that both methods assume that all available extended memory is contiguous, but several IBM compatibles have noncontiguous blocks of extended memory. Transfer Neither the VDISK method nor the INT 15h method specify a way in which to move data between conventional and extended memory. Therefore, programmers must write their own subroutine for doing this. But the only way of moving data into and out of extended memory is to quickly switch the microprocessor into 286 protected mode, perform the copy, and switch back into real mode. Realizing the difficulty in writing such a routine, IBM engineers provided a ROM BIOS function (INT 15h, AH=87h) to perform extended memory transfers in the original PC/AT computer. Unfortunately, the IBM ROM BIOS transfer subroutine has a major design problem--it disables hardware interrupts while data is being transferred. This means that during long transfers, time-dependent programs such as communications packages and networks will lose data, especially when they are running under multitasking environments such as Microsoft Windows/286 and Desqview. Programs that use the IBM transfer routine must break large transfers into many smaller chunks, thus significantly slowing an already slow process. Deallocation Unfortunately, there is no way to free allocated extended memory under either the VDISK or the INT 15h method of managing extended memory. To see why, consider the following scenario (see Figure 3). Running under a real-mode multitasking environment, for example Windows/286, Program A allocates 100Kb of extended memory, Program B allocates 50Kb of extended memory, and Program C allocates 75Kb of extended memory. How would each allocation scheme work if Program B finishes executing and wishes to free its 50Kb? Because the VDISK method is a bottom-up allocator, it assumes that no holes occur below the last block allocated. In this case, any future program that wishes to allocate extended memory has to find and use the header that Program C installed to determine how much extended memory is free and where it is located. The header does not indicate if any holes exist. The INT 15h method requires that the allocating program install an interrupt hook that fools other programs about the amount of available extended memory. Once they are installed, however, interrupt hooks cannot be removed without risk of a system crash. Not only does this fact prevent deallocation of extended memory, but, since interrupt hooks are part of a program, it means that the allocating program cannot be removed either. Therefore, only terminate-and-stay-resident (TSR) programs and device drivers can safely use the INT 15h method. High Memory Complicating the extended memory picture even further, developers at Microsoft discovered a simple hardware trick that allows DOS programs to access an additional 64Kb directly. In real mode, 286-based computers only enable 20 address lines and thereby emulate the 1Mb address space of 8086-based computers. By selectively enabling and disabling the computer's 21st address line, a DOS program running on a 286-based machine can access the first 64Kb of extended memory. This 64Kb area is called the High Memory Area (HMA). See "The High Memory Area: Addressing 64Kb More Memory in Real Mode," MSJ (Vol. 3, No. 6) for more information. Notice that the HMA must be located just above the 1Mb boundary and thus is incompatible with the VDISK allocation scheme. Introducing XMS In July 1988, after another period of lengthy discussions, Lotus(R), Intel(R), AST(R), and Microsoft (with input from many other vendors) finalized the Extended Memory Specification Version 2.0 (XMS 2.0). XMS 2.0 solves all of the problems associated with both the VDISK and INT 15h methods of accessing extended memory. It provides a set of functions that DOS programs can use to do the following: ■ reserve portions of extended memory for themselves ■ transfer any amount of data from conventional memory into extended memory or vice versa ■ release reserved extended memory when finished In addition, XMS 2.0 provides functions that: ■ reserve and protect the HMA ■ control the computer's 21st address line in a machine-independent manner ■ reserve and release any additional memory located between 640Kb and 1Mb (called Upper Memory Blocks) The XMS functions are typically added to the DOS system by installing an XMS driver via a "DEVICE=" command in the machine's CONFIG.SYS file. Anyone can get the Microsoft XMS driver, HIMEM.SYS, along with source code and the official XMS 2.0 specification by contacting the Microsoft Information Center at (800) 426-9400. Using XMS Functions Programs that wish to use XMS to access extended memory can do so by performing the following steps: 1. Determine if an XMS driver is installed. 2. Get the address of the driver's control function. 3. Set AH to the requested function's number. 4. Initialize other registers where appropriate. 5. Call the driver's control function. The first two steps are done via Interrupt 2Fh, the multiplex interrupt. Executing an INT 2Fh instruction with AX=4300h returns a value of 80h in AL if an XMS driver is installed. Programs can then get the address of the driver's control function by executing an INT 2Fh with AX=4310h. The driver will return the address in ES:BX. The program then has access to any of the 18 XMS functions (which are listed in the Extended Memory Specification sidebar). Some of these functions are defined in SCRXMS.ASM (Figure 4), a subset of the C library XMM.LIB that provides access to all XMS functions. XMM.LIB is included on the Microsoft XMS distribution diskette. Programs that need to store large amounts of data in extended memory only need to use the following XMS functions: 08h Query Free Extended Memory 09h Allocate Extended Memory Block 0Ah Free Extended Memory Block 0Bh Move Extended Memory Block These functions are demonstrated by the SCRSAVE program (see Figure 5), which shows how a DOS program can use XMS calls to store data in extended memory. Query Free Extended Memory (08h) returns two numbers--the total amount of free extended memory and the size of the largest contiguous free Extended Memory Block (EMB). Because most programs only work with contiguous data buffers, the second number is usually more important than the first number. Allocate Extended Memory Block (09h) attempts to allocate a contiguous EMB that is at least as big as the caller requested, in this case 8Kb of extended memory for the screen: lret = XMM_AllocateExtended(8); (See also _XMM_AllocateExtended in Figure 4.) If the allocation succeeds, a 16-bit EMB Handle is returned to the caller: xHandle = (WORD)lret; If there is less than 8Kb available, then an appropriate error message is generated and the memory is not allocated: if (!xHandle) { printf("Allocation Error: %X/n", XMSERROR(lret)); exit(-1); } The caller can use this handle in subsequent XMS calls as a nickname for the EMB. 0000h is reserved as a special type of EMB handle that refers to memory below the 1Mb boundary. Move Extended Memory Block (0Bh) can be used to move data from any part of memory to any other part. Typically, it is used to move data from conventional memory into extended memory or vice versa. The caller passes the function a pointer to a data structure, which contains pointers for the source and destination buffers along with the number of data words to transfer. In this case, SCRSAVE stores the contents of the test screen in an EMB. It then fills the screen with a test pattern: lret = XMM_MoveExtended (&MoveStruct); The function performs the transfer without disabling interrupts, thus side stepping the problems with INT 15h, Function 87h. In addition, HIMEM.SYS will use a faster version of the Move function when it is installed on an 80386-based machine, thus ensuring the fastest transfer rate regardless of the hardware platform. Free Extended Memory Block (0Ah) deallocates the previously allocated EMB. SCRSAVE retrieves the old contents of the screen from extended memory, restores the screen, and terminates: XMM_FreeExtended(xHandle); Since XMS does not use hooks or headers, deallocation is always possible. As with any reasonable memory management scheme, after each deallocation any adjacent blocks that are free are merged into one larger free block. The standardization of XMS and the development of the XMM function library have made extended memory easily accessible to DOS programmers. This means that you will begin to see more applications designed specifically to take advantage of the 286/386 extended memory capabilities. This article describes the extended memory functions. These functions will help give your programs instant access to more memory. Figure 4 ; SCRXMS.ASM - Adapted from the XMM C Library Routines (c) 1988 ; ;Microsoft Corp ; DATA SEGMENT _DATA segment WORD PUBLIC 'DATA' XMM_Initialized dw 0 ; 1 if the XMS driver has ; been found XMM_Control label dword ; Points to the XMS control function dw offset _XMM_NotInitialized dw seg _TEXT _DATA ends DGROUP GROUP _DATA ; CODE SEGMENT _TEXT segment WORD PUBLIC 'CODE' assume cs:_TEXT assume ds:DGROUP ;------------------------------------------------------------------- ; _XMM_NotInitialized ; Called by default if the program hasn't called _XMM_Installed ; or if no XMS driver is found. ;------------------------------------------------------------------- _XMM_NotInitialized proc far xor ax,ax ; Simulate an XMS "Not ; Implemented" error mov bl,80h ret _XMM_NotInitialized endp ;------------------------------------------------------------------- ; BOOL _XMM_Installed(void); ; Called to initialize the XMM library routines. ;------------------------------------------------------------------- public _XMM_Installed _XMM_Installed proc near ; Have we already been initialized? cmp [XMM_Initialized],0 jne Already_Initialized ; Yes, return TRUE ; Is an XMS driver installed? mov ax,4300h int 2Fh cmp al,80h jne NoDriver ; No, return FALSE ; Get and store the address of the driver's control function. mov ax,4310h int 2Fh mov word ptr [XMM_Control],bx mov word ptr [XMM_Control+2],es ; Set XMM_Initialized to TRUE. inc [XMM_Initialized] Already_Initialized: NoDriver: mov ax,[XMM_Initialized] ret _XMM_Installed endp ;------------------------------------------------------------------- ; WORD _XMM_Version(void); ; Returns the driver's XMS version number. ;------------------------------------------------------------------- public _XMM_Version _XMM_Version proc near xor ah,ah ; Call XMS Function 0 call [XMM_Control] ; Sets AX to XMS Version ret _XMM_Version endp ;------------------------------------------------------------------- ; long _XMM_AllocateExtended(int SizeK); ; Allocates an Extended Memory Block and returns its handle. ;------------------------------------------------------------------- public _XMM_AllocateExtended _XMM_AllocateExtended proc near push bp ; Create a stack frame mov bp,sp ; Call the XMS Allocate function. mov ah,9 mov dx,[bp+4] ; [bp+4] is the parameter call [XMM_Control] ; Was there an error? or ax,ax jz AEFail ; Yup, fail mov ax,dx ; Return XMS Handle in AX mov dx,0 ; Zero out DX jnz AESuccess AEFail: mov dh,bl ; Put error return in DH AESuccess: pop bp ; Restore the stack ret _XMM_AllocateExtended endp ;------------------------------------------------------------------- ; long _XMM_FreeExtended(WORD Handle); ; Deallocates an Extended Memory Block. ;------------------------------------------------------------------- public _XMM_FreeExtended _XMM_FreeExtended proc near push bp ; Create a stack ; frame mov bp,sp ; Call the XMS FreeExtended function. mov ah,0Ah mov dx,[bp+4] ; [bp+4] is the parameter call [XMM_Control] xor dx,dx ; Zero DX ; Was there an error? or ax,ax jz FESuccess ; Nope, return mov dh,bl ; Yup, return err code in DH FESuccess: pop bp ; Restore the stack ret _XMM_FreeExtended endp ;------------------------------------------------------------------- ; long _XMM_MoveExtended(XMSMOVE *pInfo); ; Moves a block of data into/outof/within extended memory. ;------------------------------------------------------------------- public _XMM_MoveExtended _XMM_MoveExtended proc near push bp ; Create a stack frame mov bp,sp ; Call the XMS MoveExtended function. mov ah,0Bh mov si,[bp+4] ; [bp+4] is the parameter call [XMM_Control] xor dx,dx ; Zero DX ; Was there an error? or ax,ax jz MESuccess ; Nope, return mov dh,bl ; Yup, return err code in DH MESuccess: pop bp ; Restore the stack ret _XMM_MoveExtended endp _TEXT ends end Figure 5 SCRSAVE scrxms.obj: scrxms.asm masm scrxms; scrsave.obj: scrsave.c cl -c scrsave.c scrsave.exe: scrxms.obj scrsave.obj link scrsave+scrxms,,,slibcec; SCRSAVE.C /******************************************************************** SCRSAVE.C - Chip Anderson XMS Demonstration Program. Saves the contents of the text screen in extended memory, writes a test pattern on the screen, then restores the screen's original contents. 11/1/88 ********************************************************************/ #include "stdio.h" #define WORD unsigned int WORD XMM_Installed(void); WORD XMM_Version(void); long XMM_AllocateExtended(WORD); long XMM_FreeExtended(WORD); long XMM_MoveExtended(struct XMM_Move *); struct XMM_Move { unsigned long Length; unsigned int SourceHandle; unsigned long SourceOffset; unsigned int DestHandle; unsigned long DestOffset; }; #define XMSERROR(x) (unsigned char)((x)>>24) main() { int i,j; long lret; WORD xHandle = 0; struct XMM_Move MoveStruct; /* Display the credits. */ printf("SCRSAVE - XMS Demonstration Program\n"); /* Is an XMS Driver installed? */ if (!XMM_Installed()) { printf("No XMS Driver was found.\n"); exit(-1); } /* Is it a version 2.00 driver (or higher)? */ if (XMM_Version() < 0x200) { printf("This program requires an XMS driver version 2.00 or \ higher.\n"); exit(-1); } /* We've found a legal XMS driver. Now allocate 8K of extended memory for the screen. */ lret = XMM_AllocateExtended(8); xHandle = (WORD)lret; if (!xHandle) { printf("Allocation Error: %X\n", XMSERROR(lret)); exit(-1); } /* Copy the contents of the screen video buffer into the extended memory block we just allocated. */ MoveStruct.SourceHandle = 0; /* Source is in conventional memory */ MoveStruct.SourceOffset = 0xB8000000L;/*Text screen data is a B800:0000 */ MoveStruct.DestHandle = xHandle; MoveStruct.DestOffset = 0L; MoveStruct.Length = 8000L; /* 80 x 25 x 4 */ lret = XMM_MoveExtended(&MoveStruct); if (!(WORD)lret) { printf("Memory Move Error: %X\n", XMSERROR(lret)); exit(-1); } /* Fill the screen with garbage. */ for (i=0; i < 22; i++) for (j=64; j < (64+81); j++) printf("%c",j); /* Prompt the user. */ printf("Press Enter to restore the screen:"); getchar(); /* Restore the screen buffer. */ MoveStruct.DestHandle = 0; MoveStruct.DestOffset = 0xB8000000L; MoveStruct.SourceHandle = xHandle; MoveStruct.SourceOffset = 0L; MoveStruct.Length = 8000L; lret = XMM_MoveExtended(&MoveStruct); if (!(WORD)lret) { printf("Memory Move Error: %X\n", XMSERROR(lret)); exit(-1); } /* Free the extended memory buffer. */ XMM_FreeExtended(xHandle); printf("Program terminated normally."); exit(0); } Extended Memory Specification Get XMS Version Number (Function 00h) ARGS: AH = 00h RETS: AX = XMS version number BX = Driver internal revision number DX = 0001h if the HMA exists, 0000h otherwise This function returns with AX equal to a 16-bit BCD number representing the revision of the DOS Extended Memory Specification (XMS) that the driver implements (for example, AX=0235h would mean that the driver implemented XMS Version 2.35). Request High Memory Area (Function 01h) ARGS: AH = 01h If the caller is a TSR or device driver, DX = Space needed in the HMA by the caller in bytes If the caller is an application program, DX = FFFFh RETS: AX = 0001h if the HMA is assigned to the caller, 0000h otherwise This function attempts to reserve the 64Kb-by-16 byte high memory area (HMA) for the caller. If the HMA is currently unused, the caller's size parameter is compared to the /HMAMIN= parameter on the driver's command line. If the value passed by the caller is greater than or equal to the amount specified by the driver's parameter, the request succeeds. This provides the ability to ensure that programs that use the HMA efficiently have priority over those that do not. Release High Memory Area (Function 02h) ARGS: AH = 02h RETS: AX = 0001h if the HMA is successfully released, 0000h otherwise This function releases the HMA and allows other programs to use it. Programs that allocate the HMA must release it before exiting. When the HMA has been released, any code or data stored in it becomes invalid and should not be accessed. Global Enable A20 (Function 03h) ARGS: AH = 03h RETS: AX = 0001h if the A20 line is enabled, 0000h otherwise This function attempts to enable the A20 line. It should only be used by programs that have control of the HMA. The A20 line should be turned off via Function 04h (Global Disable A20) before a program releases control of the system. (Note: On many machines, toggling the A20 line is a relatively slow operation.) Global Disable A20 (Function 04h) ARGS: AH = 04h RETS: AX = 0001h if the A20 line is disabled, 0000h otherwise This function attempts to disable the A20 line. It should only be used by programs that have control of the HMA. The A20 line should be disabled before a program releases control of the system. Local Enable A20 (Function 05h) ARGS: AH = 05h RETS: AX = 0001h if the A20 line is enabled, 0000h otherwise This function attempts to enable the A20 line. It should only be used by programs that need direct access to extended memory. Programs that use this function should call Function 06h (Local Disable A20) before releasing control of the system. Local Disable A20 (Function 06h) ARGS: AH = 06h RETS: AX = 0001h if the function succeeds, 0000h otherwise This function cancels a previous call to Function 05h (Local Enable A20). It should only be used by programs that need direct access to extended memory. Previous calls to Function 05h must be canceled before releasing control of the system. Query A20 (Function 07h) ARGS: AH = 07h RETS: AX = 0001h if the A20 line is physically enabled, 0000h otherwise This function checks to see if the A20 line is physically enabled. It does this in a hardware-independent manner by seeing if memory wrap occurs. Query Free Extended Memory (Function 08h) ARGS: AH = 08h RETS: AX = Size of the largest free extended memory block in kilobytes DX = Total amount of free extended memory in kilobytes This function returns the size of the largest available extended memory block (EMB) in the system. (Note: The 64Kb HMA is not included in the returned value even if it is not in use.) Allocate Extended Memory Block (Function 09h) ARGS: AH = 09h DX = Amount of extended memory being requested in kilobytes RETS: AX = 0001h if the block is allocated, 0000h otherwise DX = 16-bit handle to the allocated block This function attempts to allocate a block of the given size out of the pool of free extended memory. If a block is available, it is reserved for the caller, and a 16-bit handle to that block is returned. The handle should be used in all subsequent extended memory calls. If no memory was allocated, the returned handle is null. (Note: Extended memory handles are scarce resources. Programs should try to allocate as few as possible at any one time. When all of a driver's handles are in use, any free extended memory is unavailable.) Free Extended Memory Block (Function 0Ah) ARGS: AH = 0Ah DX = Handle to the allocated block that should be freed RETS: AX = 0001h if the block is successfully freed, 0000h otherwise This function frees a block of extended memory that was previously allocated using Function 09h (Allocate Extended Memory Block). Programs that allocate extended memory should free their memory blocks before exiting. When an extended memory buffer is freed, its handle and all data stored in it become invalid and should not be accessed. Move Extended Memory Block (Function 0Bh) ARGS: AH = 0Bh DS:SI = Pointer to an Extended Memory Move Structure (see below) RETS: AX = 0001h if the move is successful, 0000h otherwise Extended Memory Move Structure Definition ExtMemMoveStruct struct Length dd ? ; 32-bit number of bytes ; to transfer SourceHandle dw ? ; Handle of source ; block SourceOffset dd ? ; 32-bit offset into ; source DestHandle dw ? ; Handle of ; destination block DestOffset dd ? ; 32-bit offset into ; destination block ExtMemMoveStruct ends This function attempts to transfer a block of data from one location to another. It is primarily intended for moving blocks of data between conventional memory and extended memory; however, it can be used for moving blocks within conventional memory and within extended memory. Length must be even. Although not required, WORD-aligned moves can be significantly faster on most machines. DWORD aligned moves can be even faster on 80386 machines. SourceHandle and DestHandle do not have to refer to locked memory blocks. (Note: If SourceHandle is set to 0000h, the SourceOffset is interpreted as a standard segment:offset pair that refers to memory that is directly accessible by the processor. The segment:offset pair is stored in Intel DWORD notation. The same is true for DestHandle and DestOffset.) If the source and destination blocks overlap, only forward moves (that is, those in which the source base is less than the destination base) are guaranteed to work properly. Programs should not enable the A20 line before calling this function. The state of the A20 line is preserved. This function is guaranteed to provide a reasonable number of interrupt windows during long transfers. Lock Extended Memory Block (Function 0Ch) ARGS: AH = 0Ch DX = Extended memory block handle to lock RETS: AX = 0001h if the block is locked, 0000h otherwise DX:BX = 32-bit linear address of the locked block This function locks an EMB and returns its base address as a 32-bit linear address. Locked memory blocks are guaranteed not to move. The 32-bit pointer is only valid while the block is locked. Locked blocks should be unlocked as soon as possible. Unlock Extended Memory Block (Function 0Dh) ARGS: AH = 0Dh DX = Extended memory block handle to unlock RETS: AX = 0001h if the block is unlocked, 0000h otherwise This function unlocks a locked EMB. Any 32-bit pointers into the block become invalid and should no longer be used. Get EMB Handle Information (Function 0Eh) ARGS: AH = 0Eh DX = Extended memory block handle RETS: AX = 0001h if the block's information is found, 0000h otherwise BH = The block's lock count BL = Number of free EMB handles in the system DX = The block's length in kilobytes This function returns additional information about an EMB to the caller. [Note: To get the block's base address, use Function 0Ch (Lock Extended Memory Block).] Reallocate Extended Memory Block (Function 0Fh) ARGS: AH = 0Fh BX = New size for the extended memory block in kilobytes DX = Unlocked extended memory block handle to reallocate RETS: AX = 0001h if the block is reallocated, 0000h otherwise This function attempts to reallocate an unlocked EMB so that it becomes the newly specified size. If the new block's size is smaller than the old one's, all data at the upper end of the old block is lost. Request Upper Memory Block (Function 10h) ARGS: AH = 10h DX = Size of requested memory block in paragraphs RETS: AX = 0001h if the request is granted, 0000h otherwise BX = Segment number of the upper memory block If the request is granted, DX = Actual size of the allocated block in paragraphs otherwise, DX = Size of the largest available upper memory block in paragraphs This function attempts to allocate an upper memory block (UMB) to the caller. If the function fails, the size of the largest free UMB is returned in DX. Release Upper Memory Block (Function 11h) ARGS: AH = 11h DX = Segment number of the upper memory block RETS: AX = 0001h if the block was released, 0000h otherwise This function frees a previously allocated UMB. When an UMB has been released, any code and data stored in it becomes invalid and should not be accessed. ERROR CODE INDEX If AX=0000h when a function returns and the high bit of BL is set, BL equals 80h if the function is not implemented 81h if a VDISK device is detected 82h if an A20 error occurs 8Eh if a general driver error occurs 8Fh if an unrecoverable driver error occurs 90h if the HMA does not exist 91h if the HMA is already in use 92h if DX is less than the /HMAMIN= parameter 93h if the HMA is not allocated 94h if the A20 line is still enabled A0h if all extended memory is allocated A1h if all available extended memory handles are in use A2h if the handle is invalid A3h if the SourceHandle is invalid A4h if the SourceOffset is invalid A5h if the DestHandle is invalid A6h if the DestOffset is invalid A7h if the Length is invalid A8h if the move has an invalid overlap A9h if a parity error occurs AAh if the block is not locked ABh if the block is locked ACh if the block's lock count overflows ADh if the lock fails B0h if a smaller UMB is available B1h if no UMBs are available B2h if the UMB segment number is invalid Copyright (c) 1988, Microsoft Corporation Exploring the Key Functions of the OS/2 Keyboard and Mouse Subsystems Richard Hale Shaw The OS/2 operating systems (referred to herein as OS/2) offer services for handling user input from both the keyboard and the mouse. This article will explore the KBD and MOU subsystems, each of which contains a core set of functions that you will be able to use regularly in most applications. We'll also glance at some of the lesser-used, more esoteric functions. Finally, we'll include a simple program that builds on the knowledge of threads and the VIO subsystem that you've obtained from previous articles in this series. It will show you how to incorporate OS/2 keyboard and mouse functions quickly and easily into your own multithreaded applications. KBD Subsystem Since most PC programs are highly interactive and rely heavily on effective interpretation of and efficient interaction with the keyboard, an application's ability to deal effectively with the keyboard is second only to its ability to handle its video output. OS/2 keyboard services therefore provide applications with basic keyboard input facilities plus a variety of additional keyboard functions. In theory, an application can use the DosOpen Application Programming Interface (API) routine to open its session's STDIN handle and receive character-based data from the keyboard. In practice, however, it makes more sense to use OS/2 keyboard services, particularly in highly interactive programs. Or, if you wish, you can bypass the keyboard subsystem entirely and go directly to the keyboard device driver via IOCTL services. The keyboard device driver, KBD$, provides the physical keyboard support for OS/2, which loads it from KBD01.SYS or KBD02.SYS. This device driver services the keyboard interrupts and translates keyboard scan codes into characters via the code page in use by the current screen group. KBD$ also works with the OS/2 kernel to create and maintain the device monitor chains for each screen group. The primary concern of the keyboard subsystem, however, is an application's ability to receive and process keystrokes. With OS/2 keyboard subsystem services, a program can obtain a key's ASCII code and scan code, the keyboard shift state, and a time stamp every time the user presses a key. The keyboard state affects all keyboard input functions and includes the status of the shift and toggle keys and the turnaround (end-of-input) character. It also affects whether characters are echoed and the character input mode (discussed below). The keyboard state can be retrieved and altered for the session in which the program is running. More advanced keyboard services provide code-page switching, keyboard monitoring, and a means of replacing the keyboard subsystem services with functions of your own. Code-page switching allows an application to switch the character set used for keyboard interpretation. Keyboard monitor services allow background applications to monitor keyboard inputs for other processes in the same screen group. Keyboard replacement services allow an application to replace many of the keyboard service routines with its own. Like its video services, OS/2 supplies the keyboard subsystem as dynamic-link libraries (DLLs) and several API services. Two DLLs support the keyboard API: KBDCALLS.DLL and BKSCALLS.DLL. KBDCALLS.DLL is known as the keyboard router. It contains the entry points for the KBD API and passes the calls to the subsystem in use in each screen group. This is usually BKSCALLS.DLL (the default subsystem). BKSCALLS.DLL contains the Basic Keyboard Subsystem and implements the KBD services. All or part of the Keyboard Subsystem may be replaced in each screen group. In "Using the OS/2 Video I/O Subsystem to Create Appealing Visual Interfaces," MSJ (Vol. 4, No. 3), you saw that the OS/2 video services are a superset of the IBM(R) PC ROM BIOS Int 10h function. Similarly, some OS/2 keyboard functions resemble ROM BIOS Int 16h. Each of the 16 keyboard services are prefixed with Kbd to designate them as part of the KBD subsystem. Even though many of them overlap some C standard library routines, they are more powerful. You can continue to use C library routines for generic keyboard input, but you will need to use KBD calls to gain full control of the keyboard. Logical Keyboards As you have seen in this series of articles, OS/2 "virtualizes" the resources of the computer. The OS/2 Session Manager executes every OS/2 application in its own screen group. A program started in the OS/2 Presentation Manager (referred to herein as PM) screen group will either execute in a PM window or in its own session. For VIO, this meant that each program or process can access a logical video buffer, even when running in a PM window. The same is true for applications that access the keyboard. Each screen group has a logical keyboard (the virtualized physical keyboard). Any process can access it without any preliminaries. When a user brings a screen group into the foreground, the logical keyboard is "bound" to the physical keyboard, and processes (or, more precisely, threads) in that screen group can read any key the user presses. Once the user moves the screen group into the background, the OS/2 operating system blocks any threads reading the keyboard in that screen group. OS/2 provides the logical keyboard automatically. Each KBD routine requires a keyboard handle. Like VIO, the default keyboard handle is 0, and an application's keyboard thread can call any KBD service with a handle of 0. There is no need to open a handle as long as the application isolates keyboard I/O to one thread (regardless of what else the thread may be doing). If the process is going to spread keyboard input among multiple threads, then it will have to perform some kind of synchronization. Unlike VIO or MOU however, KBD allows you to create multiple logical keyboard buffers for each session. Thus, you can have a process open an additional logical keyboard handle and perform its own keyboard input. You can use this capability when you want a program to perform keyboard input through a handle other than the one provided to the screen group. You can use the KbdOpen function to open a new logical keyboard and obtain a handle to access it. When you associate a logical keyboard handle with the physical keyboard, the handle has the keyboard focus. Processes in the same screen group can give or get the keyboard focus when necessary. To request the keyboard focus from another process in the same screen group, you can call KbdGetFocus. Note that you can only call KbdGetFocus in the foreground session; it will block if called from a background thread and it will remain blocked until the thread is in the foreground. KbdGetFocus will also block when another process has the focus and will not obtain the focus until the other process releases it. You can release the keyboard focus with KbdFreeFocus. Obtaining and granting the keyboard focus forces processes in the same session to use the keyboard on a serial basis: only one may get the keyboard focus at a time. If, however, OS/2 is blocking more than one thread in a session with KbdGetFocus, there is no way to predict which one will get the keyboard focus when it is released. If there are no KbdGetFocus calls pending when a thread calls KbdFreeFocus, the physical keyboard reverts to the default keyboard buffer. As I mentioned earlier, you don't have to open a keyboard handle; the default handle will usually do. A process that uses the default logical keyboard is, however, vulnerable to the interference of other processes in the same screen group that may also use the default handle. For instance, suppose one program asynchronously runs another program via DosExecPgm. If both processes simultaneously attempt to interact with the default keyboard, the results will be unpredictable. To protect against this, you can open a new handle, obtain the keyboard focus, and begin interacting with the keyboard. When you are done, you should release the keyboard focus and close the handle. There is no need for the process to worry about the integrity of keyboard data when each process uses its own logical keyboard handle. Incidentally, the OS/2 operating system uses KbdOpen and KbdGetFocus to obtain a default logical keyboard handle for each new session (which the session references with the default handle, 0). When you close a session, OS/2 deletes the logical keyboard that corresponds to the default handle. In summary, if your application performs keyboard input in only one of its threads or if your application is not going to share the keyboard with another application in the same session, you won't have to worry about keyboard focus. Otherwise, you should consider it. The same is true for PM. It will synchronize the access to the keyboard among the applications running in its screen group. Scan Codes As you probably know, the PC keyboard does not generate ASCII codes for the letters shown on the keys. Indeed, the keyboard does not know which characters are associated with which keys. Each time a key is pressed, the keyboard generates a value that corresponds to the key's position on the keyboard, called a scan code. By using scan codes, you can use different keyboards and easily adapt them to foreign languages. In addition, scan codes let you develop and use alternative keyboard layouts, such as the Dvorak keyboard. When a key is pressed, the keyboard generates an interrupt in the main system unit of the computer. This causes the OS/2 operating system to execute a routine that reads the scan code of the key you pressed from the keyboard port. The same is true when you release the key: the keyboard sends a release code (consisting of the scan code plus 128) to the system unit. The key press and release are sometimes called make and break interrupts, respectively. Each key is identified by its scan code (note that there are no scan codes of 0). Under the MS-DOS(R) operating system, a PC's ROM-BIOS translates the scan code into a key. Under OS/2, a routine translates a scan code into a character via a translation table. When it finishes the translation, OS/2 places the scan code and the character into the keyboard buffer for the foreground process's screen group. Then the foreground application can retrieve it from there with a call to one of the KBD routines. Other keys may influence the generation of a particular ANSI character code. There are two groups of these keys: instance keys--Alt, Shift, Ctrl, and SysRq, and lock keys-- CapsLock, NumLock, ScrollLock, and Ins. The KBD routines use shift states to indicate the depression of a key in the first group and whether the user has toggled a key in the second group. OS/2 uses an extended ASCII code set to incorporate the input from function keys, numeric keypad keys, and keys pressed with Alt-Ctrl, for a total of 256 ASCII keystrokes. It can also support the double-byte character sets used by some foreign languages. Some keystrokes, such as the function keys and arrow keys, do not have an ASCII equivalent. For these keystrokes, the character value returned is either 0 or E0H after translation. You can safely use these two values to indicate the presence of a non-ASCII key. Although you must use the scan code when OS/2 returns a character code of either 0 or E0H, the latter indicates that the key is unique to an enhanced keyboard. For instance, the two sets of arrow keys on an enhanced keyboard generate the same scan codes, but a character code of E0H indicates the arrow key is from the independent arrow-key keypad. The same is true for other keys duplicated on the enhanced keyboard. Therefore, you can distinguish them in your program if you wish; that is something you usually cannot do with most high-level language keyboard functions. Cooked vs. Raw Mode OS/2 offers two flavors of keyboard input: cooked (or ASCII) and raw (or binary) mode. In cooked mode (the default), OS/2 will translate some keyboard characters, as shown in Figure 1. In addition, it treats the Carriage-Return character as an end-of-input character that is not passed back from the keyboard. The input function you use will determine whether the character is echoed to the screen and whether the extended keyboard keys and editing keys are used. In raw mode, OS/2 places each keyboard character in the foreground screen group's keyboard buffer without modification. In addition, in this mode OS/2 never echoes the characters to the screen. In either mode, the standard operating system keys are trapped by OS/2. These include Ctrl-Esc, Alt-Esc, Ctrl-Alt-Del, Ctrl-Alt-NumLock, and PrtSc. Figure 1 In cooked input mode, OS/2 will translate the keys listed here, and the application will not receive them. Each key is given with a description of the system action associated with it. In each case it affects only the program currently accepting keyboard input. ^S Pauses or continues the execution of the program. ^P Toggles the printer echo. ^C Terminates the program. ^Break Terminates the program. TAB Inserts eight characters onto the screen at the cursor position. ^PrtSc Dumps the input screen to the print spooler. KBD Data Structures You can use three principal data structures to access the KBD subsystem. The first, KBDKEYINFO (shown in Figure 2), retrieves characters from the keyboard buffer. As previously described, the chChar field is 0 or E0H when you press a non-ASCII key. In addition, this structure contains the keyboard scan code for the key, a byte that indicates the shift key state, and a time stamp (in milliseconds). An additional structure member, fbStatus, indicates a change in the shift key status (even if a key was not pressed) and signals whether a character is only the first of two bytes in a foreign language character set. The second structure you can use, KBDINFO shown in Figure 3, retrieves and sets the keyboard state. The fsMask member of this structure lets you change the input mode (raw or cooked), whether or not character echo is on, and lets you indicate which keyboard flags and settings you are changing. You can also use this structure to set the turnaround character. Although the turnaround character is usually a Carriage Return by default, it can be any character. Note that the cb member of this structure must be set to the size of the structure, thus allowing this structure to expand in the future. Finally, you can use the STRINGINBUF structure (see Figure 4) for reading in whole lines of input. Figure 2 typedef struct _KBDKEYINFO { unsigned char chChar; /* ASCII code */ unsigned char chScan; /* scan code */ unsigned char fbStatus; /* status code */ unsigned char bNlsShift; /* reserved, 0 */ unsigned fsState; /* shift key state */ unsigned long time; /* time in mseconds */ } KBDKEYINFO; The fsState bit settings: Bit Mask Meaning when bit is set 0 0001 Right Shift pressed 1 0002 Left Shift pressed 2 0004 Ctrl key pressed 3 0008 Alt key pressed 4 0010 ScrollLock on 5 0020 NumLock on 6 0040 CapsLock on 7 0080 INS on 8 0100 Left Ctrl key pressed 9 0200 Left Alt key pressed 10 0400 Right Ctrl key pressed 11 0800 Right Alt key pressed 12 1000 ScrollLock pressed 13 2000 NumLock pressed 14 4000 CapsLock pressed 15 8000 SysRq key pressed Figure 3 typedef struct _KBDINFO { unsigned cb; /* structure length */ unsigned fsMask; /* keyboard modes */ unsigned chTurnAround; /* end-of-input char */ unsigned fsInterim; /* interim char flag */ unsigned fsState; /* shift key state */ } KBDINFO; The fsMask bit settings: Bit Mask Meaning when bit set 0 0001 Echo on 1 0002 Echo off 2 0004 Raw mode 3 0008 Cooked mode 4 0010 Shift state to be changed 5 0020 Interim flags to be changed 6 0040 Turnaround char to be changed 7 0080 Length of turnaround character (1 then 2 bytes, 0 then 1 byte) The fsState bit settings: Bit Mask Meaning when set 0 0001 Right Shift pressed 1 0002 Left Shift pressed 2 0004 Ctrl key pressed 3 0008 Alt key pressed 4 0010 ScrollLock on 5 0020 NumLock on 6 0040 CapsLock on 7 0080 INS on 8-15 reserved and always 0 Figure 4 typedef struct _STRINGINBUF { unsigned cb; /* buffer length */ unsigned cchIn; /* bytes read */ } STRINGINBUF; KBD API Functions If you've read previous installments in this series, you'll be familiar with the OS/2 keyboard API functions (see Figure 5). Recall that we've used two of the better known functions, KbdCharIn and KbdStringIn, in our example programs. Generally, you'll find the keyboard API is simpler and easier to use after using VIO; that's because there aren't nearly as many ways to obtain keyboard input as there are to write to the video output. Whenever possible, limit keyboard I/O to one thread, regardless of whether that thread carries out other activities. In general, it is simpler to create one thread whose sole purpose is to interface with the keyboard, which means that it will block when there is no keyboard activity. Then that thread can communicate with the other threads via some form of interprocess communication (like a semaphore). Figure 5 KbdCharIn returns keyboard character (ascii, scan code) and shift state KbdClose closes logical keyboard * KbdDeRegister deregisters alternate subsystem KbdFlushBuffer flushes keyboard buffer for screen group KbdFreeFocus frees keyboard focus KbdGetCp retrieves code page identifier KbdGetFocus retrieves logical keyboard focus KbdGetStatus retrieves keyboard status KbdOpen opens logical keyboard KbdPeek returns status of next character in keyboard buffer * KbdRegister registers alternate keyboard subsystem KbdSetCp sets code page identifier KbdSetCustXt installs alternate translation table KbdSetStatus modifies keyboard status KbdStringIn reads line from keyboard KbdSynch serializes keyboard access KbdXlate translates scan codes to Ascii *Not allowed or does nothing if calling process is run in PM window. KbdCharIn The most commonly used function in the KBD API is KbdCharIn. This function takes three parameters: a pointer to the KBDKEYINFO structure, a wait/nowait parameter, and a keyboard handle (usually 0). If the wait parameter is IO_WAIT, the function will block the calling thread until keyboard input is available; otherwise it will return immediately. Either way you'll need to use the fbStatus member of the KBDKEYINFO structure to see if the user pressed a key. Note that KbdCharIn does not echo the characters it returns. Probably the biggest mistake made by the new OS/2 programmer is to write a function that calls KbdCharIn in a loop with the wait flag set to IO_NOWAIT. Since the function will return immediately regardless of whether a character is available, it will burn up tremendous amounts of CPU time as it cycles through the loop. Instead, it is wiser to dedicate one thread to keyboard I/O and have that thread block on a call to KbdCharIn with the wait flag set to IO_WAIT. When the thread does return, it can use a semaphore or some other device to notify the main thread that it has received a character from the keyboard. Alternatively, it can insert the character in an application buffer and return to KbdCharIn. KbdPeek Another popular function is KbdPeek. It is similar to the kbhit standard library function, since you can use it to interrogate the status of the keyboard without removing any characters waiting in the keyboard buffer. It, however, not only tells you whether a keypress is present, but can return the keyboard status and the value of the next character (if there is one) as well. Like KbdCharIn, it requires a KBDKEYINFO structure and a keyboard handle as parameters. Keyboard Status KbdGetStatus and KbdSetStatus can return and set the keyboard status information via the KBDINFO structure. To set part of the keyboard status, you must first set the proper bit in fsMask in the KBDINFO structure to indicate what type of function you are changing. Then you can set the value of the related parameter. For instance, to turn on the CapsLock key, first, you must set bit 4 in fsMask and bit 6 in fsState. Then a call to KbdSetStatus will toggle this key on. For switching between cooked and raw input modes and character echo on and off, you need only set the bits in fsMask. Note that these changes are local to the session in which the calling thread is running: they do not affect the keyboard status of other sessions. You should call KbdGetStatus first to initialize the structure so the rest of the flags in KBDINFO will be unchanged. KbdStringIn The KbdStringIn function reads a string of characters from the keyboard buffer. It requires four parameters: the buffer into which the characters will be placed, the STRINGINBUF structure, a wait/nowait flag, and the keyboard handle. You can read up to 255 characters with this function. KbdStringIn performs differently, depending on which input mode is in place when you call it. In raw mode, if you set the wait flag to IO_WAIT, the function will read characters from the keyboard buffer until your buffer is full (based on the buffer size placed in STRINGINBUF). If the wait flag is IO_NOWAIT, the function will read however many characters are waiting in the keyboard buffer (but not more than will fit into your buffer) and return immediately. KbdStringIn will not echo the characters to the display and you won't be able to use the editing keys in raw mode. The function places extended ASCII keys in the buffer as a 2-byte sequence of 00h or E0h followed by the scan code. In cooked mode, you must use IO_WAIT for the wait flag. The function will echo each character to the screen unless the echo flag is off. You can use the standard editing keys, but the function will ignore all other extended keys. TABS are expanded to spaces on the screen, using eight-column tab stops; but the function will leave them in the buffer as 09h. You must terminate input to KbdStringIn in cooked mode with ENTER, which the function will place in the buffer as 0Dh. When the buffer contains one character less than the requested buffer length, the function will discard additional keystrokes and sound a warning beep until the user presses ENTER. The cchIn member of the STRINGINBUF structure will indicate the number of characters placed in the buffer upon return from the function. In cooked mode, however, you can also set cchIn to the number of characters already in the buffer before you call the function. This allows you to use the data already in your buffer as an editing template. Mouse Interface Although the screen and keyboard are the traditional mechanisms for user interaction on microcomputer systems, the mouse is fast becoming an indispensable accessory. Nowhere is this more true than in the OS/2 environment, where the PM graphical interface seamlessly lends itself to a mouse-based operation. OS/2 programs that do not support the mouse are already rare; they are all character-based applications ported from DOS before the release of OS/2 Version 1.1. OS/2 offers two sets of mouse interface services: the core API services and the PM routines. The PM mouse services make it easy to write programs that use the mouse, since the program doesn't have to know you are using a mouse. You can, however, use the core API services to take advantage of the mouse interface without having to write a PM program. The mouse device interface gives the appearance of being tightly linked to the video screen. You can think of the screen and your desktop as grids. The mouse movements on the desktop grid control parallel movements of the mouse pointer upon the screen. Applications that use and manipulate a mouse may concern themselves with correlating the mouse and the mouse pointer, the definition of restricted areas, and, possibly, the shape of the mouse pointer itself. Mouse Drivers The OS/2 environment uses two device drivers for its mouse interface: POINTDD.SYS to draw the mouse pointer and MOUSEA0.SYS to interface with the mouse itself. OS/2 loads POINTER$, the pointer draw driver from POINTDD.SYS. POINTER$ draws and maintains the mouse pointer on screen and interfaces with the mouse driver to determine where to draw the pointer. It represents the motion of the mouse on screen by determining the mouse's new position, erasing its old image at its previous position, and redrawing the new image at the new position. POINTER$ is only operational in text mode. You must disable the driver before switching into graphics mode. Ordinary graphics applications must do much of the mouse interface work themselves, including monitoring the movement of the mouse, supplying a pointer image, and drawing the pointer on the screen. The OS/2 Presentation Manager takes care of these chores for applications running in its screen group. OS/2 loads the character device driver, MOUSE$, from MOUSEXX.SYS. There are several different versions of MOUSEXX.SYS; which one you load will depend on which mouse you use. OS/2 supports the full range of popular mice, including the entire Microsoft(R) line and the IBM(R) PS/2(R) mouse. Note that whereas the standard IBM/Microsoft mouse has two buttons, other models have one, two, or three buttons, all of which OS/2 can support. The MOUSE$ driver services the mouse interrupts and keeps track of the mouse position and button states. It also provides the device I/O interface used by the Basic Mouse Subsystem (BMS). You can configure the mouse driver parameters and the mouse driver itself by modifying its entry in CONFIG.SYS. For instance, my Microsoft Inport Mouse uses the following entry in CONFIG.SYS: DEVICE=C:\OS2\MOUSEA04.SYS You can add parameters after the driver filename that specify which serial port to use (if it's a serial mouse), the size of the mouse event queue (explained below), and the mouse operational mode. For instance, DEVICE=C:\OS2\MOUSEXX.SYS SERIAL=COM2,QSIZE=50,MODE=R will set this driver to use communications port 2, with a queue size of 50. The MODE parameter will tell OS/2 to use this mouse driver in real mode. The SERIAL= parameter will force a serial mouse driver to use the specified communications port; the default is COM1. QSIZE= will set the queue to hold up to a specified number of mouse event packets (the default is 10). Note that each packet requires 10 bytes of storage. MODE=P will set the driver to operate only in protected mode; MODE=R will do the same for real mode. The default MODE is both real and protected mode. Mouse DLLs The OS/2 operating system supplies two sets of DLLs that provide mouse interface support to the API. BMCALLS.DLL contains the BMS, which implements the MOU services and communicates with the MOUSE$ driver. Like its counterpart in the keyboard subsystem, MOUCALLS.DLL is the mouse router. It contains entry points for the MOU API functions and passes incoming calls to the appropriate subsystem in each screen group. By default, this subsystem is BMSCALLS; you can, however, replace it wholly or in part, just as you can with other subsystems. Mouse Events The mouse is a read-only device. It can generate information about its movements and the state of the mouse buttons (pressed or released). Each piece of information--a mouse movement, a button press, or both--is called a mouse event. The mouse can generate up to 30 events per second. Like the keyboard, the mouse sends event information to the mouse driver via a hardware interrupt. OS/2 is capable of providing your application with the event information. It organizes the mouse information as a packet and places it in a first-in first-out (FIFO) queue until your program reads it. The packet includes the current position of the mouse pointer and the state of the mouse buttons. Although the queue is not very large, a considerable number of packets can be generated before your program reads them. If this happens, newer packets will overwrite older ones to make room in the queue. This is not a problem, since your application will usually want to receive the most timely mouse information. Unlike the keyboard, it is unusual that you would need to trace every mouse event. The OS/2 mouse services can return mouse pointer position information in two ways. The default method returns the row and column position of the mouse pointer in screen units. In text mode these correspond to character positions; in graphics mode they are pel coordinates. A mouse pointer cannot be "in between" positions. In addition, all mouse coordinates are set relative to the upper-left-hand corner of the screen, which is row 0, column 0 in screen units. OS/2 can also return the mouse position information in mickey counts (a name taken from the cartoon character). A mickey is the smallest unit of mouse measurement and corresponds to a specific measurement in centimeters on the desktop. Mickey references use the x and y coordinates found in Cartesian algebra. If the y value is negative, it means you have moved the mouse away from you on the desk, leaving the pointer further up the screen. If the y value is positive, it means you have moved the mouse toward you with a corresponding movement of the mouse down the screen. If the x value is negative, both the mouse and pointer have moved left; if the x value is positive, both have moved right. Scaling Factors OS/2 does not force you to use a fixed relationship between mouse movements on the desktop and mouse pointer movements on the screen. Instead, you can change the scaling factors, that is, the number of mickeys you must move the mouse in order to change the location of the mouse pointer by 1 screen unit. If the scaling factor is 1, the pointer driver will move the mouse pointer 1 screen unit for every mickey you move the mouse. If the scaling factor is 2, the pointer driver will move the mouse pointer 1 screen unit for every 2 mickeys you move the mouse, and so on. In other words, the greater the scaling factor, the more you must physically move the mouse to move the mouse pointer on the screen. That's why it's easy to make the case that the choice of scaling factor is governed more by the amount of free desk space than by preference. The larger the scaling factor, the more desk space you'll need. Collision Areas The OS/2 mouse interface allows you to create collision areas. These are areas of the screen into which the mouse pointer cannot enter. The pointer driver accomplishes this simply by not drawing the mouse pointer inside the designated area. A text mode application can use a collision area to restrict mouse input from a certain part of the screen during a specific operation. OS/2 does not support collision areas in graphics mode, so you'll have to implement the collision areas yourself. PM Mouse Support The OS/2 Presentation Manager provides a set of mouse API functions that are more advanced than those supplied by the standard mouse subsystem (see Figure 6). As such, there is generally less work involved when dealing with the mouse interface under Presentation Manager. Indeed, PM graphics applications do not have to draw or move the mouse--the Presentation Manager will do that for them. In addition, the PM mouse interface is simpler, and the application does not have to keep track of the mouse position. Instead, PM notifies the application of the user selection of a menu item--the application does not have to know whether the user selected the item with the keyboard or the mouse. Also note that character-mode applications that use the mouse will generally work fine in a PM window--except for the few side effects described below. Figure 6 MouClose closes mouse handle * MouDeRegister deregisters alternate subsystem * MouDrawPtr displays mouse pointer MouFlushQue flushes mouse event queue MouGetDevStatus retrieves mouse status MouGetEventMask retrieves mouse event mask MouGetHotKey retrieves mouse hot key combination MouGetNumButtons retrieves number of buttons MouGetNumMickeys retrieves mickeys per centimeter MouGetNumQueEl retrieves number of mouse events in queue MouGetPtrPos retrieves mouse pointer position * MouGetPtrShape retrieves pointer bit mask * MouGetScaleFact retrieves scaling factors MouInitReal initializes real mode mouse MouOpen opens mouse handle MouReadEventQue retrieves mouse event information * MouRegister registers alternate subsystem MouRemovePtr removes mouse pointer (creates collision area) * MouSetDevStatus sets mouse device status MouSetEventMask sets mouse event mask MouSetHotKey sets mouse hot key MouSetPtrPos sets mouse pointer position * MouSetPtrShape sets mouse pointer mask * MouSetScaleFact sets scale factors MouSynch synchronizes mouse access *Not allowed or does nothing if calling process is run in PM window. Family Mode Support We haven't addressed Family Mode issues since the first OS/2 program in this series, "Planning and Writing a Multithreaded OS/2 Program with Microsoft C," MSJ (Vol. 4, No. 2). That's because we haven't had to. The moment we wrote the first multithreaded program, we couldn't consider Family Mode since DOS does not support multithreaded programs. Many of the VIO and KBD API services, however, have Family Mode counterparts that let you write bound programs that run under both OS/2 and DOS (as long as they are not multithreaded). The MOU services, on the other hand, are not available under DOS, and you'll have to use Int 33h. To develop an application that uses the mouse and runs under both real and protected mode, you'll have to write functions that call the MOU API in protected mode and Int 33h in real mode. Opening the Handle Since OS/2 does not provide a default handle for the mouse interface, as it does for the keyboard and screen, you must first open a mouse handle before your application can use the mouse. You can use the MouOpen function to open the mouse and receive a handle that your application can use. This will initialize the mouse driver for your application's session, but it will not display the mouse pointer. That's because when you open a mouse handle, the entire screen is a collision area and the mouse pointer will not appear until your application calls MouDrawPtr. You can close the mouse pointer with MouClose. The Basic Mouse Subsystem can synchronize access to the physical mouse by several sessions, but it cannot synchronize mouse access by multiple processes in the same session. The mouse threads in a screen group share the same mouse event queue. Thus, as with the keyboard, if your application must share mouse access with other processes in its screen group, you'll have to make provisions to synchronize this access. Unfortunately, there is no MOU equivalent to KbdGetFocus or KbdFreeFocus, so you'll have to perform this synchronization yourself. But OS/2 does provide an API service, MouSynch, to do this; in fact, the Presentation Manager uses this same facility to coordinate mouse access among the processes in its screen group. Note that like the keyboard, OS/2 blocks a background thread that tries to read the mouse. Thus, it is wise not only to limit mouse access to one thread in your program but to make mouse access the thread's raison d'etre. In text mode, it is relatively simple to access the mouse: open the mouse with MouOpen, make the pointer appear with MouDrawPtr, and then begin to evaluate the mouse events with MouReadEventQue. In a graphics mode, the process is slightly more complex. As I mentioned earlier, a graphics application must perform its own mouse manipulation. Therefore you must open the mouse while the application is in text mode and then disable it with MouSetDevStatus. You then change the video mode to graphics with VioSetMode and create your own mouse pointer with MouSetPtrShape. The application should call MouReadEventQue to interpret mouse events, but it will need to use MouSetPtrPos to move the pointer accordingly. Reading from the Mouse Queue As we have already mentioned, every time you move the mouse or press a mouse button, OS/2 will place an event information packet on the end of the mouse queue. You can use MouReadEventQue to read information from the queue. This function takes three parameters: a pointer to the MOUEVENTINFO structure, a wait/nowait parameter, and a mouse handle. The MOUEVENTINFO structure, shown in Figure 7, contains the information returned by the function. The fs member contains bits that indicate the current state of the mouse buttons and whether the mouse has moved. The Time member records the time of the event in milliseconds. You can use Time to determine when an event occurred relative to the previous or next event in the queue. This allows your application to interpret double-clicks of the mouse button, intervals between mouse movements, and so on. MOUEVENTINFO also includes the row and column coordinates of the current mouse position. These are in screen units, which are character rows and columns for text mode or pels in graphics mode. The wait/nowait parameter to MouReadEventQue allows the calling thread to block if there are no mouse events in the queue. Note that unlike other OS/2 API functions, you must pass this parameter by reference instead of by value (that is, you must set a variable to the value and pass the address of the variable). Also note that this parameter uses 1 to wait and 0 to return immediately, which again differs from other API functions. MouGetPtrPos is an alternative to MouReadEventQue. This function will return the mouse coordinates, but it won't return the state of the mouse buttons. MouGetPtrPos uses the PTRLOC structure shown in Figure 8. The default OS/2 mouse queue size is 10. Although this should be sufficient in most cases, you can set a value from 1 to 100 for it by passing a parameter to the mouse driver specified in CONFIG.SYS. You can use MouGetEventMask and MouSetEventMask to screen out certain types of events. This is particularly useful if, for instance, you want an application to ignore presses of the second mouse button. You can use MouGetScaleFact to retrieve the currently used scaling factor and use MouSetScaleFact to set a new scaling factor. These functions take two parameters: a pointer to the SCALEFACT structure and a mouse handle. The SCALEFACT structure, shown in Figure 9, allows you to set scaling factors from 1 to 32,767, although a more practical range is from 1 to about 24. Figure 7 typedef struct _MOUEVENTINFO { unsigned fs; /* event mask */ unsigned long Time; /* system timestamp */ unsigned row; /* pointer position */ unsigned col; /* pointer position */ } MOUEVENTINFO; The fs bit settings (this is 0 if no event occurred): Bit Mask Meaning when bit set 0 0001 Mouse moved, no buttons pressed 1 0002 Mouse moved, button 1 pressed 2 0004 No movement, button 1 pressed 3 0008 Mouse moved, button 2 pressed 4 0010 No movement, button 2 pressed 5 0020 Mouse moved, button 3 pressed 6 0040 No movement, button 3 pressed 7 0080 Reserved, 0 Figure 8 typedef struct _PTRLOC { unsigned row; /* pointer position */ unsigned col; /* pointer position */ } PTRLOC; Figure 9 typedef struct _SCALEFACT { unsigned rowScale; /* vertical factor */ unsigned colScale; /* horizontal factor */ } SCALEFACT; The Time member contains the system time (in milliseconds) at which the mouse event occurred. The row and column members contain the screen location of the mouse pointer. By default this is in screen units, but a call to MouSetDevStatus can change this to mickeys. KMSTATUS.C The program presented with this article, KMSTATUS.C, illustrates how you can use the OS/2 keyboard and mouse subsystems. When you run the program, it will present a display that splits the screen into two sections (see Figure 10). The upper section contains mouse-related event information, and the lower section contains keyboard information. The mouse event information includes the button and mickey counts, the time of each mouse event, and the scaling factors. It also includes the row and column of the mouse pointer position. As you move the mouse or press the mouse buttons, the program updates this event information. Specifically, pressing and releasing the mouse buttons, will cause the program to display the mouse pointer location and the time stamp for each button. Each time you press a mouse button, the program will clear the information for that button and print the press information. It will not print the release information until you release the mouse button. The program will print information on each of three mouse buttons. Below the mouse statistical information are four push buttons, which are crude imitations of the push buttons in a PM window. You can, in fact, use these push buttons as you would in the PM environment: move the mouse pointer to the push button and press either the left or right mouse button (if you are using a three-button mouse, use mouse buttons 1 and 2). Two push buttons control the horizontal and vertical scaling factors. If you click left (press the left mouse button) on the horizontal scaling factor push button the program will increment the value. If you click right, the program will decrement the value. You can modify the vertical scaling factor by clicking on its push button. The scaling factor information at the top of the screen will reflect these changes. A third push button on the left of the screen is not really a push button at all; what it does is demark a collision area. Appropriately, a quote inside the button parodies a popular saying with, "You can't get here from there " What it means is that you will not be able to move the mouse pointer into this push button; if you do, it will disappear. The program will continue to update statistical information at the top of the screen, but the mouse pointer will not reappear until you move it out of the collision area. Note that this is not the case when you run the program in a PM window. There, the pointer will continue to display, even inside of the collision area. You can use the last push button (on the right of the screen) to quit the program. The lower portion of the screen contains statistical information on the keyboard. As you press any keyboard keys, the program will display the Ascii code and Scan code received for each. It will also display the keyboard state for the Shift, Ctrl, and Alt keys. Since it is reporting the state of the keyboard for each keyboard event, these will stay on the screen until the next keyboard event. You can change the keyboard input mode (raw or cooked), by using the mouse to press the Input Mode push button. This will toggle the input mode display when the next keyboard event occurs. Examining Threads The main( ) function for KMSTATUS.C is simple. It starts a mouse thread and a keyboard thread. Then it calls DosSleep to sleep indefinitely. The mouse thread starts by opening a mouse handle and clearing the screen. Then it displays the various push buttons, displays the mouse pointer, and creates the collision area. The function prints the fixed mouse information, flushes the mouse queue, and enters a loop. During each cycle of the loop, the mouse thread reads an event from the mouse queue and prints that information on the screen (see Figure 11). if a mouse button was pressed, it also prints the mouse pointer location and the time stamp. It checks the pointer location and time stamp for each mouse button individually. The mouse thread uses the ButtonPressed function to see if a mouse button was pressed while the mouse pointer was over one of the push buttons. For the scaling factor push buttons, it adjusts the scaling factors. For the Input Mode push button, it toggles a variable that the keyboard thread will use to change the input mode. If the user has selected the Quit push button, the mouse thread will break out of the loop. it will then close the mouse handle, clear the screen, and call DosExit to terminate the program. The mouse thread function contains a DosSleep at the bottom of the loop, which prevents the function from stealing too many CPU cycles. The keyboard thread function is even simpler than the mouse thread function. It flushes the keyboard buffer and enters a loop. At the top of the loop it examines a variable to see if the mouse thread has signaled a change in the input mode. If so, it toggles the input mode bits and sets the keyboard status with KbdSetStatus. Then it calls KbdGetStatus to retrieve the keyboard state and calls KbdCharIn to wait for the next keyboard input character. When the keyboard thread receives a keyboard character, it prints the character's Ascii value, the Scan code, and the state of the Shift keys (see Figure 12). Then it returns to the top of the loop. The keyboard thread never terminates; it dies when the mouse thread terminates the program. I didn't add a DosSleep to this thread since it blocks on keyboard input. This program should give you a good understanding of how the KBD and MOU subsystems work. It certainly demonstrates how simple it is to use these subsystems in an application. Moreover, you should be able to use some of this code in your own programs. Summary This article emphasizes the lack of complexity of the KBD and MOU subsystems. They are not only easy to use but allow an application to take advantage of the OS/2 input facilities rather quickly. Of the two subsystems, you'll probably use KBD the most but exploit it the least; that is, you will probably use KbdCharIn most of the time. Although adding mouse support to an application requires a little more work, you won't have a problem if you keep the mouse code in a separate thread. In some applications, you may want to have one thread that receives input from either the keyboard or the mouse. This thread can then provide that input to the rest of the program, hiding the details of the input process. Now that we have completed our exploration of the three OS/2 subsystems, it's time to move on. In the next article, we will explore some OS/2 Interprocess Communications (IPC) facilities other than threads, including queues, monitors, signals, and pipes. Everything You Always Wanted to Know About the MS-DOS EXEC Function... Ray Duncan The MS-DOS(R) system loader, which brings COM or EXE files from disk into memory and executes them, can be invoked by any program with the MS-DOS EXEC function (Interrupt 21H Function 4BH). The default MS-DOS command interpreter, COMMAND.COM, uses the EXEC function to load and run its external commands, such as CHKDSK, as well as other application programs. Many popular commercial programs, such as databases and word processors, use EXEC to load and run subsidiary programs (spelling checkers, for example) or to load and run a second copy of COMMAND.COM. This allows a user to run subsidiary programs or enter MS-DOS commands without losing his or her current working context. When EXEC is used by one program (called the parent) to load and run another (called the child), the parent can pass certain information to the child in the form of a set of strings called the environment, a command line, and two file control blocks. The child program also inherits the parent program's handles for the MS-DOS standard devices and for any other files or character devices the parent has opened (unless the open operation was performed with the "noninheritance" option). Any operations performed by the child on inherited handles, such as seeks or file I/O, also affect the file pointers associated with the parent's handles. A child program can, in turn, load another program, and the cycle can be repeated until the system's memory area is exhausted. Because MS-DOS1 is not a multitasking operating system, a child program has complete control of the system until it has finished its work; the parent program is suspended. This type of processing is sometimes called synchronous execution. When the child terminates, the parent regains control and can use another system function call (Interrupt 21H Function 4DH) to obtain the child's return code and determine whether the program terminated normally, because of a critical hardware error, or because the user entered a Control-C. In addition to loading a child program, EXEC can also be used to load subprograms and overlays for application programs written in assembly language or in a high-level language that does not include an overlay manager in its run-time library. Such overlays typically cannot be run as self-contained programs; most require "helper" routines or data in the application's root segment. The EXEC function is available only with MS-DOS versions 2.0 and later. With MS-DOS version 1.x, a parent program can use Interrupt 21H Function 26H to create a program segment prefix for a child but must carry out the loading, relocation, and execution of the child's code and data itself, without any assistance from the operating system. How EXEC Works When the EXEC function receives a request to execute a program, it first attempts to locate and open the specified program file. If the file cannot be found, EXEC fails immediately and returns an error code to the caller. If the file exists, EXEC opens the file, determines its size, and inspects the first block of the file. If the first 2 bytes of the block are the ASCII characters MZ, the file is assumed to contain an EXE load module, and the sizes of the program's code, data, and stack segments are obtained from the EXE file header. Otherwise, the entire file is assumed to be an absolute load image (a COM program). The actual filename extension (COM or EXE) is ignored in this determination. At this point, the amount of memory needed to load the program is known, so EXEC attempts to allocate two blocks of memory: one to hold the new program's environment and one to contain the program's code, data, and stack segments. Assuming that enough memory is available to hold the program itself, the amount actually allocated to the program varies with its type. Programs of the COM type are usually given all the free memory in the system (unless the memory area has previously become fragmented), while the amount assigned to an EXE program is controlled by two fields in the file header, MINALLOC and MAXALLOC, that are set by the Microsoft(R) Object Linker (LINK). EXEC then copies the environment from the parent into the memory allocated for the child's environment, builds a program segment prefix (PSP) at the base of the child's program memory block, and copies into the child's PSP the command tail and the two default file control blocks passed by the parent. The previous contents of the terminate (Interrupt 22H), Control-C (Interrupt 23H), and critical error (Interrupt 24H) vectors are saved in the new PSP, and the terminate vector is updated so that control will return to the parent program when the child terminates or is aborted. The actual code and data portions of the child program are then read from the disk file into the program memory block above the newly constructed PSP. If the child is an EXE program, a relocation table in the file header is used to fix up segment references within the program to reflect its actual load address. Finally, the EXEC function sets up the CPU registers and stack according to the program type and transfers control to the program. The entry point for a COM file is always offset 100H within the program memory block (the first byte following the PSP). The entry point for an EXE file is specified in the file header and can be anywhere within the program. When EXEC is used to load and execute an overlay rather than a child program, its operation is much simpler than described above. For an overlay, EXEC does not attempt to allocate memory or build a PSP or environment. It simply loads the contents of the file to the address specified by the calling program and performs any necessary relocations (if the overlay file has an EXE header), using a segment value that is also supplied by the caller. EXEC then returns to the program that invoked it, rather than transferring control to the code in the newly loaded file. The requesting program is responsible for calling the overlay at the appropriate location. Using EXEC to Load a Program When one program loads and executes another, it must follow these steps: 1. Ensure that enough free memory is available to hold the code, data, and stack of the child program. 2. Set up the information to be passed to EXEC and the child program. 3. Call the MS-DOS EXEC function to run the child program. 4. Recover and examine the child program's termination and return codes. Making Memory Available MS-DOS typically allocates all available memory to a COM or EXE program when it is loaded. (The infrequent exceptions to this rule occur when the transient program area is fragmented by the presence of resident data or programs or when an EXE program is loaded that was linked with the /CPARMAXALLOC switch or modified with EXEMOD.) Therefore, before a program can load another program, it must free any memory it does not need for its own code, data, and stack. The extra memory is released with a call to the MS-DOS Resize Memory Block function (Interrupt 21H Function 4AH). In this case, the segment address of the parent's PSP is passed in the ES register, and the BX register holds the number of paragraphs of memory the program must retain for its own use. If the prospective parent is a COM program, it must be certain to move its stack to a safe area if it is reducing its memory allocation to less than 64Kb. Preparing Parameters for EXEC When used to load and execute a program, the EXEC function must be supplied with two principal parameters: ■ the address of the child program's pathname ■ the address of a parameter block The parameter block, in turn, contains the addresses of information to be passed to the child program. The Program Name The pathname for the child program must be an unambiguous, null-terminated (ASCIIZ) file specification (no wildcard characters). If a path is not included, the current directory is searched for the program; if a drive specifier is not present, the default drive is used. The Parameter Block The parameter block contains the addresses of four data items: ■ the environment block ■ the command tail ■ the two default file control blocks (FCBs) The position reserved in the parameter block for the pointer to an environment is only 2 bytes and contains a segment address, because an environment is always paragraph aligned (its address is always evenly divisible by 16); a value of 0000H indicates the parent program's environment should be inherited unchanged. The remaining three addresses are all doubleword addresses in the standard Intel format, with an offset value in the lower word and a segment value in the upper word. (See Figure 1). Figure 1 To Call AH = 4BH AL = 00H load and execute child process 03H load overlay DS:DX = segment:offset of ASCIIZ pathname for an executable program file ES:BX = segment:offset of parameter block Returns If function is successful: Carry flag is clear. Other registers are preserved if MS-DOS version 3.0 or later, destroyed if MS-DOS versions 2.x. If function is not successful: Carry flag is set. AX = error code Parameter Block Format Offset Contents If AL = 00H (load and execute program) 00H Segment pointer of the environment to be passed 02H Offset of command-line tail for the new PSP 04H Segment of command-line tail for the new PSP 06H Offset of first file control block, to be copied into new PSP at offset 5CH 08H Segment of first file control block 0AH Offset of second file control block, to be copied into new PSP at offset 6CH 0CH Segment of second file control block if AL = 03H (load overlay): 00H Segment address where overlay is to be loaded 02H Relocation factor to apply to loaded image The Environment An environment always begins on a paragraph boundary and is composed of a series of null-terminated (ASCIIZ) strings of the form: name=variable The end of the entire set of strings is indicated by an additional null byte. If the environment pointer in the parameter block supplied to an EXEC call contains zero, the child simply acquires a copy of the parent's environment. The parent can, however, provide a segment pointer to a different or expanded set of strings. In either case, under MS-DOS versions 3.0 and later, EXEC appends the child program's fully qualified pathname to its environment block. The maximum size of an environment is 32Kb, so very large amounts of information can be passed between programs by this mechanism. The original, or master, environment for the system is owned by the command processor that is loaded when the system is turned on or restarted (usually COMMAND.COM). Strings are placed into the system's master environment by COMMAND.COM as a result of PATH, SHELL, PROMPT, and SET commands, with default values always present for the first two. For example, if an MS-DOS version 3.2 system is started from drive C and a PATH command is not present in the AUTOEXEC.BAT file nor a SHELL command in the CONFIG.SYS file, the master environment will contain the two strings: PATH= COMSPEC=C:\COMMAND.COM These specifications are used by COMMAND.COM to search for executable "external" commands and to find its own executable file on the disk so that it can reload its transient portion when necessary. When the PROMPT string is present (as a result of a previous PROMPT or SET PROMPT command), COMMAND.COM uses it to tailor the prompt displayed to the user. Other strings in the environment are used only for informational purposes by transient programs and do not affect the operation of the operating system proper. For example, the Microsoft C Compiler and the Microsoft Object Linker look in the environment for INCLUDE, LIB, and TMP strings that specify the location of include files, library files, and temporary working files. Figure 2 contains a hex dump of a typical environment block. The Command Tail The command tail to be passed to the child program takes the form of a byte indicating the length of the remainder of the command tail, followed by a string of ASCII characters terminated with an ASCII carriage return (0DH); the carriage return is not included in the length byte. The command tail can include switches, filenames, and other parameters that can be inspected by the child program and used to influence its operation. It is copied into the child program's PSP at offset 80H. When COMMAND.COM uses EXEC to run a program, it passes a command tail that includes everything the user typed in the command line except the name of the program and any redirection parameters. I/O redirection is processed within COMMAND.COM itself and is manifest in the behavior of the standard device handles that are inherited by the child program. Any other program that uses EXEC to run a child program must try to perform any necessary redirection on its own and must supply an appropriate command tail so that the child program will behave as though it had been loaded by COMMAND.COM. The Default File Control Blocks The two default FCBs pointed to by the EXEC parameter block are copied into the child program's PSP at offsets 5CH and 6CH. Few of the currently popular application programs use FCBs for file and record I/O because FCBs do not support the hierarchical directory structure. But some programs do inspect the default FCBs as a quick way to isolate the first two switches or other parameters from the command tail. Therefore, to make its own identity transparent to the child program, the parent should emulate the action of COMMAND.COM by parsing the first two parameters of the command tail into the default FCBs. This can be conveniently accomplished with the MS-DOS function Parse Filename (Interrupt 21H Function 29H). If the child program does not require one or both of the default FCBs, the corresponding address in the parameter block can be initialized to point to two dummy FCBs in the application's memory space. These dummy FCBs should consist of 1 zero byte followed by 11 bytes containing ASCII blank characters (20H). Running the Child Program After the parent program has constructed the necessary parameters, it can invoke the EXEC function by issuing Interrupt 21H with the registers set as follows: AH = 4BH AL = 00H (EXEC subfunction to load and execute program) DS:DX = segment:offset of program pathname ES:BX = segment:offset of parameter block Upon return from the software interrupt, the parent must test the carry flag to determine whether the child program did, in fact, run. If the carry flag is clear, the child program was successfully loaded and given control. If the carry flag is set, the EXEC function failed, and the error code returned in AX can be examined to determine why. The usual reasons are: ■ the specified file could not be found ■ the file was found, but not enough memory was free to load it Other causes are uncommon and can be symptoms of more severe problems in the system as a whole (such as damage to disk files or to the memory image of MS-DOS). With MS-DOS versions 3.0 and later, additional details about the cause of an EXEC failure can be obtained by subsequently calling Interrupt 21H Function 59H (Get Extended Error Information). In general, supplying either an invalid address for an EXEC parameter block or invalid addresses within the parameter block itself does not cause a failure of the EXEC function, but may result in the child program behaving in unexpected ways. Special Considerations With MS-DOS versions 2.x, the previous contents of all the parent registers except for CS:IP can be destroyed after an EXEC call, including the stack pointer in SS:SP. Consequently, before issuing the EXEC call, the parent must push onto the stack the contents of any registers that it needs to preserve, and then it must save the stack segment and offset in a location that is addressable with the CS segment register. Upon return, the stack segment and offset can be loaded into SS:SP with code segment overrides, and then the other registers can be restored by popping them off the stack. With MS-DOS versions 3.0 and later, registers are preserved across an EXEC call in the usual fashion. Note: The code segments of Windows applications that use this technique should be given the IMPURE attribute. In addition, a bug in MS-DOS version 2.0 and in PC-DOS versions 2.0 and 2.1 causes an arbitrary doubleword in the parent's stack segment to be destroyed during an EXEC call. When the parent is a COM program and SS=PSP, the damaged location falls within the PSP and does no harm; however, in the case of an EXE parent where DS = SS, the affected location may overlap the data segment and cause aberrant behavior or even a crash after the return from EXEC. This bug was fixed in MS-DOS versions 2.11 and later and in PC-DOS versions 3.0 and later. Examining the Child Program's Return Codes If the EXEC function succeeds, the parent program can call Interrupt 21H Function 4DH (Get Return Code of Child Process) to learn whether the child executed normally to completion and passed back a return code or was terminated by the operating system because of an external event. Function 4DH returns: AH = termination type: 00H Child terminated normally (that is, exited via Interrupt 20H or Interrupt 21H Function 00H or Function 4CH). 01H Child was terminated by user's entry of a Ctrl-C. 02H Child was terminated by critical error handler (either the user responded with A to the Abort, Retry, Ignore prompt from the system's default Interrupt 24H handler, or a custom Interrupt 24H handler returned to MS-DOS with action code = 02H in register AL). 03H Child terminated normally and stayed resident (that is, exited via Interrupt 21H Function 31H or Interrupt 27H). AL = return code: Value passed by the child program in register AL when it terminated with Interrupt 21H Function 4CH or 31H. 00H if the child terminated using Interrupt 20H, Interrupt 27H, or Interrupt 21H Function 00H. These values are only guaranteed to be returned once by Function 4DH. Thus, a subsequent call to Function 4DH, without an intervening EXEC call, does not necessarily return any useful information. Additionally, if Function 4DH is called without a preceding successful EXEC call, the returned values are meaningless. Using COMMAND.COM with EXEC An application program can "shell" to MS-DOS--that is, provide the user with an MS-DOS prompt without terminating--by using EXEC to load and execute a secondary copy of COMMAND.COM with an empty command tail. The application can obtain the location of the COMMAND.COM disk file by inspecting its own environment for the COMSPEC string. The user returns to the application from the secondary command processor by typing exit at the COMMAND.COM prompt. Batch-file interpretation is carried out by COMMAND.COM, and a batch (BAT) file cannot be called using the EXEC function directly. Similarly, the sequential search for COM, EXE, and BAT files in all of the locations specified in the environment's PATH variable is a function of COMMAND.COM, rather than of EXEC. To execute a batch file or search the system path for a program, an application program can use EXEC to load and execute a secondary copy of COMMAND.COM to use as an intermediary. The application finds the location of COMMAND.COM as described in the preceding paragraph, but it passes a command tail in the form: /C program parameter1 parameter2 ... where program is the EXE, COM, or BAT file to be executed. When program terminates, the secondary copy of COMMAND.COM exits and returns control to the parent. A Parent and Child Example The source programs in Figures 3 and 4, PARENT.ASM and CHILD.ASM, illustrate how one program uses EXEC to load another. PARENT.ASM can be assembled and linked into the executable program PARENT.EXE with the following commands: C>MASM PARENT; <Enter> C>LINK PARENT; <Enter> Similarly, CHILD.ASM can be assembled and linked into the file CHILD.EXE as follows: C>MASM CHILD; <Enter> C>LINK CHILD; <Enter> When PARENT.EXE is executed with the command C>PARENT <Enter> PARENT reduces the size of its main memory block with a call to Interrupt 21H Function 4AH, to maximize the amount of free memory in the system, and then calls the EXEC function to load and execute CHILD.EXE. CHILD.EXE runs exactly as though it had been loaded directly by COMMAND.COM. CHILD resets the DS segment register to point to its own data segment, uses Interrupt 21H Function 40H to display a message on standard output, and then terminates using Interrupt 21H Function 4CH, passing a return code of zero. When PARENT.EXE regains control, it first checks the carry flag to determine whether the EXEC call succeeded. If the EXEC call failed, PARENT displays an error message and terminates with Interrupt 21H Function 4CH, itself passing a nonzero return code to COMMAND.COM to indicate an error. Otherwise, PARENT uses Interrupt 21H Function 4DH to obtain CHILD.EXE's termination type and return code, which it converts to ASCII and displays. PARENT then terminates using Interrupt 21H Function 4CH and passes a return code of zero to COMMAND.COM to indicate success. COMMAND.COM in turn receives control and displays a new user prompt. Using EXEC to Load Overlays Loading overlays with the EXEC function is much less complex than using EXEC to run another program. The main program, called the root segment, must carry out the following steps to load and execute an overlay: 1. Make a memory block available to receive the overlay. 2. Set up the overlay parameter block to be passed to the EXEC function. 3. Call the EXEC function to load the overlay. 4. Execute the code within the overlay by transferring to it with a far call. The overlay itself can be constructed as either a memory image (COM) or a relocatable (EXE) file and need not be the same type as the root program. In either case, the overlay should be designed so that the entry point (or a pointer to the entry point) is at the beginning of the module after it is loaded. This allows the root and overlay modules to be maintained separately and avoids a need for the root to have a "magical" knowledge of addresses within the overlay. To prevent users from inadvertently running an overlay directly from the command line, overlay files should be assigned an extension other than COM or EXE. The most convenient method relates overlays to their root segment by assigning them the same filename but an extension like OVL or OV1, OV2, and so on. Making Memory Available If EXEC is to load a child program successfully, the parent must release memory. In contrast, EXEC loads an overlay into memory that belongs to the calling program. If the root segment is a COM program and has not explicitly released extra memory, the root segment program need only ensure that the system contains enough memory to load the overlay and that the overlay load address does not conflict with its own code, data, or stack areas. If the root segment program was loaded from an EXE file, no straightforward way exists for it to determine unequivocally how much memory it already owns. The simplest course is for the program to release all extra memory, as discussed earlier in the section on loading a child program, and then use the MS-DOS memory allocation function (Interrupt 21H Function 48H) to obtain a new block of memory that is large enough to hold the overlay. Preparing Overlay Parameters When it is used to load an overlay, the EXEC function requires two major parameters: ■ the address of the pathname for the overlay file ■ the address of an overlay parameter block As for a child program, the pathname for the overlay file must be an unambiguous ASCIIZ file specification (again, no wildcard characters), and it must include an explicit extension. As before, if a path and/or disk drive are not included in the pathname, the current directory and default disk drive are used. The overlay parameter block contains the segment address at which the overlay should be loaded and a fixup value to be applied to any relocatable items within the overlay file. If the overlay file is in EXE format, the fixup value is typically the same as the load address; if the overlay is in memory-image (COM) format, the fixup value should be zero. The EXEC function does not attempt to validate the load address or the fixup value or to ensure that the load address actually belongs to the calling program. Loading and Executing the Overlay After the root segment program has prepared the filename of the overlay file and the overlay parameter block, it can invoke the EXEC function to load the overlay by issuing an Interrupt 21H with the registers set as follows: AH = 4BH AL = 03H (EXEC subfunction to load overlay) DS:DX = segment:offset of overlay file pathname ES:BX = segment:offset of overlay parameter block Upon return from Interrupt 21H, the root segment must test the carry flag to determine whether the overlay was loaded. If the carry flag is clear, the overlay file was located and brought into memory at the requested address. The overlay can then be entered by a far call, and should exit back to the root segment with a far return. If the carry flag is set, the overlay file was not found or some other (probably severe) system problem was encountered, and the AX register contains an error code. With MS-DOS versions 3.0 and later, Interrupt 21H Function 59H can be used to get more information about the EXEC failure. An invalid load address supplied in the overlay parameter block does not (usually) cause the EXEC function itself to fail but may result in the disconcerting message Memory Allocation Error, System Halted when the root program terminates. An Overlay Example The source programs ROOT.ASM (see Figure 5) and OVERLAY.ASM (see Figure 6) demonstrate the use of EXEC to load a program overlay. The program ROOT.EXE is executable from the MS-DOS prompt; it represents the root segment of an application. OVERLAY is constructed as an EXE file (although it is named OVERLAY.OVL because it cannot be run alone) and represents a subprogram that can be loaded by the root segment when and if it is needed. ROOT.ASM can be assembled and linked into the executable program ROOT.EXE with the following commands: C>MASM ROOT; <Enter> C>LINK ROOT; <Enter> OVERLAY.ASM can be assembled and linked into the file OVERLAY.OVL by typing: C>MASM OVERLAY; <Enter> C>LINK OVERLAY,OVERLAY.OVL; <Enter> The Microsoft Object Linker will display the message Warning: no stack segment but this message can be ignored. When ROOT.EXE is executed with the command C>ROOT <Enter> it first shrinks its main memory block with a call to Interrupt 21H Function 4AH and then allocates a separate block for the overlay with Interrupt 21H Function 48H. Next, ROOT calls the EXEC function to load the file OVERLAY.OVL into the newly allocated memory block. If the EXEC function fails, ROOT displays an error message and terminates with Interrupt 21H Function 4CH, passing a nonzero return code to COMMAND.COM to indicate an error. If the EXEC function succeeds, ROOT saves the contents of its DS segment register and then enters the overlay through an indirect far call. The overlay resets the DS segment register to point to its own data segment, displays a message using Interrupt 21H Function 40H, and then returns. Note that the main procedure of the overlay is declared with the far attribute in order to force the assembler to generate the opcode for a far return. When ROOT regains control, it restores the DS segment register to point to its own data segment again and displays an additional message, also using Interrupt 21H Function 40H, to indicate that the overlay executed successfully. ROOT then terminates using Interrupt 21H Function 4CH, passing a return code of zero to indicate success, and control returns to COMMAND.COM. Figure 3 name parent title 'PARENT --- demonstrate EXEC call' ; ; PARENT.EXE --- demonstration of EXEC to run process ; ; Uses MS-DOS EXEC (Int 21H, Function 4BH Subfunction 00H) ; to load and execute a child process named CHILD.EXE, ; then displays CHILD's return code. ; ; Ray Duncan, June 1987 ; stdin equ 0 ; standard input stdout equ 1 ; standard output stderr equ 2 ; standard error stksize equ 128 ; size of stack cr equ 0dh ; ASCII carriage return lf equ 0ah ; ASCII linefeed DGROUP group _DATA,_ENVIR,_STACK _TEXT segment byte public 'CODE' ; executable code segment assume cs:_TEXT,ds:_DATA,ss:_STACK stk_seg dw ? ; original SS contents stk_ptr dw ? ; original SP contents main proc far ; entry point from MS-DOS mov ax,_DATA ; set DS = our data segment mov ds,ax ; now give back extra memory ; so child has somewhere to ; run... mov ax,es ; let AX = segment of PSP base mov bx,ss ; and BX = segment of stack base sub bx,ax ; reserve seg stack - seg psp add bx,stksize/16 ; plus paragraphs of stack mov ah,4ah ; fxn 4ah = modify memory ; block int 21h jc main1 ; display parent message ... mov dx,offset DGROUP:msg1 ; DS:DX = address of message mov cx,msg1_len ; CX = length of message call pmsg push ds ; save parent's data segment mov stk_seg,ss ; save parent's stack pointer mov stk_ptr,sp ; now EXEC the child process... mov ax,ds ; set ES = DS mov es,ax mov dx,offset DGROUP:cname ; DS:DX = child pathname mov bx,offset DGROUP:pars ; EX:BX = parameter block mov ax,4b00h ; function 4BH, subfunction 00H int 21h ; transfer to MS-DOS cli ; (for bug in some early 8088s) mov ss,stk_seg ; restore parent's stack ; pointer mov sp,stk_ptr sti ; (for bug in some early 8088s) pop ds ; restore DS = our data segment jc main2 ; jump if EXEC failed ; otherwise EXEC succeeded, ; convert and display child's ; termination and return ; codes... mov ah,4dh ; fxn 4dh = get return code int 21h ; transfer to MS-DOS xchg al,ah ; convert termination code mov bx,offset DGROUP:msg4a call b2hex mov al,ah ; get back return code mov bx,offset DGROUP:msg4b ; and convert it call b2hex mov dx,offset DGROUP:msg4 ; DS:DX = address of message mov cx,msg4_len ; CX = length of message call pmsg ; display it mov ax,4c00h ; no error, terminate program int 21h ; with return code = 0 main1: mov bx,offset DGROUP:msg2a ; convert error code call b2hex mov dx,offset DGROUP:msg2 ; display message 'Memory mov cx,msg2_len ; resize failed...' call pmsg jmp main3 main2: mov bx,offset DGROUP:msg3a ; convert error code call b2hex mov dx,offset DGROUP:msg3 ; display message 'EXEC mov cx,msg3_len ; call failed...' call pmsg main3: mov ax,4c01h ; error, terminate program int 21h ; with return code = 1 main endp ; end of main procedure b2hex proc near ; convert byte to hex ASCII ; call with AL=binary value ; BX=addr to store string push ax shr al,1 shr al,1 shr al,1 shr al,1 call ascii ; become first ASCII character mov [bx],al ; store it pop ax and al,0fh ; isolate lower 4 bits, which call ascii ; become the second ASCII ; character mov [bx+1],al ; store it ret b2hex endp ascii proc near ; convert value 00-0FH in AL add al,'0' ; into a "hex ASCII" character cmp al,'9' jle ascii2 ; jump if in range 00-09H, add al,'A'-'9'-1 ; offset it to range 0A-0FH, ascii2: ret ; return ASCII char. in AL. ascii endp pmsg proc near ; displays message on ; standard output ; call with DS:DX = address, ; CX = length mov bx,stdout ; BX = standard output handle mov ah,40h ; function 40h = write ; file/device int 21h ; transfer to MS-DOS ret ; back to caller pmsg endp _TEXT ends _DATA segment para public 'DATA' ; static & variable data segment cname db 'CHILD.EXE',0 ; pathname of child process pars dw _ENVIR ; segment of environment block dd tail ; long address, command tail dd fcb1 ; long address, default FCB #1 dd fcb2 ; long address, default FCB #2 tail db fcb1-tail-2 ; command tail for child db 'dummy command tail',cr fcb1 db 0 ; copied into default FCB #1 in db 11 dup (' ') ; child's program segment prefix db 25 dup (0) fcb2 db 0 ; copied into default FCB #2 in db 11 dup (' ') ; child's program segment prefix db 25 dup (0) msg1 db cr,lf,'Parent executing!',cr,lf msg1_len equ $-msg1 msg2 db cr,lf,'Memory resize failed, error code=' msg2a db 'xxh.',cr,lf msg2_len equ $-msg2 msg3 db cr,lf,'EXEC call failed, error code=' msg3a db 'xxh.',cr,lf msg3_len equ $-msg3 msg4 db cr,lf,'Parent regained control!' db cr,lf,'Child termination type=' msg4a db 'xxh, return code=' msg4b db 'xxh.',cr,lf msg4_len equ $-msg4 _DATA ends _ENVIR segment para public 'DATA' ; example environment block ; to be passed to child db 'PATH=',0 ; basic PATH, PROMPT, db 'PROMPT=$p$_$n$g',0 ; and COMSPEC strings db 'COMSPEC=C:\COMMAND.COM',0 db 0 ; extra null terminates block _ENVIR ends _STACK segment para stack 'STACK' db stksize dup (?) _STACK ends end main ; defines program entry point Figure 4 name child title 'CHILD process' ; ; CHILD.EXE --- a simple process loaded by PARENT.EXE ; to demonstrate the MS-DOS EXEC call, subfunction 00H. ; ; Ray Duncan, June 1987 ; stdin equ 0 ; standard input stdout equ 1 ; standard outpu stderr equ 2 ; standard error cr equ 0dh ; ASCII carriage return lf equ 0ah ; ASCII linefeed DGROUP group _DATA,STACK _TEXT segment byte public 'CODE' ; executable code segment assume cs:_TEXT,ds:_DATA,ss:STACK main proc far ; entry point from MS-DOS mov ax,_DATA ; set DS = our data segment mov ds,ax ; display child message ... mov dx,offset msg ; DS:DX = address of message mov cx,msg_len ; CX = length of message mov bx,stdout ; BX = standard output handle mov ah,40h ; AH = fxn 40h, write ; file/device int 21h ; transfer to MS-DOS jc main2 ; jump if any error mov ax,4c00h ; no error, terminate child int 21h ; with return code = 0 main2: mov ax,4c01h ; error, terminate child int 21h ; with return code = 1 main endp ; end of main procedure _TEXT ends _DATA segment para public 'DATA' ; static & variable data ; segment msg db cr,lf,'Child executing!',cr,lf msg_len equ $-msg _DATA ends STACK segment para stack 'STACK' dw 64 dup (?) STACK ends end main ; defines program entry point Figure 5 name root title 'ROOT --- demonstrate EXEC overlay' ; ; ROOT.EXE --- demonstration of EXEC for overlays ; ; Uses MS-DOS EXEC (Int 21H, Function 4BH Subfunction 03H) ; to load an overlay named OVERLAY.OVL, calls a routine ; within the OVERLAY, then recovers control and terminates. ; ; Ray Duncan, June 1987 ; stdin equ 0 ; standard input stdout equ 1 ; standard output stderr equ 2 ; standard error stksize equ 128 ; size of stack cr equ 0dh ; ASCII carriage return lf equ 0ah ; ASCII linefeed DGROUP group _DATA,_STACK _TEXT segment byte public 'CODE' ; executable code segment assume cs:_TEXT,ds:_DATA,ss:_STACK stk_seg dw ? ; original SS contents stk_ptr dw ? ; original SP contents main proc far ; entry point from MS-DOS mov ax,_DATA ; set DS = our data segment mov ds,ax ; now give back extra memory mov ax,es ; AX = segment of PSP base mov bx,ss ; BX = segment of stack base sub bx,ax ; reserve seg stack - seg psp add bx,stksize/16 ; plus paragraphs of stack mov ah,4ah ; fxn 4ah = modify memory ; block int 21h ; transfer to MS-DOS jc main1 ; jump if resize failed ; display message 'Root ; 'segment executing...' mov dx,offset DGROUP:msg1 ; DS:DX = address of message mov cx,msg1_len ; CX = length of message call pmsg ; allocate memory for overlay mov bx,1000h ; get 64 KB (4096 paragraphs) mov ah,48h ; fxn 48h, allocate mem block int 21h ; transfer to MS-DOS jc main2 ; jump if allocation failed mov pars,ax ; set load address for ; overlay mov pars+2,ax ; set relocation segment for ; overlay mov word ptr entry+2,ax ; set segment of entry point push ds ; save root's data segment mov stk_seg,ss ; save root's stack pointer mov stk_ptr,sp ; now use EXEC to load ; overlay mov ax,ds ; set ES = DS mov es,ax mov dx,offset DGROUP:oname ; DS:DX = overlay pathname mov bx,offset DGROUP:pars ; EX:BX = parameter block mov ax,4b03h ; function 4BH, subfunction ; 03H int 21h ; transfer to MS-DOS cli ; (for bug in some early ; 8088s) mov ss,stk_seg ; restore root's stack ; pointer mov sp,stk_ptr sti ; (for bug in some early 8088s) pop ds ; restore DS = our data ; segment jc main3 ; jump if EXEC failed ; otherwise EXEC succeeded... push ds ; save our data segment call dword ptr entry ; now call the overlay pop ds ; restore our data segment ; display message that root ; segment regained control... mov dx,offset DGROUP:msg5 ; DS:DX = address of message mov cx,msg5_len ; CX = length of message call pmsg ; display it mov ax,4c00h ; no error, terminate program int 21h ; with return code = 0 main1: mov bx,offset DGROUP:msg2a ; convert error code call b2hex mov dx,offset DGROUP:msg2 ; display message 'Memory mov cx,msg2_len ; resize failed...' call pmsg jmp main4 main2: ov bx,offset DGROUP:msg3a ; convert error code call b2hex mov dx,offset DGROUP:msg3 ; display message 'Memory mov cx,msg3_len ; allocation failed...' call pmsg jmp main4 main3: mov bx,offset DGROUP:msg4a ; convert error code call b2hex mov dx,offset DGROUP:msg4 ; display message 'EXEC mov cx,msg4_len ; call failed...' call pmsg main4: mov ax,4c01h ; error, terminate program int 21h ; with return code = 1 main endp ; end of main procedure b2hex proc near ; convert byte to hex ASCII ; call with AL = binary value ; BX = addr to store string push ax shr al,1 shr al,1 shr al,1 shr al,1 call ascii ; become first ASCII ; character mov [bx],al ; store it pop ax and al,0fh ; isolate lower 4 bits, which call ascii ; become the second ASCII ; character mov [bx+1],al ; store it ret b2hex endp ascii proc near ; convert value 00-0FH in AL add al,'0' ; into a "hex ASCII" character cmp al,'9' jle ascii2 ; jump if in range 00-09H, add al,'A'-'9'-1 ; offset it to range 0A-0FH, ascii2: ret ; return ASCII char. in AL. ascii endp pmsg proc near ; displays message on ; standard output ; call with DS:DX = address, ; CX = length mov bx,stdout ; BX = standard output handle mov ah,40h ; function 40H = write ; file/device int 21h ; transfer to MS-DOS ret ; back to caller pmsg ndp _TEXT ends _DATA segment para public 'DATA' ; static & variable data ; segment oname db 'OVERLAY.OVL',0 ; pathname of overlay file pars dw 0 ; load address (segment) for ; file dw 0 ; relocation (segment) for ; file entry dd 0 ; entry point for overlay msg1 db cr,lf,'Root segment executing!',cr,lf msg1_len equ $-msg1 msg2 db cr,lf,'Memory resize failed, error code=' msg2a db 'xxh.',cr,lf msg2_len equ $-msg2 msg3 db cr,lf,'Memory allocation failed, error code=' msg3a db 'xxh.',cr,lf msg3_len equ $-msg3 msg4 db cr,lf,'EXEC call failed, error code=' msg4a db 'xxh.',cr,lf msg4_len equ $-msg4 msg5 db cr,lf,'Root segment regained control!',cr,lf msg5_len equ $-msg5 _DATA ends _STACK segment para stack 'STACK' db stksize dup (?) _STACK ends end main ; defines program entry point Figure 6 name overlay title 'OVERLAY segment' ; ; OVERLAY.OVL --- a simple overlay segment ; loaded by ROOT.EXE to demonstrate use of ; the MS-DOS EXEC call subfunction 03H. ; ; The overlay does not contain a STACK segment ; because it uses the ROOT segment's stack. ; ; Ray Duncan, June 1987 ; stdin equ 0 ; standard input stdout equ 1 ; standard output stderr equ 2 ; standard error cr equ 0dh ; ASCII carriage return lf equ 0ah ; ASCII linefeed _TEXT segment byte public 'CODE' ; executable code segment assume cs:_TEXT,ds:_DATA ovlay proc far ; entry point from root segment mov ax,_DATA ; set DS = local data segment mov ds,ax ; display overlay message ... mov dx,offset msg ; DS:DX = address of message mov cx,msg_len ; CX = length of message mov bx,stdout ; BX = standard output handle mov ah,40h ; AH = fxn 40h, write file/device int 21h ; transfer to MS-DOS ret ; return to root segment ovlay endp ; end of ovlay procedure _TEXT ends _DATA segment para public 'DATA' ; static & variable data segment msg db cr,lf,'Overlay executing!',cr,lf msg_len equ $-msg _DATA ends end Customizing a Microsoft Windows Dialog Box with New Control Classes Gregg L. Spaulding Although the power inherent in Microsoft(R) Windows Presentation Manager is seen by anyone who becomes acquainted with its programming environment, some tools are just not provided with the interface. For one thing, although one of the strengths of Windows is its controls--child windows belonging to a predefined class and having predefined behaviors, which can be used by any application or, more frequently, by a dialog box--this feature does not handle all situations effectively. Windows has five predefined control types: list box, button, static, edit, and scrollbar. Each of these has a window procedure associated with it, which is an integral part of Windows and takes a great deal of burden off the programmer. But suppose, for example, that we have a list of items and want a user to pick one. Typically, this would be the job of the list box, which displays a list of items with a scroll bar on the right-hand side and highlights the current selection. The list box is effective, especially for a long list. If, however, a list has only three or four items, the list box is cumbersome and takes up a lot of space on the screen. An alternative would be to represent the choices as a group of radio buttons. But that also uses a great deal of screen space and would be difficult to do if the number of items and the items themselves were not known in advance. What is needed therefore is a custom control. MSJ readers will remember that an in-depth analysis on designing custom controls was presented by Kevin Welch in "Creating User-Defined Controls for Your Own Windows Applications," MSJ (Vol. 3, No. 4). In this article we will focus on developing and implementing a new and practical custom control class to fill the special need defined above. We'll call this new control the select class. This new class, once developed, can then be added to your toolkit of Windows tools for use in any Windows program. The Select Class We begin by defining what the custom behavior of the new control, the select class, will be so that we can write its window class procedure. For our purposes, the control should display only the currently selected item at any given moment and should be highlighted if it currently has the input focus. The arrow keys or a click of the left mouse button should cause the selection to change. A click of the right mouse button should pop up a list of all available selections; the user should be able to select the desired item from the list by clicking on it. We will also need to provide an interface for the select class similar to that of list box. This interface would consist of a message to add a new item to the list, messages to set and get the index of the current selection, a message to get the currently selected item, and a message to get the count of items in the list. Additionally, the control should send a message to its parent anytime the current selection is changed. The parent can, of course, ignore this message if it desires. The control should also contain a picture to indicate that it is our customized select control, not just static text being displayed on the screen. The picture we will use is that of two arrows chasing each other in a circle. These circular arrows will be implemented as a 16-by 16-bitmap (see Figure 1). Fragments from the source code for the select control are shown in Figure 2. Select and Windows Since the select control has to be available to all applications, it must reside in a dynamic-link library (DLL). The mere mention of a DLL often strikes fear into the hearts of many Windows programmers, but it shouldn't. There's nothing magical about a DLL. There are simply a few extra rules to follow. First, since SS != DS, the DLL must be compiled using the compiler switch -Asnw for small models or -Alnw for medium models. All passed pointers must be long for this same reason. Second, a DLL does not have an entry point like WinMain in a Windows application. Instead, Windows makes a call to a programmer-supplied initialization routine when the library is first loaded into memory. This routine must be written in assembly language although it can call to C to do most of its work. Figure 3 demonstrates how I've done that in the files LIBENTRY.ASM and LIBMAIN.C. The LIBMAIN routine makes a call to LocalInit to initialize the local heap, unlocks the data segment (which is left locked by LocalInit), and calls RegisterSelect, which will register the window class with Windows itself. After all that is performed, the class becomes available to all applications. In order to install the select class into Windows without any application being aware of it, the DLL's EXE file must be placed into the win.ini file on the load= line. This will force Windows to call its initialization function when Windows is started. The control will remain registered and available for the duration of any given session in which it was loaded. The RegisterSelect function called by LibMain looks much like any window class registration, except that the cbWndExtra field of the class structure is set to 2*sizeof(WORD) instead of 0. When CreateWindow is called, Windows allocates a window structure internally and places the parameters in it. Setting cbWndExtra in the window class structure to a nonzero value instructs Windows to allocate that many extra bytes at the end of each window structure created by CreateWindow with this window class. The two extra words that are requested in this case will be used by the control class procedure to identify information unique to each instance (or window) of the control. This will become more apparent later. The procedure also creates a bitmap for the circular arrows, since it would be wasteful for each iteration to create its own copy. Thus, only one is created and shared by all controls of the select class. Writing the Procedure The first thing we need in order to write the window class procedure is a structure defining the data that is unique to a particular instance of the control. Since it is possible to define the control to be any size, let's establish some standards. The bitmap will always appear flush against the left edge of the control window, with the currently selected text item appearing immediately to its right. Both items will be centered vertically in the control. To set this standard, the structure must have values indicating the y starting positions for both the bitmap and the text relative to the top of the window. To calculate the minimum bounding box around a text string, the character width must also be retained. Since we want to be able to pop up a list of all items, we will need to remember the height of the characters. That way the window that is displaying them can be of minimum size. We also will need a field to retain the handle to that pop-up window. The structure will need to store a rectangle defining the boundaries of the bitmap and text, a Boolean flag indicating whether or not it has the current input focus, two integers describing the number of items in the list and the index of the current selection, and an array of memory handles to the strings themselves. Notice that in the listing of select.c the array of handles in the structure is declared to be of size 1 and is used only as a placeholder. The size of the structure will be dynamically altered to accommodate the actual array size at run time. Responding to Messages When the window procedure receives a WM_CREATE message from Windows, it must allocate the control structure defined previously from the local heap, initialize it, and place the memory handle into the window structure using the SetWindowWord function. The GWW_MEMHANDLE argument, which is defined as 0, instructs SetWindowWord to place the passed word at offset 0 from the bottom of the window structure. This is the first of the two extra words that are allocated by the setting of cbWndExtra in RegisterSelect to 2*sizeof(WORD). The second word will be set to the current size of the data structure since it can change as strings are added. The WM_DESTROY message must also be processed by the window procedure, since we allocated memory from the local heap and must release it. Also, a call to LocalShrink is made so as to reduce the size of the data segment in case it was expanded by a large number of LocalAlloc calls. Data segments are moveable but not discardable so we want to keep them as small as possible. Since the control is likely to be invoked by a dialog box, it should have the standard dialog box interface (tab, back tab). The dialog manager takes care of this automatically by capturing these keys, but note that it captures the arrow keys as well. Because the procedure needs the arrow messages in order to function properly, this is a problem. Fortunately, the dia-log box manager sends a WM_GETDLGCODE message to all controls when it creates them. By responding to it with DLGC_WANTARROWS, we inform the dialog manager that it should pass WM_KEYUP and WM_KEYDOWN messages for the arrow keys directly to the procedure without acting upon them itself. Since we want the control to highlight the current selection when it has the input focus, we will have to capture the WM_SETFOCUS and the WM_KILLFOCUS messages. Rather than actually performing the highlighting in response to these messages, let's defer that task to the paint routine. When focus messages are received, all that is necessary is to set or reset the bFocus flag in the structure and call InvalidateRect to force a repaint. This prevents duplication of the paint code. Next, we need to think about how the selection can be changed. It must change in response to an arrow keystroke, to mouse clicks, and to the SL_SETCURSEL message, which is defined in select.h (refer back to Figure 2). Rather than duplicating the code to change selection, it is easier to write a general purpose routine, ChangeSelection, which will perform the action. This function must accept the window handle of the control, a Boolean flag indicating whether the change is to be made to an absolute selection or relative to the current selection, and an integer indicating the change to make. The routine only needs to modify the index value in the control's structure to reflect the new selection and to call InvalidateRect to force a repaint of the control. Since we decided that the control should inform its parent of any change to the selection, this would be the best place to do it. We must therefore call PostMessage with the message value equal to WM_COMMAND and the high word of lParam equal to SLN_SELCHANGE. This conforms to the same standard used by list boxes. With that done, the processing of WM_KEYUP, WM_LBUTTONUP, and SL_SETCURSEL messages becomes almost trivial. They need only make calls to ChangeSelection. It is also worth remembering that the WM_LBUTTONDOWN and WM_RBUTTONDOWN commands should cause the control to gain the input focus so these are captured and a simple call to SetFocus is made. Processing the message WM_RBUTTONUP is a little more complicated. This is the case in which we want to pop up a full list of the available selections for the user. This pop-up window should appear directly below the select control and be the same width. Conveniently, the control structure contains the size of the control in the frect field. This will give us the window's width. The control structure also contains the number of items in the list and the characters' height, from which we can easily calculate the height of the window. All of the values, however, are in client coordinates. Since we want the pop-up window to be able to overlap the edges of the dialog box in which it appears, it must have the WS_POPUP rather than WS_CHILD style. This implies that the x and y coordinates that are sent to CreateWindow must be in screen rather than client coordinates. This problem is easily solved by calling the Windows routine ClientToScreen to map our calculated coordinates to the screen. Since the functionality desired of the pop-up window is basically that of a list box, it makes sense to use one. We will create a list box with styles WS_POPUP, WS_DLGFRAME, and LBS_NOTIFY. We don't need the WS_VSCROLL type since the box will be exactly large enough to hold all the choices. Once the window is created, it must be filled in the standard fashion by sending it LB_ADDSTRING messages. And its current selection should be set to the same as the select control. To prevent any possible confusion, we must also disable the parent window at this time. Since we have given the list box the LBS_NOTIFY style, it will send us a WM_COMMAND message when the selection is changed. When this is received, we need only update the selection in the select control, destroy the list box, and reenable the parent window. In response to a WM_PAINT message, the select control must, as with any window procedure, paint its window. Since the locations for the bitmap and text were saved in the control structure during the processing of the WM_CREATE message, this task will involve a simple call to BitBlt to draw the bitmap and a call to TextOut to write the currently selected text. Additionally, if the control has the input focus as defined by the bFocus flag in the control structure, a call to InvertRect is made to highlight the selection. What could be simpler? To complete our window class procedure, we must process the additional user-defined messages that can be sent by an application or a dialog box procedure. The SL_ADDSTRING message must add the string pointed to by lParam to the current list of strings associated with the receiving control. This will involve allocating local memory for the string and copying it, then placing the handle to that local memory into the array of handles stored in the control's structure. The structure, however, is only large enough to hold the number of handles that are currently defined in the array. For this reason, we must use LocalReAlloc to expand the size of the structure to add the new handle. Since this function requires the desired new size (not the increment or decrement) as an argument, we must retrieve the current size from the Window structure using GetWindowWord with the GWW_MEMSIZE argument defined at the top of select.c. Once the control structure has been expanded and the new handle added, the count field must be incremented. The SL_GETCURSEL and SL_GETCOUNT messages return the value found in the index and count fields of the control structure, respectively. Note that a value returned by a window function is returned as the value of SendMessage or SendDlgItemMessage. Processing the SL_GETTEXT message is relatively straightforward. The string pointed to by handle[wParam] in the control structure is copied to the location pointed to by lParam. In some cases the lParam value is used to represent a long pointer to a string. Therefore, in order to prevent a caller's data segment from being moved before the message is processed and the pointer made invalid, the messages should be sent using either SendMessage or SendDlgItemMessage rather than PostMessage. The former two calls cause Windows to call the window class procedure directly as a function, whereas the latter places the message in a queue and eventually will call the control function via DispatchMessage after allowing the memory manager to move things around. Memory Considerations A few words should be said about select class's make file and def file. I compiled this as a medium model (although it would also work as a small model) to minimize the memory it uses at any given instant. Since the initialization code in LIBMAIN.C is used only when Windows is started up, it is in a separate discardable segment named _INIT. Then, after initialization is completed, only the _SELECT segment with the actual window class procedure needs to be in memory--and its size is less than 2Kb. Note that in the def file, HEAPSIZE is specified as 200 bytes. Certainly this will not be enough, but Windows will be more than happy to relocate the entire data segment in global memory and expand the heap as necessary. Unfortunately, it will never shrink it back down again. This is why (as mentioned previously), in response to the WM_DESTROY message, the control procedure attempts to shrink the segment back to its initial size manually using the LocalShrink function. Windows will never shrink the heap smaller than the currently allocated amount so this process is always safe. DEMO Program Figure 4 lists the source code for a very simple Windows program, DEMO, that uses the select control, and Figures 5, 6, and 7 show DEMO making use of the select control. To run DEMO, edit the win.ini file to include select.exe on the "LOAD=" line. Next, run demo.exe from Windows. Finally, clicking on the dialog menu in DEMO brings up the new select control. As DEMO and the newly developed select class show, writing customized controls is not as difficult as it is often perceived to be. In fact, once there is a clearly defined need and a clearly defined set of design goals and parameters, all that is left is the implementation. As shown here, that's not all that difficult. All it takes is a little patience and a willingness to explore the environment and take advantage of the powerful features provided by Windows. Figure 2 SELECT.C - Source File ■ ■ ■ typedef struct { int yBitStart; /* y location to start bitmap at */ int yCharStart; /* y location to start text at */ int xCharWidth; /* width of a character */ int yCharHeight; /* height of character */ RECT frect; /* formatting rectangle for text */ BOOL bFocus; /* this control has the input focus HWND hWndList; int index, count; /* which item is selected and how * many items total */ HANDLE handle[1]; /* expandable array of handles to * strings */ } HSELECT; typedef HSELECT NEAR * NPHSELECT; ■ ■ ■ long FAR PASCAL SelectWndProc(HWND hWnd, unsigned message, WORD wParam, LONG lParam) { HDC hDC, hMemDC; /* display contexts for drawing */ PAINTSTRUCT ps; /* for BeginPaint and EndPaint */ TEXTMETRIC tm; /* need info about character * sizes */ RECT rect; /* generic */ HANDLE hSelect, hString; /* memory handles */ NPHSELECT pSelect; /* pointer to structure header */ NPSTR pString; /* pointer to a string */ POINT pt1,pt2; int i; /* generic */ switch (message) { case WM_CREATE: /* calculate size of initial node not including any */ /* handles to strings, allocate memory and lock it */ i = sizeof(HSELECT)-sizeof(HANDLE); if ((hSelect=LocalAlloc( LMEM_MOVEABLE, i ) ) == NULL) { MessageBox(hWnd, szMemErr, NULL, MB_ICONEXCLAMATION); break; } pSelect = (NPHSELECT)LocalLock( hSelect ); ■ ■ ■ /* fill in the structure */ GetClientRect ( hWnd, &rect ); pSelect->yCharStart = (rect.bottom - i) / 2; pSelect->xCharWidth = tm.tmAveCharWidth; pSelect->yBitStart = (rect.bottom - YBITMAP)/2; rect.left = XBITMAP + 4; rect.top = pSelect->yCharStart - 2; rect.bottom = rect.top + i + 4; pSelect->frect = rect; pSelect->count = 0; pSelect->index = -1; pSelect->hWndList = pSelect->bFocus = FALSE; ■ ■ ■ case WM_DESTROY: /* all we have to do is free up all associated memory */ ■ ■ ■ case WM_PAINT: hDC = BeginPaint ( hWnd, &ps ); /* lock down structure associated with this control */ hSelect = GetWindowWord ( hWnd, GWW_MEMHANDLE ); pSelect = (NPHSELECT)LocalLock ( hSelect ); /* draw the bitmap'd circular arrows */ hMemDC = CreateCompatibleDC ( hDC ); SelectObject ( hMemDC, hBitmap ); BitBlt ( hDC, 2, pSelect->yBitStart, XBITMAP, YBITMAP, hMemDC, 0, 0, SRCCOPY ); DeleteObject ( hMemDC ); /* draw the current string selection */ rect = pSelect->frect; if (pSelect->index > -1) { hString = pSelect->handle[pSelect->index]; pString = LocalLock ( hString ); i = lstrlen(pString); TextOut ( hDC, rect.left+2, pSelect->yCharStart, pString, i ); LocalUnlock( hString ); ■ ■ ■ case WM_RBUTTONUP: /* pop up list box with all choices */ hSelect = GetWindowWord ( hWnd, GWW_MEMHANDLE ); pSelect = (NPHSELECT)LocalLock ( hSelect ); GetClientRect( hWnd,&rect ); /* calculate corners of box in screen coordinates */ pt1.x = pSelect->frect.left; pt1.y = rect.bottom; pt2.x = rect.right; pt2.y = pt1.y + pSelect->yCharHeight * pSelect->count; ClientToScreen(hWnd,&pt1); ClientToScreen(hWnd,&pt2); pt2.x += 2*GetSystemMetrics( SM_CXDLGFRAME ); pt2.y += 2*GetSystemMetrics( SM_CYDLGFRAME ); pSelect->hWndList = CreateWindow ( "listbox", "", WS_POPUP | WS_DLGFRAME | LBS_NOTIFY, pt1.x, pt1.y, pt2.x - pt1.x, pt2.y - pt1.y, hWnd, NULL, hInstLib, NULL ); /* WINDOWS has set parent to be the dialog manager; */ /* we must make this call to regain our child */ SetWindowWord ( pSelect->hWndList, GWW_HWNDPARENT, hWnd); ■ ■ ■ case WM_COMMAND: /* listbox informing us that something has happened */ hSelect = GetWindowWord(hWnd, GWW_MEMHANDLE ); pSelect = (NPHSELECT)LocalLock ( hSelect ); PostMessage ( hWnd, SL_SETCURSEL, SendMessage ( pSelect->hWndList, LB_GETCURSEL, 0, 0L), 0L ); if (HIWORD(lParam) == LBN_SELCHANGE) { DestroyWindow(pSelect->hWndList); pSelect->hWndList = NULL; EnableWindow (GetParent(hWnd), TRUE); SetFocus(hWnd); } LocalUnlock(hSelect); break; ■ ■ ■ static void near ChangeSelection (HWND hWnd, BOOL bRel, int sel) { HANDLE hSelect; NPHSELECT pSelect; /* get the handle to the structure and lock it down */ hSelect = GetWindowWord ( hWnd, GWW_MEMHANDLE ); pSelect = (NPHSELECT)LocalLock( hSelect ); /* if change is relative, take care of it */ if (bRel) { if (sel > 0) pSelect->index++; else if (sel < 0) pSelect->index--; if (sel > 0 && pSelect->index == pSelect->count) pSelect->index = 0; else if (sel < 0 && pSelect->index < 0) pSelect->index = pSelect->count - 1; } /* if change is absolute, set it */ else if(sel >= -1 && sel < pSelect->count) pSelect->index = sel; /* force a repaint */ InvalidateRect ( hWnd, &(pSelect->frect), TRUE ); LocalUnlock( hSelect ); /* inform our parent that we are changing selection */ PostMessage ( GetParent(hWnd), WM_COMMAND, GetWindowWord (hWnd, GWW_ID), MAKELONG(hWnd, SLN_SELCHANGE)); } Figure 3 Source code for LIBENTRY.ASM Extrn LibMain:far _TEXT SEGMENT BYTE PUBLIC 'CODE' ASSUME CS:_TEXT PUBLIC LibEntry LibEntry PROC FAR push di ; hInstance push ds ; data segment push cx ; heap size push es push si call LibMain ret LibEntry ENDP _TEXT ENDS End LibEntry Source code for LIBMAIN.C #include <windows.h> HANDLE hInstLib; HANDLE hBitmap; #define XBITMAP 16 #define YBITMAP 16 static BYTE byBits[] = { 0xFE, 0xFF, 0xFE, 0x7F, 0xF8, 0x3F, 0xE6, 0x7F, 0xDE, 0xF7, 0xBF, 0xFB, 0x7F, 0xFD, 0x7F, 0xFD, 0x7F, 0xFD, 0x7F, 0xFD, 0xBF, 0xFB, 0xDE, 0xF7, 0xFC, 0xCF, 0xF8, 0x3F, 0xFC, 0xFF, 0xFE, 0xFF }; /* some prototypes */ BOOL NEAR RegisterSelect (HANDLE hInstance); long FAR PASCAL SelectWndProc( HWND hWnd, unsigned message, WORD wParam, LONG lParam); /* this function is called from the assembler initialization in libentry.asm */ int FAR PASCAL LibMain(HANDLE hInstance, WORD wDataSeg, WORD wHeapSize, LPSTR lpCmdLine) { int iret; if (wHeapSize == 0) return(0); iret = LocalInit(wDataSeg, NULL, (NPSTR)wHeapSize); UnlockData(0); RegisterSelect ( hInstLib = hInstance ); return iret; } /* this is called by LibMain to register the class and initialize the bitmap */ static BOOL NEAR RegisterSelect (HANDLE hInstance) { WNDCLASS wndclass; wndclass.style = CS_HREDRAW | CS_VREDRAW; wndclass.lpfnWndProc = SelectWndProc; wndclass.cbClsExtra = 0; wndclass.cbWndExtra = 2*sizeof(WORD); /* space for * control info */ wndclass.hIcon = NULL; wndclass.hInstance = hInstance; wndclass.hCursor = LoadCursor(NULL, IDC_ARROW); wndclass.hbrBackground = COLOR_WINDOW+1; wndclass.lpszMenuName = NULL; wndclass.lpszClassName = "select"; hBitmap=CreateBitmap ( XBITMAP, YBITMAP, 1, 1, byBits ); return RegisterClass(&wndclass); } Figure 4 DEMO - Make file demo.res: demo.dlg demo.rc demo.h rc -r demo.rc demo.obj: demo.c demo.h cl -c -AS -Gsw -G2 -Ownlt -Zpe -W2 demo.c demo.exe: demo.obj demo.res demo.def link4 demo, demo.exe/NOD, demo.map/map, slibw slibcew, demo.def rc demo.res DEMO.DLG - Dialog file Note: The highlighted lines below must be entered as part of each preceding line - these lines cannot be broken in the actual file TEST DIALOG LOADONCALL MOVEABLE DISCARDABLE 26, 54, 132, 65 STYLE WS_DLGFRAME | WS_VISIBLE | WS_POPUP BEGIN CONTROL "Ok", IDD_OK, "button", BS_DEFPUSHBUTTON | WS_TABSTOP | WS_CHILD, 47, 42, 37, 14 CONTROL "", IDD_COLORS, "select", WS_TABSTOP | WS_CHILD, 68, 6, 40, 14 CONTROL "", IDD_STRINGS, "select", WS_TABSTOP | WS_CHILD, 68, 25, 55, 14 CONTROL "Color", IDD_NULL, "static", SS_RIGHT | WS_CHILD, 4, 9, 61, 8 CONTROL "Company", IDD_NULL, "static", SS_RIGHT | WS_CHILD, 8, 27, 57, 8 END DEMO.DEF - Definition file NAME demo STUB 'WINSTUB.EXE' CODE LOADONCALL MOVEABLE DISCARDABLE DATA MOVEABLE MULTIPLE STACKSIZE 2048 HEAPSIZE 1024 EXPORTS DemoWndProc @1 DemoDlg @2 DEMO.RC - Resource File #include <windows.h> #include "demo.h" demo MENU BEGIN MENUITEM "Dialog...", IDM_TEST END STRINGTABLE BEGIN IDS_COLOR1, "RED" IDS_COLOR2, "BLUE" IDS_COLOR3, "GREEN" IDS_COLOR4, "ORANGE" IDS_COLOR5, "YELLOW" IDS_COLOR6, "PURPLE" IDS_COLOR7, "BLACK" IDS_COLOR8, "WHITE" IDS_STRING1, "IBM" IDS_STRING2, "Microsoft" IDS_STRING3, "Tencor" IDS_STRING4, "Varian" IDS_STRING5, "Other" END rcinclude demo.dlg DEMO.H - Header File /* dialog box id's */ #define IDD_OK 1 #define IDD_NULL -1 #define IDD_COLORS 10 #define IDD_STRINGS 11 /* menu id's */ #define IDM_TEST 100 /* string id's */ #define IDS_COLOR1 1 #define IDS_COLOR2 2 #define IDS_COLOR3 3 #define IDS_COLOR4 4 #define IDS_COLOR5 5 #define IDS_COLOR6 6 #define IDS_COLOR7 7 #define IDS_COLOR8 8 #define IDS_STRING1 9 #define IDS_STRING2 10 #define IDS_STRING3 11 #define IDS_STRING4 12 #define IDS_STRING5 13 DEMO.C - Source Code Listing #include <windows.h> #include "select.h" #include <stdio.h> #include <string.h> #include "demo.h" HWND hWndDemo; HANDLE hInstDemo; static char szClassName[] = "demo"; static int nTrueFalse = 0; static int nString = 0; int PASCAL WinMain ( HANDLE hInstance, HANDLE hPrevInstance, LPSTR lpszCmdLine, int cmdShow ); long FAR PASCAL DemoWndProc ( HWND hWnd, unsigned message, WORD wParam, LONG lParam ); void DemoCommand ( HWND hWnd, WORD wCmd ); BOOL FAR PASCAL DemoDlg ( HWND hWndDlg, unsigned message, WORD wParam, LONG lParam ); int PASCAL WinMain ( HANDLE hInstance, HANDLE hPrevInstance, LPSTR lpszCmdLine, int cmdShow ) { MSG msg; WNDCLASS Class; if (hPrevInstance) return TRUE; Class.hCursor = LoadCursor(NULL,IDC_ARROW); Class.hIcon = NULL; Class.cbClsExtra = 0; Class.cbWndExtra = 0; Class.lpszMenuName = szClassName; Class.lpszClassName = szClassName; Class.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); Class.hInstance = hInstance; Class.style = CS_VREDRAW | CS_HREDRAW; Class.lpfnWndProc = DemoWndProc; if (!RegisterClass(&Class)) return FALSE; hWndDemo = CreateWindow( szClassName, szClassName, WS_OVERLAPPEDWINDOW, 30, 50, 200, 150, NULL, NULL, hInstDemo=hInstance, NULL); ShowWindow ( hWndDemo, cmdShow ); UpdateWindow ( hWndDemo ); while (GetMessage( &msg, NULL, 0, 0 )) { TranslateMessage((LPMSG)&msg); DispatchMessage((LPMSG)&msg); } return msg.wParam; } long FAR PASCAL DemoWndProc ( HWND hWnd, unsigned message, WORD wParam, LONG lParam ) { switch (message) { case WM_COMMAND: DemoCommand ( hWnd, wParam ); break; default: return DefWindowProc (hWnd, message, wParam, lParam) ; } return (long) 0 ; } void DemoCommand ( HWND hWnd, WORD wCmd ) { int i; HMENU hMenu; FARPROC lpfnDlg; switch (wCmd) { case IDM_TEST: lpfnDlg = MakeProcInstance( DemoDlg, hInstDemo ); DialogBox( hInstDemo, "TEST", hWnd, lpfnDlg ); FreeProcInstance ( lpfnDlg ); break; default: break; } } BOOL FAR PASCAL DemoDlg ( HWND hWndDlg, unsigned message, WORD wParam, LONG lParam ) { char szBuf[40]; int i; switch ( message ) { case WM_INITDIALOG: for ( i = IDS_COLOR1 ; i <= IDS_COLOR8 ; i++ ) { LoadString( hInstDemo, i, szBuf, 40 ); SendDlgItemMessage( hWndDlg, IDD_COLORS, SL_ADDSTRING, 0, (LONG)(LPSTR)szBuf ); } for ( i = IDS_STRING1 ; i <= IDS_STRING5 ; i++ ) { LoadString( hInstDemo, i, szBuf, 40 ); SendDlgItemMessage( hWndDlg, IDD_STRINGS , SL_ADDSTRING, 0, (LONG)(LPSTR)szBuf); } SendDlgItemMessage( hWndDlg, IDD_COLORS, SL_SETCURSEL, nTrueFalse, 0L ); SendDlgItemMessage( hWndDlg, IDD_STRINGS, SL_SETCURSEL, nString, 0L ); return TRUE; case WM_COMMAND: if (wParam == IDD_OK) { nTrueFalse = (int)SendDlgItemMessage( hWndDlg, IDD_COLORS, SL_GETCURSEL, 0, 0L ); nString = (int)SendDlgItemMessage( hWndDlg, IDD_STRINGS, SL_GETCURSEL, 0, 0L ); EndDialog( hWndDlg, TRUE ); } break; default: return FALSE; } return TRUE; } Techniques for Calling OS/2 System Services from BASIC Programs Ethan Winer One of the great myths about programming for OS/2 operating systems is that it must be done using either C or assembly language. Nearly all of the books and magazine articles that describe OS/2 system services rely on these two languages exclusively for their examples. In this article I will disprove that myth and show you how to access a variety of useful OS/2 services using the powerful Microsoft(R) BASIC Compiler Version 6.0. Of all the high-level languages currently available, BASIC remains the most immediately accessible to the largest number of people. It is the most English-like and intuitive language and uses a command syntax that is simple and easy to understand. With the introduction of Microsoft QuickBASIC Version 4.5 and BASIC 6, no one can dispute that BASIC is now suitable for serious program development. This article will look at two different methods for implementing OS/2 services. The first uses purely BASIC commands in which an appropriate data structure is defined and the various system functions are called by name. This allows the BASIC programmer to immediately set up and call any OS/2 service using only BASIC statements. The second method provides an assembly language interface between the BASIC program and the OS/2 operating systems. The advantage of using assembly language is a reduction in the total amount of code and data space needed in a program. Although its use does require a bit more effort at the outset, the final result will clearly be worth it to programmers who want to create the smallest and fastest programs possible. The OS/2 functions presented in this article can serve as utilities or routines for use in BASIC programs. For example, I will not cover opening and reading files, because BASIC already provides those as part of the language. But I will present programs that perform useful disk-related functions, such as getting and setting the current default drive and obtaining a list of file names. OS/2 Essentials Many OS/2 services require the address of a data structure in which the various parameters will be passed between it and the calling program. There are, however, several different types of structures used, and setting aside memory for every possible variation will impinge on the available data space. By using local stack variables in the assembler routines, this space can be set aside before calling OS/21 and then recovered when it is no longer needed. Using the stack for variable storage also ensures that these routines will support reentrant code if and when that feature is added to the BASIC run-time libraries. Similarly, all of the BASIC subprograms and functions presented here do not use the STATIC keyword. Therefore, they too will use the stack for all of their working variables. One minor complication in interfacing BASIC to OS/2 services arises from the way in which BASIC maintains string variables and constants. While OS/2 expects all strings to be terminated with a CHR$(0) 0 byte, BASIC uses a 4-byte descriptor to show where in memory a string is located and how long it is. This incompatibility is easily resolved by simply appending a 0 byte to the string before passing its address to the OS/2 routine. Likewise, strings that are returned by OS/2 are terminated with a 0 byte. Thus, the BASIC program must perform an extra step to locate that byte in order to determine where the string ends. Unlike DOS services, which are accessed via Interrupt 21h, the OS/2 operating system allows you to call system services by name. This is a tremendous improvement from the programmer's point of view and provides the most natural way to extend a high-level language. No matter which OS/2 service is going to be called, each expects one or more parameters in a particular order. There are a number of sources that list all of the OS/2 functions and the parameters they expect. I recommend Advanced OS/2 Programming by Ray Duncan (Microsoft Press) and the OS/2 Technical Reference manual published by IBM. BASIC 6.0 can't use all of the OS/2 functions that are described in these manuals, however. The reader should refer to the Include files provided with BASIC 6.0, which include all the functions BASIC 6.0 does support. Nearly all OS/2 services are set up to be declared and invoked as functions from BASIC. Although they could just as easily be called as subroutines, there would be no way to determine the success or failure of the call. Integer functions that will be used with BASIC are designed to return their value in the AX register. Because OS/2 also uses AX to indicate the success of a given system call, this provides a simple and natural link between BASIC and OS/2. It is important to understand that external functions must be declared before they can be used in a program. BASIC and assembler subprograms can be invoked with the CALL keyword, which eliminates the requirement of declaring them. With functions, however, BASIC would have no way of knowing the difference between a function reference and an ordinary variable or array element. For example, the GetDrive$ function that you will see in a moment returns the current drive as a character. But if it hasn't been declared, it will appear to be an ordinary string variable to the compiler: CurDrive$ = GetDrive$ Disk Services The first function to consider is GetDir$, which returns a string containing the current directory for any valid drive. It will first be presented as a purely BASIC function, and then the steps required to translate it into assembly language will be shown. In both of these cases it will be relying on the OS/2 DosQCurDir (Dos Query Current Directory) service, which does the real work. GETDIR.BAS, the BASIC program shown in Figure 1, is intended to be compiled and linked with your main programs as a separate module. We will take a look at each program statement in turn. To offer the most flexibility, the GetDir$ function has been designed to accept either a drive letter or a null string to indicate the current default drive. Therefore, the first step is to see which was used. If the string is not null, then the drive number is derived by subtracting 64 from an uppercase version of the letter. Otherwise, the drive number is assigned to 0. This corresponds to the OS/2 system, where drive A is indicated with a 1, B with a 2, and so forth. The next step is to create a buffer string in which OS/2 will place the name of the current directory. The maximum length for a directory name is 64 characters, which is the same as the DOS operating system. If this changes in a future version of OS/2, simply modify the value 64 in this program. Path$ is then assigned to that many spaces. As mentioned earlier, DosQCurDir is invoked as a function rather than being called, because it returns any error codes as a function value. Thus, DOSError will be set to 0 if no error occurred, or it will hold an OS/2 error code. If an error does occur, GetDir$ will return a null string to indicate this. You could, of course, modify the function to return the error, perhaps in an additional passed parameter. Figure 1 '***** GetDir.Bas - BASIC function that returns the current directory 'syntax: Directory$ = GetDir$(Drive$) ' where: Drive$ is "A", "C", etc, or a null string meaning ' the current drive DEFINT A-Z DECLARE FUNCTION DosQCurDir%(BYVAL Drive, BYVAL StrSeg, _ BYVAL StrAdr, SEG Length) FUNCTION GetDir$(Drive$) IF LEN(Drive$) THEN 'see if a drive letter was specified DriveNum = ASC(UCASE$(Drive$)) - ASC("A") + 1 'convert to a drive number ELSE DriveNum = 0 'else, tell OS/2 to use the current 'drive END IF BufLen = 64 'define the buffer's length Path$ = SPACE$(BufLen) 'create a buffer to receive the 'directory 'invoke the function ErrorCode = DosQCurDir%(DriveNum, VARSEG(Path$), SADD(Path$), _ BufLen) IF ErrorCode THEN 'see if there was an error GetDir$ = "" 'return null string to indicate the 'error ELSE 'otherwise return what precedes the 'CHR$(0) GetDir$ = "\" + LEFT$(Path$, INSTR(Path$, CHR$(0)) - 1) END IF END FUNCTION Variable Passing Methods The DosQCurDir function requires three parameters to be passed to it. The first is the drive number as described above. A point worth mentioning here is that DosQCurDir expects the actual value of the drive number, as opposed to an address of a variable set to that value. This is why the BYVAL statement is used in the function declaration. When a subprogram or function is declared with BYVAL preceding a parameter's name, BASIC pushes the value of the parameter onto the stack, instead of its address, which is the default method. In general, OS/2 parameters that will be modified by a function must be passed as a segmented address, whereas parameters that will only be read by the routine are passed by value. By default, BASIC passes variables by reference so it will always be necessary to use BYVAL when calling OS/2 services. Since all OS/2 calls are FAR calls, the SEG keyword will be required as well. The second parameter is the string that was already assigned to a length capable of holding the returned directory name. BASIC uses string descriptors to indicate where and how long a given string is. OS/2, however, expects both the segment and the address of the string data, as opposed to the address of a BASIC string descriptor. Therefore, both VARSEG (Variable Segment) and SADD (String Address) must be used when sending a BASIC string to an OS/2 function. The third parameter tells DosQCurDir how long a string you are sending to it. If the string is too short, the function will return an error; but it will also assign the BufLen variable to the required length. This way DOSQCurDir may be invoked again with the string set to the proper length. In that case, though, Path$ will have been assigned to a length that you know will be sufficient before calling DosQCurDir. The final step in the GetDir function is to locate the CHR$(0) 0 byte that marks the end of the returned string and keep only those characters that precede it. This is accomplished with a combination of BASIC's INSTR and LEFT$ functions. Notice that OS/2 does not return the leading backslash, so it is added in the GetDir$ function. With an understanding of how GetDir$ works as a foundation, you can now proceed to several other useful routines. Indeed, none of the remaining routines to be created in BASIC are any more difficult than GetDir$; several of them are, in fact, much simpler. GetDrive and SetDrive Related to obtaining the current directory (and indeed much simpler to implement) is getting and setting the current default drive. The GetDrive routine is designed as a function, while SetDrive is intended to be called as a subprogram. The routines are shown in Figures 2 and 5 respectively. DosQCurDisk, the OS/2 service to obtain the current default drive, expects the segmented addresses of two variables. The first is an integer that will receive the drive number; the second is a long integer that receives a drive "map." The drive is returned as 1 for drive A, 2 for B, and so forth. Thus, 64 must be added to this value so the GetDrive function can return a value that is equivalent to an ASCII character. The drive map variable is bit coded to indicate all of the valid drives in the host PC (see Figure 3). Even though GetDrive does not use the drive map information, it is easy to write a new function that would indicate if any given drive is legal. The lowest 26 bits in the long integer are set to either 1 or 0 to indicate if a given drive is present. The Valid program shown in Figure 4 will accept any drive letter and returns a value to indicate if the drive exists. Valid is set up to return either 0 or -1, which allows the optional use of the NOT operator: IF Valid%(Drive$) THEN or IF NOT Valid%("C") THEN Setting the drive is equally simple, which you can see by examining the SetDrive routine that is shown in Figure 5. Because the DosSelectDisk service does not return information, the new drive to be assigned is passed as a value. As with the other routines presented thus far, any errors that are encountered are discarded. That is, if you try to assign a drive that doesn't exist, the request will simply be ignored. Figure 2 '***** GetDrive.Bas - BASIC function that returns the current drive 'syntax: Drive$ = CHR$(GetDrive%) ' where: Drive$ is returned as "A", "B", and so forth DEFINT A-Z DECLARE SUB DosQCurDisk(SEG Drive, SEG DriveMap&) FUNCTION GetDrive% CALL DosQCurDisk(Drive, DriveMap&) GetDrive% = (Drive - 1) + ASC("A") END FUNCTION Figure 4 '***** Valid.Bas - BASIC function that reports if a drive ' letter is valid 'syntax: Okay = Valid%(Drive$) ' where: Okay receives -1 if the drive is valid, or zero if it is not DEFINT A-Z DECLARE SUB DosQCurDisk(SEG Temp, SEG DriveMap&) FUNCTION Valid%(Drive$) Call DosQCurDisk(Temp, DriveMap&) Drive = ASC(Drive$) - ASC("A") IF DriveMap& AND 2 ^ Drive THEN 'Check the appropriate bit in the mask Valid% = -1 ELSE Valid% = 0 END IF END FUNCTION Figure 5 '***** SetDrive.Bas - BASIC subprogram that sets the current drive 'syntax: CALL SetDrive(Drive$) ' where: Drive$ is specified as "A", "B", and so forth DEFINT A-Z DECLARE FUNCTION DosSelectDisk%(BYVAL Drive) SUB SetDrive(Drive$) Drive = ASC(UCASE$(Drive$)) - ASC("A") + 1 'now "A" = 1, "B" = 2, etc. ErrorCode = DosSelectDisk%(Drive) END SUB Reading File Names One feature that nearly every application must provide is the ability to obtain a list of file names from disk, perhaps before presenting them in a menu. The approach taken here is to create two routines--the first is a function that returns a count of the files that match a given search specification, and the second is a subprogram that fills a string array with all of their matching filenames. It is important to know how many matching files exist ahead of time so that the array may be sufficiently dimensioned. The FileCount function shown in Figure 6 accepts a file specification such as *.BAS or C:\SUBDIR\*.* and returns the number of files that match. Once the string array has been dimensioned, the same file specification is passed to ReadNames (Figure 7), along with the array that will be filled with each name. Notice that by specifying a complete name, FileCount may also be used merely to determine whether a file exists. For example, if FileCount returns 1 for \XYZ.DAT, you know such a file exists. Both of these routines--FileCount and ReadNames--call upon the DosFindFirst and DosFindNext OS/2 services to do the real work. Not unlike their DOS counterparts, these services accept a file name or specification and begin or continue a directory search, respectively. Once a matching file has been located, an area of memory that the calling program supplies is filled with information about the file. When the OS/2 operating system fills the buffer with the file's information, it includes much more than just the file's name. The date, time, size, and attribute are also included, and these may be accessed by constructing a suitable TYPE variable. A TYPE definition that organizes all of the returned information is shown in Figure 8. Notice that with FAT-based disks, the Create and Access portions of the buffer are always returned holding 0. Besides an incoming file specification and buffer address, DosFindFirst also requires some additional items. One is a search handle, which facilitates subsequent searches. To initiate a new search, this handle will be assigned to -1 before calling DosFindFirst. DosFindFirst then modifies this variable by assigning a handle that will be used for all subsequent searches. When DosFindNext reports that there are no more matching files by returning a nonzero value in AX, the DosFindClose function is called to release the handle. The calling program may also specify a search attribute that will include or exclude certain types of files. A file search may, for example, be expanded to include hidden, read-only, or system files or subdirectories. These attributes are combined by OS/2, thus returning files that match one or more of the search attributes. That is, if you ask for read-only files, you will get normal files as well. Therefore, if you are searching for, say, directory names, you must inspect each file's attribute that is returned to be sure it really is a directory. A list of bit settings for each of the possible file attributes is shown in Figure 9. To search for directory names only, you would assign the attribute variable to 10000b, which is 16 decimal. Then you must also inspect the attribute portion of the buffer for that same value. If it matches, the count would be incremented (in the case of FileCount) or the name would be transferred to the string array (in the case of ReadNames). In addition to providing a buffer for the returned information, the calling program also tells DosFindFirst how large the buffer is. The minimum length is 36 bytes, which is sufficient to hold information about a single file. But you can also ask DosFindFirst to process several files at once. You could, for example, create a buffer of 360 bytes and then assign MaxCount to 10. OS/2 will then return up to 10 matching files at once. Unfortunately, the information that is returned is not truly record-oriented. Instead of padding each file name to the same length, information about the next file immediately follows the end of the previous file. This is unfortunate, since it precludes loading multiple elements into a BASIC TYPE array. Once the call to DosFindFirst has been set up and the first file has been found, then subsequent calls to DosFindNext are handled in a simple DO loop. In the FileCount function, for each matching file that is found the Count variable is incremented. If an error occurs within the loop, you know that there are no more files and you should exit the loop. The same logic is used in the ReadNames routine to know when it has read the last available file name. Notice in ReadNames the devious method that is used to obtain the length of each file name. Because BASIC does not provide a "byte-sized" variable type, the FileFindBuf TYPE variable must instead use a string with a length of 1. Then the ASCII value of the string will tell how long the name is. Because the file names are terminated with a 0 byte, INSTR could also be used to locate that byte and determine the length. But the method used here is more to the point and requires less code to implement. As you can see, FileCount and ReadNames are nearly identical, differing only in what they do with the results of the OS/2 system calls. FileCount tracks the number of matches found, and ReadNames copies each name from the buffer into an array. But because FileCount doesn't use any of the information about the files that is returned, a single string is used for the file information buffer. ReadNames, however, needs access to the name and length portion of the buffer, so a TYPE is defined that allows ready access to just those portions. A complete demonstration program is shown in Figure 10 using each of the routines discussed here in context. It begins by asking for a file specification and then obtains a count of the matching files. The count is used to dimension a string array; then ReadNames is called to read them. Following that, GetDrive returns the current drive, which is saved. The program then prompts for a new drive to which to change. This drive is again retrieved just to prove that it worked. Finally, the original drive is restored. Assembly Language Calls Now that you have an understanding of how the OS/2 services are called, you can look at the steps required to translate them into assembly language. As I mentioned, routines that are coded in assembler will require much less code space than an equivalent BASIC subprogram or function. All of the examples presented here are designed to be assembled with Microsoft Macro Assembler (MASM) Version 5.1 or later. I'll begin with the GetDir routine, which is shown in Figure 11. Like its BASIC counterpart, GetDir has been designed as a string function. The Mixed Languages Programming Guide that comes with MASM Version 5.0 and later provides a brief discussion of creating integer and long integer functions. Unfortunately, it does not describe how to create a string function; the technique is, however, quite easy. Any assembler routine can return a string by first creating a string descriptor in BASIC's default data segment and then loading AX with the address of the descriptor before returning to BASIC. Of course, the data that is pointed to by the descriptor must also be in BASIC's data segment. The first two items in the GetDir.Asm source listing are, therefore, a length word followed by an address word. The string data itself immediately follows the descriptor, although it could be located anywhere in near memory. The Length portion of the descriptor is left uninitialized, but the address part is assigned to point to the directory name area. Because OS/2 does not return a leading backslash as part of a directory name, this is included within the name data. GetDir.Asm is designed to accept either an uppercase or lowercase drive letter or a null string to indicate the default drive. The first step is to load the address of the Drive$ descriptor into DI and move its length into DX. If after ORing DX against itself the result is 0, you know a null string was sent and the default drive should be used. If the length of Drive$ is not 0, the first character is placed into DL and then capitalized if necessary. The final step is to subtract 64 from the ASCII value to convert A to 1, B to 2, and so forth. For each variable expected by the DosQCurDir service, you must obtain and pass either a segmented address or the value of the parameter. For example, the first parameter is the drive number in DX, which is passed by value. But in the case of the buffer that receives the directory name, its segment (DS) must first be pushed, followed by its address, which was loaded into DI earlier. Likewise, the LenName variable is a local stack variable, so the SS (Stack Segment) register rather than DS is pushed. BASIC 6.0 programs currently use the same segment for DS and the stack. This is likely to change in the future, however, so it is wise to push the correct segment. In order to get the address for any local variables, you must use the Lea (Load Effective Address) instruction instead of Offset. Whereas Mov Offset is the most efficient way to obtain the address of a normal assembler variable, the address of a local variable must be computed at run time. Internally, MASM replaces your references to a local variable with an expression such as [BP-2] or [BP-14]. But an instruction such as Mov AX,Offset [BP-2] is obviously not legal. Therefore, Lea must be used to load the effective address into a register (in this case CX), then that register may be pushed. Once DosQCurDir has been called, it will return the status in the AX register. Note that, Or AX,AX is slightly faster and smaller than Cmp AX,0. If the result is nonzero, you can assume that either an invalid drive was given or perhaps the drive door was open. In either case, the only reasonable action is to exit, leaving a null string to indicate the error. If there was no error, a short loop is used to increment the length portion of the descriptor while searching for the 0 byte that marks the end of the directory name. Once that is done, the address of the descriptor is placed into AX, where BASIC expects it. In order to simplify the amount of coding and show an integer function in context, the GetDrive routine has been designed to return an ASCII value that corresponds to the current drive. It will therefore be declared as an integer function with no incoming arguments. This is shown in Figure 12. First, DosQCurDisk is called to place the drive into the local DriveNum variable, which is then moved into AX. After adding 64 to convert it to the equivalent ASCII drive letter, it is left in AX for the function's output. The SetDrive routine in Figure 13 is equally simple to understand, so I won't belabor its operation. The assembly language routines that count and read file names are shown in Figures 14 and 15, respectively. Like their BASIC counterparts, they are similar in operation, except that FileCount merely counts the matches and ReadNames transfers each name to the appropriate string array element. Although an assembly language function can return a single string by leaving a descriptor address in AX, it cannot actually allocate string space or assign strings to a new length. This means that before ReadNames can be used, the calling program must assign each string to a length of at least 12. The demonstration program uses SPACE$(12) in a FOR/NEXT loop to accomplish this. To prevent a possible crash, ReadNames examines the length portion of each string descriptor and exits if the string isn't at least 12 characters in length. Notice that to simplify these routines and keep them to the point, it is up to your program to append the required 0 byte to the end of the file specification string. Summing Up As you have seen, accessing OS/2 system services directly can allow your programs to include features that simply would not be possible using BASIC commands alone. Nearly all of the available OS/2 services are called in a manner similar to the ones covered here. Of course, BASIC offers nearly all of the file and video capabilities that most programs need as part of the language. But in many cases, calling the operating system directly can result in either an improvement in performance or an easy way to avoid disk errors without having to resort to the use of ON ERROR. I hope that this brief introduction will encourage you to experiment on your own and will expand your understanding of what OS/2 is all about. As the OS/2 environment becomes more prevalent, there is no reason that programmers using BASIC should miss out on all the fun. Figure 6 '***** FCount.Bas - contains BASIC function returning number of ' matching file names 'syntax: Count = FileCount%(FileSpec$) ' where: FileSpec$ is in the form "*.BAS" or "A:\subdir\*.*", ' and so forth ' note: You may also specify a complete file name to see if ' it exists DEFINT A-Z TYPE FileFindBuf 'this receives the date, time, 'size, etc. MiscInfo AS STRING * 36 END TYPE DECLARE FUNCTION _ DosFindFirst%(BYVAL SpecSeg, BYVAL SpecAdr, _ SEG Handle, BYVAL Attrib, SEG Buffer _ AS FileFindBuf, BYVAL BufLen, SEG MaxCount, _ BYVAL Reserved&) DECLARE FUNCTION DosFindNext%(BYVAL Handle, SEG Buffer AS _ FileFindBuf, BYVAL BufLen, SEG _ MaxCount) DECLARE SUB DosFindClose(BYVAL Handle) FUNCTION FileCount%(FileSpec$) FileCount% = 0 'default to no matching file 'names FSpec$ = FileSpec$ + CHR$(0) 'create an ASCIIZ string from 'the file spec Handle = -1 'request a new search handle Attrib = 39 'matches all file types except 'subdirectory DIM Buffer AS FileFindBuf 'create a buffer to hold the 'returned info BufLen = 36 'tells OS/2 how big the buffer is MaxCount = 1 'get just one file Reserved& = 0 'this is needed, but not used 'call DosFindFirst ErrorCode = DosFindFirst%(VARSEG(FSpec$), SADD(FSpec$), Handle, _ Attrib, Buffer, BufLen, MaxCount, Reserved&) IF ErrorCode THEN EXIT FUNCTION 'no matching files or another 'error, exit Count = 0 'we got one, increment at next 'step DO Count = Count + 1 'we got another one LOOP WHILE (DosFindNext%(Handle, Buffer, BufLen, MaxCount) = 0) DosFindClose%(Handle) 'close the file handle FileCount% = Count 'assign the function output END FUNCTION Figure 7 '***** ReadName.Bas - contains BASIC function returning an array of 'file names 'syntax: Count = FileCount%(FileSpec$) ' DIM Array$(Count) ' CALL ReadNames(FileSpec$, Array$()) ' where: FileSpec$ is in the form "*.BAS" or "A:\subdir\*.*", and so ' forth, and Array$() is filled in with each file's name DEFINT A-Z TYPE FileFindBuf MiscInfo AS STRING * 22 'Receives date, time, size, etc. FileLen AS STRING * 1 'Receives the length of the name FileName AS STRING * 13 'Receives the name END TYPE DECLARE FUNCTION _ DosFindFirst%(BYVAL SpecSeg, BYVAL SpecAdr, _ SEG Handle, BYVAL Attrib, SEG Buffer _ AS FileFindBuf, BYVAL BufLen, SEG MaxCount, _ BYVAL Reserved&) DECLARE FUNCTION DosFindNext%(BYVAL Handle, SEG Buffer AS _ FileFindBuf, BYVAL BufLen, _ SEG MaxCount) DECLARE SUB DosFindClose(BYVAL Handle) SUB ReadNames(FileSpec$, Array$()) FSpec$ = FileSpec$ + CHR$(0) 'create an ASCIIZ string from 'the file spec Handle = -1 'request a new search handle Attrib = 39 'matches all file types except 'subdirectory DIM Buffer AS FileFindBuf 'create a buffer to hold the 'returned info BufLen = 36 'tells OS/2 how big the buffer 'is MaxCount = 1 'get just one file Reserved& = 0 'this is needed, but not used 'call DosFindFirst ErrorCode = DosFindFirst%(VARSEG(FSpec$), SADD(FSpec$), Handle, _ Attrib, Buffer, BufLen, MaxCount, Reserved&) IF ErrorCode THEN EXIT SUB 'no matching files, exit Count = 0 'initialize counter DO Count = Count + 1 'we got another one Length = ASC(Buffer.FileLen) 'get the name's length Array$(Count) = LEFT$(Buffer.FileName, Length) 'assign the name LOOP WHILE (DosFindNext%(Handle, Buffer, BufLen, MaxCount) = 0) DosFindClose(Handle) 'close the handle END SUB Figure 8 TYPE FileBuffer CreateDate AS INTEGER 'date file was originally created CreateTime AS INTEGER 'time file was originally created AccessDate AS INTEGER 'date of last file access AccessTime AS INTEGER 'time of last file access WriteDate AS INTEGER 'date file was last written to WriteTime AS INTEGER 'time file was last written to FileSize AS LONG 'the size of the file in bytes Allocation AS LONG ' memory allocation for the file in bytes Attribute AS INTEGER 'the file's attribute NameLength AS STRING * 1 'the length of the file's name FileName AS STRING * 13 'the file's name, terminated with 'a CHR$(0) END TYPE Figure 10 '***** DEMO.BAS - demonstrates the BASIC and assembler OS/2 routines ' 'This routine can call either assembly language or BASIC modules. 'Include the statements marked "*** BASIC only" if using BASIC 'modules,and those marked "*** Assembly only" if using assembly 'language routines. DEFINT A-Z DECLARE FUNCTION FileCount%(FileSpec$) DECLARE FUNCTION GetDir$(Drive$) DECLARE FUNCTION GetDrive%() DECLARE SUB ReadNames(Spec$, Array$())` '*** BASIC only DECLARE SUB ReadNames(Spec$, BYVAL Element) '*** Assembly only DECLARE SUB SetDrive(Drive$) INPUT "Enter a file specification or press Enter for *.*: ", Spec$ IF (Spec$ = "") THEN 'Pressed Enter, use '*.* Spec$ = "*.*" ENDIF Spec$ = Spec$ + CHR$(0) '*** Assembly only '-- append NULL Count = FileCount%(Spec$) 'count the number of matching files IF (Count > 0) THEN DIM Array$(1 TO Count) 'make an array to 'hold them FOR X = 1 TO Count '*** Assembly only Array$(X) = SPACE$(12) 'Make array 12 characters long NEXT ReadNames Spec$, Array$() '*** BASIC only ReadNames Spec$, VARPTR(Array$(1)) '*** Assembly only FOR X = 1 TO Count 'print the 'filenames PRINT Array$(X), NEXT ELSE PRINT "No files found" 'No matching files ENDIF SaveDrive$ = CHR$(GetDrive%) 'save the current 'drive PRINT PRINT "The current drive is " SaveDrive$ 'print it PRINT "and its directory is " GetDir$(SaveDrive$) 'print current 'directory INPUT "Enter a new drive to move to: ", NewDrive$ 'prompt for a new 'drive SetDrive NewDrive$ 'set it PRINT "The current drive is: " CHR$(GetDrive%) 'print the new 'drive PRINT "Moving back to "SaveDrive$ 'inform user SetDrive SaveDrive$ 'restore original 'drive Figure 11 ;----- GetDir.Asm - retrieves current directory ;Syntax - ; ; Drive$ = "A" ; Dir$ = GetDir$(Drive$) ; PRINT "Current directory is: " Dir$ ; ; Where Drive$ = "" for the default drive, or "A" or "a" or "B", etc. ; and Dir$ receives the current directory path. If an error occurs, ; Dir$ will be assigned to a null string. .286 .Model Medium, Basic .Data DescrLen DW ? DescrAdr DW Offset DirName DirName DB "\", 64 Dup (?) .Code Extrn DosQCurDir:Proc ;declare the OS/2 call as external GetDir Proc, Drive:Ptr Local LenName:Word ;create a local variable Cld ;clear direction flag so ;string moves are forward Mov DescrLen, 0 ;assume a null string in case ;there's an error Mov LenName,65 ;tells OS/2 the available buffer length Mov DI,Drive ;put address of Drive$ into DI Mov DX,[DI] ;assume they want the default ;drive for now Or DX,DX ;check LEN(Drive$) Je Do_It ;it's null, leave DX holding 0 ;and skip ahead Mov DI,[DI+02] ;put address of first character ;in Drive$ into DI Mov DL,[DI] ;put ASC(Drive$) into DL Cmp DL,"a" ;is it less than "a"? Jb Continue ;yes, skip Sub DL,32 ;no, convert to upper case Continue: Sub DL,64 ;"A" now equals 1, "B" = 2, etc. Do_It: Mov DI,Offset DirName ;get address for the string descriptor Inc DI ;bump past the leading backslash Mov SI,DI ;save it in SI too for later Push DX ;pass the drive number Push DS ;show which segment receives the name Push DI ;and which address Push SS ;pass segment for LenName ;(SS because it's local) Lea CX,LenName ;get address for LenName Push CX ;and pass that too Call DosQCurDir ;ask OS/2 to do the dirty work Or AX,AX ;was there an error? Jnz Exit ;yes, get out with DescrLen ;showing a null string AddLen: ;scan for zero byte that marks the end Inc DescrLen ;show the string as being one longer Lodsb ;get a character from directory string Or AL,AL ;are we at the end of directory name? Jnz AddLen ;no, keep searching Exit: Mov AX,Offset DescrLen ;tell BASIC where to find the descriptor Ret ;return to BASIC GetDir Endp End Figure 12 ;----- GetDrive.Asm - retrieves the default drive ;Syntax - ; ; PRINT "The current drive is "; CHR$(GetDrive%) .286 .Model Medium, BASIC .Code Extrn DosQCurDisk:Proc ;declare the OS/2 call as external GetDrive Proc Local DriveNum:Word ;drive number Local DriveMap:DWord ;logical drive map (not used here) Push SS ;push segment for DriveNumber Lea AX,DriveNum ;get address Push AX ;and push that Push SS ;same for DriveMap segment Lea AX,DriveMap ;and address Push AX Call DosQCurDisk ;call OS/2 Mov AX,DriveNum ;load AX with the returned drive number Add AX,64 ;now 1 = "A", 2 = "B", and so forth Ret ;return to BASIC with the function ;output in AX GetDrive Endp End Figure 13 ;----- SetDrive.Asm - sets the default drive ;Syntax - ; ; CALL SetDrive(Drive$) ; where Drive$ = "A" or "a" or "B", etc. .286 .Model Medium, Basic .Code Extrn DosSelectDisk:Proc ;declare the OS/2 call as external SetDrive Proc, Drive:Ptr Mov SI,Drive ;put Drive$ descriptor address into SI Cmp Word Ptr [SI],0 ;is Drive$ null? Jz Exit ;yes, ignore the request Mov SI,[SI+02] ;put address of Drive$ data in SI Mov DL,[SI] ;put ASC(Drive$) into DL Cmp DL,'a' ;is it below "a"? Jb Continue ;yes, skip Sub DL,32 ;no, convert to upper case Continue: Sub DL,64 ;"A" now equals 1, "B" = 2, etc. Xor DH,DH ;clear DH so we can use all of DX Push DX ;push drive number Call DosSelectDisk ;call OS/2 Exit: Ret ;return to BASIC SetDrive Endp End Figure 14 ; FCount.Asm - counts the number of files that match a specification ;Syntax - ; ; Spec$ = "*.*" + CHR$(0) ; Count = FileCount%(Spec$) ; Where Spec$ holds the file specification, and Count is assigned the ; number of files that match. .286 .Model Medium, Basic .Code Extrn DosFindFirst:Proc ;Declare the OS/2 calls as external Extrn DosFindNext: Proc Extrn DosFindClose:Proc FileCount Proc, Spec:Ptr Local Handle: Word Local Buffer [18]: Word ;36 bytes Local SearchCount: Word Xor BX,BX ;zero the count accumulator Mov SI,Spec ;SI holds address of Spec$ descriptor Mov DX,[SI+02] ;now DX points to the first character Mov Handle, -1 ;directory handle of -1 to initiate ;search Mov SearchCount,1 ;number of entries to find Push DS ;push segment of Spec$ Push DX ;push offset of Spec$ Push SS ;push segment of local Handle Lea AX,Handle ;get Handle's address Push AX ;push it too Push 00100111b ;push attribute - all file types ;(except directory) Push SS ;segment of buffer Lea AX,Buffer [18] ;get address of buffer Push AX ;push it Push 36 ;push length of buffer Push SS ;push segment of SearchCount Lea AX,SearchCount ;get address of SearchCount Push AX ;push it Push 0 ;push a DWord of 0's (Reserved& in ;BASIC version) Push 0 Call DosFindFirst ;make call to find first occurrence of ;file Or AX,AX ;was there a file by that name? Jnz Exit ;if AX is not zero, then no file was ;found Next_File: Inc BX ;show that we got another matching ;file Push Handle ;push handle Push SS ;push segment of buffer Lea AX,Buffer [18] ;get address of buffer Push AX ;push it Push 36 ;push length of buffer Push SS ;push segment of SearchCount Lea AX,SearchCount ;get address of SearchCount Push AX ;push it Call DosFindNext ;make call to find first occurrence of ;file Or AX,AX ;was there a file by that name? Jz Next_File ;if not carry, keep looking Done: Push Handle ;push handle Call DosFindClose ;close it Exit: Mov AX,BX ;assign the function output Ret ;return to BASIC FileCount Endp End Figure 15 ; ReadName.Asm - reads a list of file names into a BASIC string array ;Syntax - ; ; Spec$ = "*.*" + CHR$(0) ; CALL ReadFile(Spec$, BYVAL VARPTR(FileName$(0))) ; ; Where Spec$ holds the directory specification, and elements in the ; FileName$() array receive the directory entries. .286 .Model Medium, Basic .Data FileJunk DB 22 Dup (?) NameLen DB ? FileName DB 13 Dup (?) .Code Extrn DosFindFirst: Proc ;declare the OS/2 calls as external Extrn DosFindNext: Proc Extrn DosFindClose: Proc ReadNames Proc, Spec:Ptr, Array:Ptr Local Handle: Word Local MaxCount: Word Mov SI,Spec ;SI holds address of Spec$ descriptor Mov SI,[SI+02] ;now SI points to the first character Mov BX,Array ;get address of FileName$(1) for later Mov Handle, -1 ;directory handle of -1 to initiate ;search Mov MaxCount,1 ;maximum number of entries to find Push DS ;push segment of Spec$ Push SI ;push offset of Spec$ data Push SS ;push segment of Handle in local ;storage Lea AX,Handle ;get address of Handle Push AX ;push it Push 00100111b ;the attribute for any type of file Push DS ;segment for the buffer Push Offset FileJunk ;push buffer address Push 36 ;push length of buffer Push SS ;push segment of MaxCount Lea AX,MaxCount ;get address of MaxCount Push AX ;push it Push 0 ;push a DWord of 0's Push 0 Call DosFindFirst ;make call to find first matching file ;name Or AX,AX ;was there a file by that name? Jnz Exit ;no, exit Push DS ;ensure that ES points to BASIC's ;string Pop ES ;space for Movsb instructions below NextFile: Cmp Word Ptr [BX],12 ;is the string at least 12 characters ;long? Jb Done ;no, so get out now Mov DI,[BX+02] ;DI holds address of first character in ;FileName$() Lea SI,FileName ;get address of the name portion of ;buffer Mov CL,NameLen ;load CL with the number of characters to ;transfer Xor CH,CH ;clear CH so we can use all of CX Rep Movsb ;copy the string Add BX,4 ;point BX to next array element for later Push SS:Handle ;push handle Push DS ;push segment of buffer Push Offset FileJunk ;and buffer address Push 36 ;push length of result buffer Push SS ;push segment of MaxCount Lea AX,MaxCount ;get address of MaxCount Push AX ;push it Call DosFindNext ;make call to find next matching file name Or AX,AX ;did we get another one? Jz NextFile ;if so, keep looking Done: Push SS:Handle ;push the handle number Call DosFindClose ;free it up Exit: Ret ;return to BASIC ReadNames Endp End Figure 16 ; ReadName.Asm - reads a list of file names into a BASIC string array ;Syntax - ; ; Spec$ = "*.*" + CHR$(0) ; CALL ReadFile(Spec$, BYVAL VARPTR(FileName$(0))) ; ; Where Spec$ holds the directory specification, and elements in the ; FileName$() array receive the directory entries. .286 .Model Medium, Basic .Data FileJunk DB 22 Dup (?) NameLen DB ? FileName DB 13 Dup (?) .Code Extrn DosFindFirst: Proc ;declare the OS/2 calls as external Extrn DosFindNext: Proc Extrn DosFindClose: Proc ReadNames Proc, Spec:Ptr, Array:Ptr Local Handle: Word Local MaxCount: Word Mov SI,Spec ;SI holds address of Spec$ descriptor Mov SI,[SI+02] ;now SI points to the first character Mov BX,Array ;get address of FileName$(1) for later Mov Handle, -1 ;directory handle of -1 to initiate ;search Mov MaxCount,1 ;maximum number of entries to find Push DS ;push segment of Spec$ Push SI ;push offset of Spec$ data Push SS ;push segment of Handle in local ;storage Lea AX,Handle ;get address of Handle Push AX ;push it Push 00100111b ;the attribute for any type of file Push DS ;segment for the buffer Push Offset FileJunk ;push buffer address Push 36 ;push length of buffer Push SS ;push segment of MaxCount Lea AX,MaxCount ;get address of MaxCount Push AX ;push it Push 0 ;push a DWord of 0's Push 0 Call DosFindFirst ;make call to find first matching file ;name Or AX,AX ;was there a file by that name? Jnz Exit ;no, exit Push DS ;ensure that ES points to BASIC's ;string Pop ES ;space for Movsb instructions below NextFile: Cmp Word Ptr [BX],12 ;is the string at least 12 characters ;long? Jb Done ;no, so get out now Mov DI,[BX+02] ;DI holds address of first character in ;FileName$() Lea SI,FileName ;get address of the name portion of ;buffer Mov CL,NameLen ;load CL with the number of characters to ;transfer Xor CH,CH ;clear CH so we can use all of CX Rep Movsb ;copy the string Add BX,4 ;point BX to next array element for later Push SS:Handle ;push handle Push DS ;push segment of buffer Push Offset FileJunk ;and buffer address Push 36 ;push length of result buffer Push SS ;push segment of MaxCount Lea AX,MaxCount ;get address of MaxCount Push AX ;push it Call DosFindNext ;make call to find next matching file name Or AX,AX ;did we get another one? Jz NextFile ;if so, keep looking Done: Push SS:Handle ;push the handle number Call DosFindClose ;free it up Exit: Ret ;return to BASIC ReadNames Endp End Pointers 101: Understanding and Using Pointers in the C Language Greg Comeau Pointers are responsible for a good deal of the power in the C language. They are often the only way to express a calculation and are used to produce more compact and efficient code. On the other hand, pointers account for most of the problems that occur while learning C, as well as most of the run-time errors. In addition, most debugging sessions are spent deciphering code involving them. It is safe to say that most programmers have never completely understood pointers. The terminology is confusing, and most of the explanatory material found in the literature is, at best, incomplete. Hence, many programmers develop an inconsistent and often incorrect understanding of them. My own experience with pointers certainly proves this. Time and again I've said that I've had to learn C twice; in reality, it's more like three times. Regrettably, that means I've lost a lot of time writing incorrect and poorly constructed programs. This article and the following ones in this series will discuss some of the more pressing issues involving pointers. The discussion will begin at the simplest level, but one thing it won't do is cover the kind of pointer basics most texts provide. As we turn our attention to some "heavy" details, we will gain a solid grasp of fundamental issues, as well as cover topics that still cause raging debates even among experts. Let's start by looking at some of the definitions involved with pointer usage. Identifiers and Variables Many computer languages use the terms variable and identifier interchangeably. While this practice is often followed when we describe our C programs, it is incorrect to make such a generalization with C. An identifier is a symbolic name given to an entity such as a label, a function, a union, or some other object. An identifier provides both the programmer and the compiler with an easy way of manipulating a given entity. Variables, on the other hand, are unnamed objects that are locations in storage. These objects might be absolute memory references, for example, or a manipulation of data returned from routines such as malloc. Programmers manipulate these variables by using pointers. Be aware that identifiers, like unnamed objects, are symbolic values. Therefore a variable can be an identifier and an identifier can be a variable. Both variables and identifiers usually have storage allocated to them. While identifiers are names we give to specific memory locations, there are also memory locations that we don't give names to. That doesn't mean that they don't exist--they do, and they are as real and important as any other memory that a program uses. For instance, most of you have at least seen code that uses a far pointer to poke into screen memory; or, in some cases, the compiler may generate code that makes use of temporary stack locations that you know nothing about. All these are instances of unnamed variables. Constants Identifiers refer not only to variables, but to such things as labels or function names. One somewhat obtuse issue that is directly related to references of these types involves the const keyword. Const is a term borrowed from C++, though it has a slightly different meaning in C: in C const can be reasonably defined as meaning "read only." This new qualifier was added to the C language in the ANSI draft proposal. The const keyword underscores this discussion. A variable is simply something that changes; it can be read from and written to. That's why it's a variable--it is able to change values. A constant does not change; it can only be read from. For instance, let's say that you have an external declaration such as int someextern = 5; whose value you do not want to change through the life of your program. Unfortunately, the programmer is responsible for enforcing that the value of someextern never changes. This is error-prone and in some cases inefficient since you're only looking for a read-only identifier. Draft ANSI C allows you to specify that an identifier serve as a constant rather than a variable with the const keyword qualifier. As a simple example you would code const int someextern = 5; to ensure that the compiler enforces the criteria outlined above. Once you have coded someextern as a constant, it becomes a syntax error to assign a different value to someextern later in the program. For other examples consider the declarations in Figure 1. Anything labeled as a constant cannot be assigned to. Considering this, it must be agreed that an identifier such as ci and cpci can be given a const attribute. Also, any unnamed memory location that you've pointed to, such as *pci and *cpci, should be able to inherit such a quality as well. Remember constants are in direct contrast to variables. For more information on this point, see "A Guide to Understanding Even the Most Complex C Declarations," MSJ (Vol. 3, No. 5). Figure 1 const int ci; /* ci is a constant int */ const int *pci; /* pci is a pointer to a constant int */ int * const cpi; /* cpi is a constant pointer to an int */ const int * const cpci; /* cpi is a constant pointer to a constant int */ Objects An object is defined as "a manipulatable region of storage" in the first edition of The C Programming Language (referred to as K&R), Brian W. Kernighan and Dennis M. Ritchie (Prentice Hall, 1978), p. 183. In the second edition (Prentice Hall, 1988), p. 197, this definition changed somewhat to "an object is a named region of storage," which seems to be an ambiguous pun on terminology. I say that because names seem to have nothing to do with objects alone. As I noted, unnamed objects can and do exist. I suspect the definition in the second edition was intended to imply that if one can pinpoint some object in memory, then it's been named (since it can be accessed); such pinpointing, however, is akin to children being able to find their way home even though they may not know the exact street and house number. To a child, home is just a place--it doesn't necessarily require a label (or address) to be identified. In this sense, the house is similar to an unnamed object. It is accessible, yet we do not need to know where it is or be able to describe it in all its contexts. Therefore, even though we can't label every object--say, an integer at memory location 8--with a precise identifier name, we can name an object "by location" (the 8) or by means of a pointer. I admit that this definition is perhaps a bit on the esoteric side. The definition in The ANSI C December 7, 1988 Draft (X3 Secretariat, Computer and Business Equipment Manufacturers Association), on p. 3, section 1.6, says "an object is a region of data storage in the execution environment, the contents of which can represent values. Except for bitfields, objects are composed of contiguous sequences of one or more bytes, the number, order, and encoding of which are either explicitly specified or implementation-defined. When referenced, an object may be interpreted as having a particular type." This definition is comparable to the first edition of K&R, except that the contents of an object can represent values. Note that every object is simply some location and area of storage whose type is of no concern until it is accessed. The type of an object is determined by the lvalue that refers to the object. A union is the perfect example--when an expression references a member of a union, the region of storage is interpreted as if it had the type of the member. Lvalues We've all been faced at one time or another with the humiliating and apparently meaningless "lvalue required" error message. Most of us think that an lvalue's definition is something along the lines of "an lvalue goes on the left-hand side of an equal sign." Certainly, it has to deal with an equal sign in part, but let's investigate further, since it turns out that C has its own view of this. First, let's get straight what an lvalue isn't. An lvalue is not a constant. Nor is it an rvalue (a translation does occur, but more on that below). Nor does it need to be an identifier--and not all identifiers need to be lvalues. One possible definition of an lvalue might be "an expression referring to the location of an object." See Figure 2 for an elaboration of this definition and keep it in mind as we proceed. Figure 2 indicates that an lvalue can be an identifier. The next possible lvalue is an expression that is allowed to have indirection applied to it. Most of you are familiar with a situation like that shown in Figure 3a. You should be able to recognize that the *p here is indeed an lvalue. Unfortunately, for many C programmers this is the end of comprehending lvalues involving dereferences. The line in Figure 3b is really no different from the one in Figure 3a, although some of you may frown upon the fact, or not be aware, that such a possibility exists. Structure names are next on the list. Note that we are not talking about a structure tag here, but about the actual name of a structure. A structure name must be an lvalue and there are several reasons why. First, structure assignments are allowed to occur. Second, since a struct.member construct also refers to an object, the struct part itself must be one. Therefore, the constraint here is that the struct must be an lvalue, which is a reasonable requirement because the member is really a part of a bigger object that is locatable through the struct lvalue. A common derivation of structure usage is by means of pointers, as in the ptr->member syntax. This syntax has the same constraint as above. That makes sense, since such syntax is translatable and equivalent to (*ptr).member syntax, which once again is another valid lvalue. Note that the definitions involving structures hold true for unions as well. The last lvalue category relates to arrays and is typically a major source of confusion among both new and experienced C programmers. We will consider this category below; for now, let's just say that an array[element] is analogous to an identifier or variable, and I am sure you agree that it is an lvalue as well. If we apply the fact that a[i] is translatable into *(a + i), I think it's fairly obvious that some of the information earlier in this section ties together rather well. Also, since the translation above actually transforms the array into a pointer expression, then *(ptr + i) and ptr[i] are both valid lvalues as well. The use of & in constructs such as &array[i] always yields an rvalue. Figure 2 An lvalue can be: an identifier an expression using indirection a structure name structure.member ptr_to_structure->member (*ptr_to_structure).member array[element] *(array + index) or *(pointer + index) An lvalue is not: a function name an array name enumerator names a function call a result of an assignment a result of a cast Operators requiring lvalue as operands: &, + +, - -, ., left of = Operators that do not produce lvalues: + +, - -, &, (cast), = (there are others, such as + or -, but I think their nonparticipation here is obvious) Operators that can produce lvalues: ., ->, *, [ ] Figure 3 3a char far *p = (char far *)0xB0000000; *p = 'a'; 3b *(char far *)0xB0000000 = 'a'; Rvalues The use of the term lvalue in C has been borrowed directly from compiler terminology, along with the term rvalue. Consider the definitions in Compilers, Principles, Techniques, and Tools, Alfred V. Aho, Ravi Sethi, and Jeffrey D. Ullman (Addison-Wesley, 1986) p. 65: "The terms l-value and r-value refer to values that are appropriate on the left and right sides of an assignment, respectively. That is, r-values are what we usually think of as 'values,' while l-values are locations." This is the source of the equal sign idea you often hear about when texts attempt to define an lvalue. The authors do provide a better description on p. 424, "the term lvalue refers to the storage represented by an expression and an rvalue refers to the value contained in that storage." Simply, that means that an rvalue is the result obtained from a partial or full expression. In C this definition is modified slightly to include constants and the other entries in Figure 2 that are not lvalues. Also, note that when an lvalue is used in a context where its value is being taken, it is translatable into an rvalue. In other words, we know that the comments in the lines of code shown in Figure 4 are true. Think, though, about what happens when we see something like: i = j; c = *p; Figure 4 i = 0; /* i is the lvalue, 0 is the rvalue */ p = malloc(somenumber); /* p is lvalue, rc from malloc is rvalue */ If we view all of i, j, c, and *p as individual entities in and of themselves, they all are clearly lvalues. However, the contextual situation of j and *p alert us (and the compiler) that, once the object we are interested in has been located, we will be interested solely in the value of that object, and the lvalue therefore must become an rvalue. That is, we need to take the rvalue of the lvalue. This is an important piece of information that is missing from such documents as K&R and is buried in a footnote on p. 37 in the ANSI draft. By the way, unnamed variables are not rvalues in and of themselves. To repeat, the existence of an object determines its lvalue and rvalue, not a quality such as whether that object has a "real" name that we can use. Lvalues--A Second Look Now that rvalues are out of the way, let's return to the definition of lvalue presented in Figure 2. We can build upon the definition from Compilers: Principles, Techniques, and Tools and say that an lvalue is an expression that refers to an address. This reference is really to some object in storage. Simplifying, we see that an lvalue refers to an object and is therefore an "object locator." Thus, basically any variable reference is an lvalue. There's no trick to it. Modifiable vs. Unmodifiable Lvalues Now it's time to get to some of the more confusing issues. Both K&R and the ANSI draft fail to provide enough information. Instead of encapsulating the discussion about lvalues, either they don't mention it at all (which is almost the case with K&R), or one is forced to play an almost "goto: See goto" game in what seems to be a mistaken attempt to avoid redundancy. What this section really comes down to is the question of whether or not array names are lvalues. C purists, of which I am one, would argue that they are not. We purists feel that because an array name is usually synonymous with its address or implicitly translatable into a pointer, there shouldn't be any discussion about this question. In the former case, an address is really a constant, which is really an rvalue, and the latter case should represent an rvalue as well, since there is an implicit cast. Because of the terseness of C and the definitions presented above, some confusion exists that ANSI (not K&R) felt was necessary to consider and correct. They believe that the number of C programmers confused over the fact that array[index] is an lvalue whereas an array name is not was enough justification to declare that an array name is now an lvalue as well. This is quite silly. Rather than developing a more proper definition, they seem to have created another problem--because what we said in the previous paragraph no longer applies. The problem is that now we can say &array without any regrets. Purists would say this expression is incorrect since array is an address (constant) and it is no different from saying &5, for instance, which is obviously meaningless. I should note, as an example of the kind of confusion making an array name an lvalue causes, that at least one C text, C, A Reference Manual, Second Edition, Samuel P. Harbison and Guy L. Steele, Jr. (Prentice-Hall, 1987), and the Lattice C compiler work under the assumption that &array means "pointer to array of type" rather than "pointer of type"--which are not the same. I don't blame the authors for their interpretation, since it is a natural expectation of the & operator. However, I think that we need time to see what the real definition will be. Amazingly, anomalies such as ending up with the equivalent of &5, don't seem to matter much. First, the consensus was that it can't hurt to allow &array, since the context is totally unambiguous and it's clear what the code intended. Second, situations arise, especially when typedef'ing or using macros, where it may not be clear what type a given piece of data is, but you need to use its address. For instance, when dealing with objects (that is, in the object-oriented programming sense) like gizmos and widgets, you may or may not care what the underlying internal representation of the gizmo or widget is. Therefore, the following simplified code (and the compiler) must act consistently: typedef int anint; typedef int array[10]; anint gizmo; array widget; func(&gizmo); func(&widget); Note that func( ) must be prototyped to accept a parameter with void * type. You may argue that the array should be encapsulated within a struct so long as we're dealing with abstract data. However, such use has other side effects, and others would say that such a measure would only be a kludge. Finally, to put the frosting on the cake, ANSI decided to endorse two official definitions of lvalue. One deals strictly with the object locator terminology mentioned above, and the other deals only with the left-hand side of an equal sign. Giving two definitions is not ambiguous, because they've labeled the second one as "modifiable lvalue." They haven't introduced the terminology of "unmodifiable lvalue," since all modifiable lvalues are also "normal" lvalues. However, without that terminology we can't talk about arrays as well as we should be able to. One last piece of information tying all this together involves the const keyword. Notice that a const object (not a pointer to one) is not a modifiable lvalue either. It cannot appear on the left-hand side of an equal sign (except during initialization). It really belongs in the unmodifiable lvalue category along with arrays. Is this fact actually a blessing in disguise? While an array name is also a constant, use of const in C really means "read only" rather than "constant" (somewhat contrary to it's use in C++). Incomplete Types A type is an attribute of an object or function return value that allows you to make use of an access (read and/or write) of that object or function. The object types consist of base types, such as integer, floating-point, enumeration, void, and derived types as well. Derived types include combinations of arrays, functions, pointers, structures, and unions. There is one final category of types; the incomplete type, which uses underlying bases types in most cases but does not yet contain enough information about the object in question so that its size can be determined. For instance, in the structure discussions in "Organizing Data in Your C Programs with Structures, Unions, and Typedefs," MSJ (Vol. 4, No. 2), and "Advanced Techniques for Using Structures and Unions in Your C Code," MSJ (Vol. 4, No. 3), mention is made of how you can refer to a structure tag in a declaration of a pointer even though that structure tag has not yet been described in the code. Another example of an incomplete type would be a common declaration such as: extern sometype identifier[]; where the bounds of the array are not specified in the source file (in this case, of course, it should be specified in at least one source file or you'll get a link error complaining about an unresolved identifier). Perhaps the best example of an incomplete type is the object pointed to by a void *. We are stating without a doubt that, as a generic pointer, the void * cannot be dereferenced, since that's part of what the generic pointer is all about. Char* used to serve this purpose in part, but such overloaded functionality was considered inappropriate. As with all incomplete types, a good test of its completeness is to check (at least mentally) if you can take the sizeof a given potential object. This test can be particularly deceptive when dealing with structures containing other incomplete structures (not pointers to them, though), so watch out for this. A type based on another incomplete type is incomplete as well--in fact, it is so incomplete that it would become a syntax error and an invalid type. Arrays and Pointers The definition of array[type] is simple and is no different than in other languages: it is a group of like objects that are contiguously adjacent to each other in memory. Each adjacent object is an element of the array set, and its type lacks the immediate array attribute that it is an element of. In general, a pointer is defined as a variable that contains the address of another variable. In practice, this definition is mostly true. However we must also consider pointers to functions and pointers to incomplete types. Because of these pointers, a more refined definition is required: a pointer is a variable that contains the address of a function or another object (in other words, things that will exist at run time). Note the use of objects: their use means that pointers cannot point to labels anymore. If you want to do that, you will need to take the risk and use setjmp and longjmp. The relationships among the multitude of terms discussed above are important. One question that may bother you is whether the "lvalue required" message is related to the difference between a pointer reference to an array and a subscript reference to an array, as well as to the way that the array is declared. The answer is, in part, yes, there is a relationship. Details of this relationship will be covered in a future article. For the time being, suffice it to say that you must use each entity correctly or it will be bad news. Most times the bad news is in the form of a compiler error message, but it could also result in a run-time problem. Pointer Mismatches There are two rules of pointer use that you must constantly be aware of. They are: ■ A pointer is not an integer. It never has been, and it never will be. ■ A pointer of type x is not equal to and has nothing to do with, a pointer of type y. It never has, and it never will. Some of you may feel that these rules are incorrect since you may have seen code that performs operations contrary to them. No doubt you have, but that code will be highly nonportable; it depends upon a common yet in no way universal feature of many CPUs. This feature is that in many machines pointers have the same size and internal representation as integers (or "natural words"). However, in the IBM(R) PC, for one, they do not--a program making use of both a near pointer and a far pointer breaks both of these rules for one of the pointers (which one depends upon the memory model you're in). That is not to say that you should stay away from such "tricks" totally, but that you should look at the alternatives before doing so. In general, you might find such code existing in a device driver, where portability might be silly to worry about, since the code is machine-specific anyway. Furthermore, intimate knowledge of the machine leads to highly optimized drivers. Just make sure you're aware that the use of such techniques is termed undefined behavior. Under the right circumstances, though, you simply may not care about this constraint. Casts Although most C programmers are aware that a cast coerces one type into another, they do not always understand why you might want to do that. The result is either too many casts or too few. You should definitely use casts in some cases to ensure that arithmetic type expressions perform as desired and with the correct precisions. This is probably the most portable use of a cast, even though it is still dependent on the architecture and the result is not always what you expect. Another situation where there is a high occurrence of casts is when comparing a pointer to zero. You will often see code that either casts zero or NULL to the type of the pointer being compared. First, neither of the casts is actually required, since C guarantees that the integer zero is a special case and may be used in comparisons and assignments to pointers. Be aware that you should really use NULL (typically found in <stdio.h> and <stddef.h>) as it does represent an implementation-defined null pointer constant. If you hide the value within the NULL macro, you can maintain a machine-tailored representation for it with very little effort. In any event, casts of the null pointer constant are more of a documentation or style issue than a C requirement. Even though a cast from signed to unsigned is fine (without a given sized type), casting in the other direction is not. You are only guaranteed success when moving to larger types (for example, char to int). However, the determination of the two sizes of the type is not static and hence is not portable either. Therefore most uses of casts, especially those involving arithmetic calculation, imply nonportable behavior. The ramifications of such constructs can be either system-dependent behavior or undefined behavior. In the early days of C, type checking or rather the reporting of it was almost nonexistent. To write code such as: int *ip; char *cp; ip = cp; wouldn't produce a single warning. Today similar code would generate an indirection to different types warning. You can quickly shut up the compiler with a cast: ip = (int *)cp; However, is that always the correct thing to do? A large group of programmers and shops insist that warnings are as good (or shall we say as bad?) as errors and that no code should be brought into production if it contains warnings. Such an attitude is not always the proper one. If your car is running out of oil, disconnecting the low-oil light doesn't make the problem go away; the mismatched pointer message is not much different. It's telling you that you're doing something that has the potential for side effects, and that even if you can get it to work on your machine the way you wrote it (and you may not even be able to), there is a good possibility it may not work on another machine or under a different memory model. With the warning list, you'll always be able to run through it and recheck the lines the compiler complained about. Finally, before leaving casts, I want to make it perfectly clear that casting does not guarantee portability; casts only guarantee a quiet compiler! The bottom line is to use them intelligently. Bitfields C started as a systems development language. Even though it is now used in many other areas, it still serves the systems programming community well by letting systems programmers toy with the underlying hardware of the machine in many situations. One of the ways that programmers can do this is by using bitfields. Bitfield implementation can be compiler and system architecture specific and the differing implementations can limit their usefulness. If you examine the definition of bitfields and look at the strict rules governing their use, you will be surprised at how uncompromising bitfields really are. In short, a bitfield is a set of adjacent bits within a storage unit called a word. Its definition and access are based on structures. For example struct { signed int reserved : 4; signed int reserved : 2; signed int reset : 1; signed int rwflag : 1; } example; defines a variable called example that contains bitfields of various sizes. The number following the colon is an integer type specifying the number of bits in the field. Bitfields are also subject to the following conditions: ■ a bitfield may be only type int, unsigned int, or signed int ■ an optional identifier, such as reserved in the above example, names the bitfield ■ any sized area may be allocated for the field ■ adjacent fields are merged in an area if bits are available ■ nonbitfield members and 0 length bitfield members guarantee that storage for the member following it in the member declaration list begin on an int boundary. Individual bitfields are referenced in the same way as other structure members: example.reserved, example.speed, and so on. Bitfields behave like integers and may participate in arithmetic expressions like other integers. However there are a few caveats you should be aware of when using bitfields. Bitfields are not arrays and do not have addresses, so you cannot use & with them. You cannot use pointers to bitfields either, only to the addressable area that they use. In addition, you cannot take the address or the size of a bitfield. In fact, you may even question whether all this bitfield terminology is a misnomer given the limits discussed throughout this sidebar. In general, a programmer is usually conscious of portability and will code with different concerns in mind. However, with bitfields, not only are there limitations to their use, but each C compiler is at liberty to choose its own algorithm for implementing bitfields without regard to another compiler, even if it is running on the same hardware. Some of the implementation-defined aspects of bitfields that you should be aware of are: ■ the high-order bit of an int field is a sign bit ■ adjacent bitfields occupy two areas (merged and new) if bits are unavailable in the area ■ adjacent bitfields take over new area if bits are unavailable ■ the direction of bits ordering, for example low to high ■ field area alignment requirements ■ nonbitfield member alignment requirements Of course, things like word ordering probably wouldn't change from compiler to compiler on the same machine. However, let's say you are trying to add something to a group of fields that is a member of a structure. This structure is being pointed to in such a way that it uses direct memory access (DMA) to write into a special register on a device. If you change compilers, you don't want to find that the bitfields in your code are accessed in a slightly different manner. For example, the code in Figure A is unpredictable. The programmer of such a piece of code would like the bitfields to be placed within the same word. There is, however, no guarantee that this will happen. An implementation of the C compiler is free to place a bitfield right next to another bitfield in the same word, but if it doesn't want to, it doesn't have to. Assuming the worst case, struct _example may actually take 20 bytes of space (5 bitfields using one 4-byte int each); the best case is of course only 2 bytes. Code such as that shown in Figure A usually expects the 2-byte situation and therefore anything more, although valid C, can be detrimental. There is something else to worry about as well. You may not know precisely how the generated code in your executable file will access the bitfields. Since you might be dealing with a device's register, the bitfield memory references must occur in a specific fashion for the device to respond in a reasonable manner. In this case, you will probably need to resort to allocating the space for the register(s) to a proper size and then applying the & (and) and | (or) operators (at least) to that memory area as required, instead of using a bitfield. The code in Figure B, for example, may cause the memory being pointed to by pbits to be read from and written to eight times for each bit. For hardware using strobe lines or where the access of one register affects the value of another, all this reading and writing could produce meaningless results even if the bits refer to the right memory locations. That may sound unattractive, and it is. You should realize that once you leave the hardware-specific problems of bitfields, their nonportable aspects become much less important and in general you are now faced with the typical space-versus-time trade-offs. For instance, you may find the need to use bitfields when encoding a large database to save precious disk or memory space. However, the encoding of the fields in a smaller area will most likely produce a lot of bit twiddling and therefore the program will run slower. Depending on your application, the amount of code generated to manipulate a bitfield can be very lengthy. Look at the assembler code generated from a piece of code using bitfields and you will see what I mean. One unsatisfying experience I went through when learning C was creating an array of bitfields to mimic a bitmap. At first try, you expect to be able to create an array of bits much like the one shown in Figure C. Unfortunately this code produces a syntax error since you cannot have an array of bitfields. The only other "natural" choice is to use code like that shown in Figure D; however, you will quickly notice that the program begins to consume an enormous amount of memory. The reason is that bitmap[1].bit is not 1 bit away from bitmap[0].bit but is 1 word away (or possibly more if alignment requirements must be considered). The only real option you have here is to forget about the bitfield altogether and make the field a char. You can forget about the struct as well and allocate an array of chars along with a bunch of byte and bit access macros or functions making use of the & and | operators. This, however, becomes tedious for programmers to code. I was disappointed that you can't use the & (address) operator on bitfields as you can with other members of structures. Bitfields are not addressable entities. That makes sense when you realize that one or more bitfields could appear within the same word. Instead of returning the same address for the fields, the operation is simply not allowed. In Figure E, the assignments of the address of bit1 and bit2 to pi are therefore both syntax errors. I have covered the problems of bitfields and why they are avoided. For most systems programming, bitfields should be used cautiously. I would advise writing a small piece of code that uses them and executing that code while in a debugger so you can verify what is happening to that memory. In application programming, though, most of the implementation-specific pitfalls of bitfields can be ignored. Under these circumstances, the notational convenience of bitfields is well worth their use. Figure A #define RESET 0 #define NORESET 1 #define READ 0 #define WRITE 1 struct _example { int reserved : 4; int speed : 2; int reset : 1; int rwflag : 1; int data : 8; }; main() { struct _example *pexample; /* code initializing pexample to an absolute address */ pexample->speed = 96; pexample->reset = NORESET; pexample->rwflag = WRITE; pexample->data = 'a'; /* ... */ } Figure B #define DMA_ADDR 0 /* dummy addr for now */ struct _bits { int bit1 : 1; int bit2 : 1; int bit3 : 1; int bit4 : 1; int bit5 : 1; int bit6 : 1; int bit7 : 1; int bit8 : 1; }; main() { struct _bits *pbits = (struct _bits *)DMA_ADDR; pbits->bit1 = 1; pbits->bit2 = 1; pbits->bit3 = 1; pbits->bit4 = 1; pbits->bit5 = 1; pbits->bit6 = 1; pbits->bit7 = 1; pbits->bit8 = 1; } Figure C struct _bitmap { int bits[1024] : 1; } bitmap; main() { int i; /* initialize the bitmap */ for (i = 0; i < 1024; i++) bitmap.bits[i] = 0; } Figure D struct _bitmap { int bit : 1; } bitmap[1024]; main() { int i; /* initialize the bitmap */ for (i = 0; i < 1024; i++) bitmap[i].bit = 0; printf("%d\n", sizeof(bitmap)); } Figure E struct _example { int xyz; int bit1 : 1; /* .......... */ int bit2 : 1; } example; main() { int *pi; pi = &example.xyz; /* fine */ pi = &example.bit1; /* not fine */ pi = &example.bit2; /* not fine */ } ────────────────────────────────────────────────────────────────────────────

Volume 4 - Number 5

──────────────────────────────────────────────────────────────────────────── Design Goals and Implementation of the New High Performance File System Ray Duncan The High Performance File System (hereafter HPFS), which is making its first appearance in the OS/2 operating system Version 1.2, had its genesis in the network division of Microsoft and was designed by Gordon Letwin, the chief architect of the OS/2 operating system. The HPFS has been designed to meet the demands of increasingly powerful PCs, fixed disks, and networks for many years to come and to serve as a suitable platform for object-oriented languages, applications, and user interfaces. The HPFS is a complex topic because it incorporates three distinct yet interrelated file system issues. First, the HPFS is a way of organizing data on a random access block storage device. Second, it is a software module that translates file-oriented requests from an application program into more primitive requests that a device driver can understand, using a variety of creative techniques to maximize performance. Third, the HPFS is a practical illustration of an important new OS/2 feature known as installable file systems. This article introduces the three aspects of the HPFS. But first, it puts the HPFS in perspective by reviewing some of the problems that led to the system's existence. FAT File System The so-called FAT file system, which is the file system used in all versions of the MS-DOS(R) operating system to date and in the first two releases of OS/21 (Versions 1.0 and 1.1), has a dual heritage in Microsoft's earliest programming language products and the Digital Research(R) CP/M(R) operating system--software originally written for 8080-based and Z-80-based microcomputers. It inherited characteristics from both ancestors that have progressively turned into handicaps in this new era of multitasking, protected mode, virtual memory, and huge fixed disks. The FAT file system revolves around the File Allocation Table for which it is named. Each logical volume has its own FAT, which serves two important functions: it contains the allocation information for each file on the volume in the form of linked lists of allocation units (clusters, which are power-of-2 multiples of sectors) and it indicates which allocation units are free for assignment to a file that is being created or extended. The FAT was invented by Bill Gates and Marc McDonald in 1977 as a method of managing disk space in the NCR version of standalone Microsoft(R) Disk BASIC. Tim Paterson, at that time an employee of Seattle Computer Products (SCP), was introduced to the FAT concept when his company shared a booth with Microsoft at the National Computer Conference in 1979. Paterson subsequently incorporated FATs into the file system of 86-DOS, an operating system for SCP's S-100 bus 8086 CPU boards. 86-DOS was eventually purchased by Microsoft and became the starting point for MS-DOS2 Version 1.0, which was released for the original IBM(R) PC in August 1981. When the FAT was conceived, it was an excellent solution to disk management, mainly because the floppy disks on which it was used were rarely larger than 1Mb. On such disks, the FAT was small enough to be held in memory at all times, allowing very fast random access to any part of any file. This proved far superior to the CP/M method of tracking disk space, in which the information about the sectors assigned to a file might be spread across many directory entries, which were in turn scattered randomly throughout the disk directory. When applied to fixed disks, however, the FAT began to look more like a bug than a feature. It became too large to be held entirely resident and had to be paged into memory in pieces; this paging resulted in many superfluous disk head movements as a program was reading through a file and degraded system throughput. In addition, because the information about free disk space was dispersed across many sectors of FAT, it was impractical to allocate file space contiguously, and file fragmentation became another obstacle to good performance. Moreover, the use of relatively large clusters on fixed disks resulted in a lot of dead space, since an average of one-half cluster was wasted for each file. (Some network servers use clusters as large as 64Kb.) The FAT file system's restrictions on naming files and directories are inherited from CP/M. When Paterson was writing 86-DOS, one of his primary objectives was to make programs easy to port from CP/M to his new operating system. He therefore adopted CP/M's limits on filenames and extensions so the critical fields of 86-DOS File Control Blocks (FCBs) would look almost exactly like those of CP/M. The sizes of the FCB filename and extension fields were also propagated into the structure of disk directory entries. In due time, 86-DOS became MS-DOS and application programs for MS-DOS proliferated beyond anyone's wildest dreams. Since most of the early programs depended on the structure of FCBs, the 8.3 format for filenames became irrevocably locked into the system. During the last couple of years, Microsoft and IBM have made valiant attempts to prolong the useful life of the FAT file system by lifting the restrictions on volume sizes, improving allocation strategies, caching pathnames, and moving tables and buffers into expanded memory. But these can only be regarded as temporizing measures, because the fundamental data structures used by the FAT file system are simply not well suited to large random access devices. The HPFS solves the FAT file system problems mentioned here and many others, but it is not derived in any way from the FAT file system. The architect of the HPFS started with a clean sheet of paper and designed a file system that can take full advantage of a multitasking environment, and that will be able to cope with any sort of disk device likely to arrive on microcomputers during the next decade. HPFS Volume Structure HPFS volumes are a new partition type--type 7--and can exist on a fixed disk alongside of the several previously defined FAT partition types. IBM-compatible HPFS volumes use a sector size of 512 bytes and have a maximum size of 2199Gb (232 sectors). Although there is no particular reason why floppy disks can't be formatted as HPFS volumes, Microsoft plans to stick with FAT file systems on floppy disks for the foreseeable future. (This ensures that users will be able to transport files easily between MS-DOS and OS/2 systems.) An HPFS volume has very few fixed structures (Figure 1). Sectors 0-15 of a volume (8Kb) are the BootBlock and contain a volume name, 32-bit volume ID, and a disk bootstrap program. The bootstrap is relatively sophisticated (by MS-DOS standards) and can use the HPFS in a restricted mode to locate and read the operating system files wherever they might be found. Sectors 16 and 17 are known as the SuperBlock and the SpareBlock respectively. The SuperBlock is only modified by disk maintenance utilities. It contains pointers to the free space bitmaps, the bad block list, the directory block band, and the root directory. It also contains the date that the volume was last checked out and repaired with CHKDSK /F. The SpareBlock contains various flags and pointers that will be discussed later; it is modified, although infrequently, as the system executes. The remainder of the disk is divided into 8Mb bands. Each band has its own free space bitmap in which a bit represents each sector. A bit is 0 if the sector is in use and 1 if the sector is available. The bitmaps are located at the head or tail of a band so that two bitmaps are adjacent between alternate bands. This allows the maximum contiguous free space that can be allocated to a file to be 16Mb. One band, located at or toward the seek center of the disk, is called the directory block band and receives special treatment (more about this later). Note that the band size is a characteristic of the current implementation and may be changed in later versions of the file system. Files and Fnodes Every file or directory on an HPFS volume is anchored on a fundamental file system object called an Fnode (pronounced "eff node"). Each Fnode occupies a single sector and contains control and access history information used internally by the file system, extended attributes and access control lists (more about this later), the length and the first 15 characters of the name of the associated file or directory, and an allocation structure (Figure 2). An Fnode is always stored near the file or directory that it represents. The allocation structure in the Fnode can take several forms, depending on the size and degree of contiguity of the file or directory. The HPFS views a file as a collection of one or more runs or extents of one or more contiguous sectors. Each run is symbolized by a pair of doublewords--a 32-bit starting sector number and a 32-bit length in sectors (this is referred to as run-length encoding). From an application program's point of view, the extents are invisible; the file appears as a seamless stream of bytes. The space reserved for allocation information in an Fnode can hold pointers to as many as eight runs of sectors of up to 16Mb each. (This maximum run size is a result of the band size and free space bitmap placement only; it is not an inherent limitation of the file system.) Reasonably small files or highly contiguous files can therefore be described completely within the Fnode (Figure 3). HPFS uses a new method to represent the location of files that are too large or too fragmented for the Fnode and consist of more than eight runs. The Fnode's allocation structure becomes the root for a B+ Tree of allocation sectors, which in turn contain the actual pointers to the file's sector runs (see Figure 4 and the sidebar, "B- Trees and B+ Trees"). The Fnode's root has room for 12 elements. Each allocation sector can contain, in addition to various control information, as many as 40 pointers to sector runs. Therefore, a two-level allocation B+ Tree can describe a file of 480 (12*40) sector runs, with a theoretical maximum size of 7.68Gb (12*40*16 Mb) in the current implementation (although the 32-bit signed offset parameter for DosChgFilePtr effectively limits file sizes to 2Gb). In the unlikely event that a two-level B+ Tree is not sufficient to describe a highly fragmented file, the file system will introduce additional levels in the tree as needed. Allocation sectors in the intermediate levels can hold as many as 60 internal (nonterminal) B+ Tree nodes, which means that the descriptive ability of this structure rapidly grows to numbers that are nearly beyond comprehension. For example, a three-level allocation B+ Tree can describe a file with as many as 28,800 (12*60*40) sector runs. Run-length encoding and B+ Trees of allocation sectors are a memory-efficient way to specify a file's size and location, but they have other significant advantages. Translating a logical file offset into a sector number is extremely fast: the file system just needs to traverse the list (or B+ Tree of lists) of run pointers until it finds the correct range. It can then identify the sector within the run with a simple calculation. Run-length encoding also makes it trivial to extend the file logically if the newly assigned sector is contiguous with the file's previous last sector; the file system merely needs to increment the size doubleword of the file's last run pointer and clear the sector's bit in the appropriate freespace bitmap. Directories Directories, like files, are anchored on Fnodes. A pointer to the Fnode for the root directory is found in the SuperBlock. The Fnodes for directories other than the root are reached through subdirectory entries in their parent directories. Directories can grow to any size and are built up from 2Kb directory blocks, which are allocated as four consecutive sectors on the disk. The file system attempts to allocate directory blocks in the directory band, which is located at or near the seek center of the disk. Once the directory band is full, the directory blocks are allocated wherever space is available. Each 2Kb directory block contains from one to many directory entries. A directory entry contains several fields, including time and date stamps, an Fnode pointer, a usage count for use by disk maintenance programs, the length of the file or directory name, the name itself, and a B-Tree pointer. Each entry begins with a word that contains the length of the entry. This provides for a variable amount of flex space at the end of each entry, which can be used by special versions of the file system and allows the directory block to be traversed very quickly (Figure 5). The number of entries in a directory block varies with the length of names. If the average filename length is 13 characters, an average directory block will hold about 40 entries. The entries in a directory block are sorted by the binary lexical order of their name fields (this happens to put them in alphabetical order for the U.S. alphabet). The last entry in a directory block is a dummy record that marks the end of the block. When a directory gets too large to be stored in one block, it increases in size by the addition of 2Kb blocks that are organized as a B-Tree (see "B-Trees and B+ Trees"). When searching for a specific name, the file system traverses a directory block until it either finds a match or finds a name that is lexically greater than the target. In the latter case, the file system extracts the B- Tree pointer from the entry. If there is no pointer, the search failed; otherwise the file system follows the pointer to the next directory block in the tree and continues the search. A little back-of-the-envelope arithmetic yields some impressive statistics. Assuming 40 entries per block, a two-level tree of directory blocks can hold 1640 directory entries and a three-level tree can hold an astonishing 65,640 entries. In other words, a particular file can be found (or shown not to exist) in a typical directory of 65,640 files with a maximum of three disk hits--the actual number of disk accesses depending on cache contents and the location of the file's name in the directory block B-Tree. That's quite a contrast to the FAT file system, where in the worst case more than 4000 sectors would have to be read to establish that a file was or was not present in a directory containing the same number of files. The B-Tree directory structure has interesting implications beyond its effect on open and find operations. A file creation, renaming, or deletion may result in a cascade of complex operations, as directory blocks are added or freed or names are moved from one block to the other to keep the tree balanced. In fact, a rename operation could theoretically fail for lack of disk space even though the file itself is not growing. In order to avoid this sort of disaster, the HPFS maintains a small pool of free blocks that can be drawn from in a directory emergency; a pointer to this pool of free blocks is stored in the SpareBlock. Extended Attributes File attributes are information about a file that is maintained by the operating system outside the file's overt storage area. The FAT file system supports only a few simple attributes (read only, system, hidden, and archive) that are actually stored as bit flags in the file's directory entry; these attributes are inspected or modified by special function calls and are not accessible through the normal file open, read, and write calls. The HPFS supports the same attributes as the FAT file system for historical reasons, but it also supports a new form of file-associated, highly generalized information called Extended Attributes (EAs). Each EA is conceptually similar to an environment variable, taking the form name=value except that the value portion can be either a null-terminated (ASCIIZ) string or binary data. In OS/2 1.2, each file or directory can have a maximum of 64Kb of EAs attached to it. This limit may be lifted in a later release of OS/2. The storage method for EAs can vary. If the EAs associated with a given file or directory are small enough, they will be stored right in the Fnode. If the total size of the EAs is too large, they are stored outside the Fnode in sector runs, and a B+ Tree of allocation sectors can be created to describe the runs. If a single EA gets too large, it can be pushed outside the Fnode into a B+ Tree of its own. The kernel API functions DosQFileInfo and DosSetFileInfo have been expanded with new information levels that allow application programs to manipulate extended attributes for files. The new functions DosQPathInfo and DosSetPathInfo are used to read or write the EAs associated with arbitrary pathnames. An application program can either ask for the value of a specific EA (supplying a name to be matched) or can obtain all of the EAs for the file or directory at once. Although application programs can begin to take advantage of EAs as soon as the HPFS is released, support for EAs is an essential component in Microsoft's long-range plans for object-oriented file systems. Information of almost any type can be stored in EAs, ranging from the name of the application that owns the file to names of dependent files to icons to executable code. As the HPFS evolves, its facilities for manipulating EAs are likely to become much more sophisticated. It's easy to imagine, for example, that in future versions the API might be extended with EA functions that are analogous to DosFindFirst and DosFindNext and EA data might get organized into B-Trees. I should note here that in addition to EAs, the LAN Manager version of HPFS will support another class of file-associated information called Access Control Lists (ACLs). ACLs have the same general appearance as EAs and are manipulated in a similar manner, but they are used to store access rights, passwords, and other information of interest in a networking multiuser environment. Installable File Systems Support for installable file systems has been one of the most eagerly anticipated features of OS/2 Version 1.2. It will make it possible to access multiple incompatible volume structures--FAT, HPFS, CD ROM, and perhaps even UNIX(R)--on the same OS/2 system at the same time, will simplify the life of network implementors, and will open the door to rapid file system evolution and innovation. Installable file systems are, however, only relevant to the HPFS insofar as they make use of the HPFS optional. The FAT file system is still embedded in the OS/2 kernel, as it was in OS/2 1.0 and 1.1, and will remain there as the compatibility file system for some time to come. An installable file system driver (FSD) is analogous in many ways to a device driver. An FSD resides on the disk in a file that is structured like a dynamic-link library (DLL), typically with a SYS or IFS extension, and is loaded during system initialization by IFS= statements in the CONFIG.SYS file. IFS= directives are processed in the order they are encountered and are also sensitive to the order of DEVICE= statements for device drivers. This lets you load a device driver for a nonstandard device, load a file system driver from a volume on that device, and so on. Once an FSD is installed and initialized, the kernel communicates with it in terms of logical requests for file opens, reads, writes, seeks, closes, and so on. The FSD translates these requests--using control structures and tables found on the volume itself--into requests for sector reads and writes for which it can call special kernel entry points called File System Helpers (FsHlps). The kernel passes the demands for sector I/O to the appropriate device driver and returns the results to the FSD (Figure 6). The procedure used by the operating system to associate volumes with FSDs is called dynamic mounting and works as follows. Whenever a volume is first accessed, or after it has been locked for direct access and then unlocked (for example, by a FORMAT operation), OS/2 presents identifying information from the volume to each of the FSDs in turn until one of them recognizes the information. When an FSD claims the volume, the volume is mounted and all subsequent file I/O requests for the volume are routed to that FSD. Performance Issues The HPFS attacks potential bottlenecks in disk throughput at multiple levels. It uses advanced data structures, contiguous sector allocation, intelligent caching, read-ahead, and deferred writes in order to boost performance. First, the HPFS matches its data structures to the task at hand: sophisticated data structures (B-Trees and B+ Trees) for fast random access to filenames, directory names, and lists of sectors allocated to files or directories, and simple compact data structures (bitmaps) for locating chunks of free space of the appropriate size. The routines that manipulate these data structures are written in assembly language and have been painstakingly tuned, with special focus on the routines that search the freespace bitmaps for patterns of set bits (unused sectors). Next, the HPFS's main goal --its prime directive, if you will-- is to assign consecutive sectors to files whenever possible. The time required to move the disk's read/write head from one track to another far outweighs the other possible delays, so the HPFS works hard to avoid or minimize such head movements by allocating file space contiguously and by keeping control structures such as Fnodes and freespace bitmaps near the things they control. Highly contiguous files also help the file system make fewer requests of the disk driver for more sectors at a time, allow the disk driver to exploit the multisector transfer capabilities of the disk controller, and reduce the number of disk completion interrupts that must be serviced. Of course, trying to keep files from becoming fragmented in a multitasking system in which many files are being updated concurrently is no easy chore. One strategy the HPFS uses is to scatter newly created files across the disk--in separate bands, if possible--so that the sectors allocated to the files as they are extended will not be interleaved. Another strategy is to preallocate approximately 4Kb of contiguous space to the file each time it must be extended and give back any excess when the file is closed. If an application knows the ultimate size of a new file in advance, it can assist the file system by specifying an initial file allocation when it creates the file. The system will then search all the free space bitmaps to find a run of consecutive sectors large enough to hold the file. That failing, it will search for two runs that are half the size of the file, and so on. The HPFS relies on several different kinds of caching to minimize the number of physical disk transfers it must request. Naturally, it caches sectors, as did the FAT file system. But unlike the FAT file system, the HPFS can manage very large caches efficiently and adjusts sector caching on a per-handle basis to the manner in which a file is used. The HPFS also caches pathnames and directories, transforming disk directory entries into an even more compact and efficient in-memory representation. Another technique that the HPFS uses to improve performance is to preread data it believes the program is likely to need. For example, when a file is opened, the file system will preread and cache the Fnode and the first few sectors of the file's contents. If the file is an executable program or the history information in the file's Fnode shows that an open operation has typically been followed by an immediate sequential read of the entire file, the file system will preread and cache much more of the file's contents. When a program issues relatively small read requests, the file system always fetches data from the file in 2Kb chunks and caches the excess, allowing most read operations to be satisfied from the cache. Finally, the OS/2 operating system's support for multitasking makes it possible for the HPFS to rely heavily on lazy writes (sometimes called deferred writes or write behind) to improve performance. When a program requests a disk write, the data is placed in the cache and the cache buffer is flagged as dirty (that is, inconsistent with the state of the data on disk). When the disk becomes idle or the cache becomes saturated with dirty buffers, the file system uses a captive thread from a daemon process to write the buffers to disk, starting with the oldest data. In general, lazy writes mean that programs run faster because their read requests will almost never be stalled waiting for a write request to complete. For programs that repeatedly read, modify, and write a small working set of records, it also means that many unnecessary or redundant physical disk writes may be avoided. Lazy writes have their dangers, of course, so a program can defeat them on a per-handle basis by setting the write-through flag in the OpenMode parameter for DosOpen, or it can commit data to disk on a per-handle basis with the DosBufReset function. Fault Tolerance The HPFS's extensive use of lazy writes makes it imperative for the HPFS to be able to recover gracefully from write errors under any but the most dire circumstances. After all, by the time a write is known to have failed, the application has long since gone on its way under the illusion that it has safely shipped the data into disk storage. The errors may be detected by the hardware (such as a "sector not found" error returned by the disk adapter), or they may be detected by the disk driver in spite of the hardware during a read-after-write verification of the data. The primary mechanism for handling write errors is called a hotfix. When an error is detected, the file system takes a free block out of a reserved hotfix pool, writes the data to that block, and updates the hotfix map. (The hotfix map is simply a series of pairs of doublewords, with each pair containing the number of a bad sector associated with the number of its hotfix replacement. A pointer to the hotfix map is maintained in the SpareBlock.) A copy of the hotfix map is then written to disk, and a warning message is displayed to let the user know that all is not well with the disk device. Each time the file system requests a sector read or write from the disk driver, it scans the hotfix map and replaces any bad sector numbers with the corresponding good sector holding the actual data. This lookaside translation of sector numbers is not as expensive as it sounds, since the hotfix list need only be scanned when a sector is physically read or written, not each time it is accessed in the cache. One of CHKDSK's duties is to empty the hotfix map. For each replacement block on the hotfix map, it allocates a new sector that is in a favorable location for the file that owns the data, moves the data from the hotfix block to the newly allocated sector, and updates the file's allocation information (which may involve rebalancing allocation trees and other elaborate operations). It then adds the bad sector to the bad block list, releases the replacement sector back to the hotfix pool, deletes the hotfix entry from the hotfix map, and writes the updated hotfix map to disk. Of course, write errors that can be detected and fixed on the fly are not the only calamity that can befall a file system. The HPFS designers also had to consider the inevitable damage to be wreaked by power failures, program crashes, malicious viruses and Trojan horses, and those users who turn off the machine without selecting Shutdown in the Presentation Manager Shell. (Shutdown notifies the file system to flush the disk cache, update directories, and do whatever else is necessary to bring the disk to a consistent state.) The HPFS defends itself against the user who is too abrupt with the Big Red Switch by maintaining a Dirty FS flag in the SpareBlock of each HPFS volume. The flag is only cleared when all files on the volume have been closed and all dirty buffers in the cache have been written out or, in the case of the boot volume (since OS2.INI and the swap file are never closed), when Shutdown has been selected and has completed its work. During the OS/2 boot sequence, the file system inspects the DirtyFS flag on each HPFS volume and, if the flag is set, will not allow further access to that volume until CHKDSK has been run. If the DirtyFS flag is set on the boot volume, the system will refuse to boot; the user must boot OS/2 in maintenance mode from a diskette and run CHKDSK to check and possibly repair the boot volume. In the event of a truly major catastrophe, such as loss of the SuperBlock or the root directory, the HPFS is designed to give data recovery the best possible chance of success. Every type of crucial file object--including Fnodes, allocation sectors, and directory blocks--is doubly linked to both its parent and its children and contains a unique 32-bit signature. Fnodes also contain the initial portion of the name of their file or directory. Consequently, CHKDSK can rebuild an entire volume by methodically scanning the disk for Fnodes, allocation sectors, and directory blocks, using them to reconstruct the files and directories and finally regenerating the freespace bitmaps. Application Programs and the HPFS Each of the OS/2 releases thus far have carried with them a major discontinuity for application programmers who learned their trade in the MS-DOS environment. In OS/2 1.0, such programmers were faced for the first time with virtual memory, multitasking, interprocess communications, and the protected mode restrictions on addressing and direct control of the hardware and were challenged to master powerful new concepts such as threading and dynamic linking. In OS/2 Version 1.1, the stakes were raised even further. Programmers were offered a powerful hardware-independent graphical user interface but had to restructure their applications drastically for an event-driven environment based on objects and message passing. In OS/2 Version 1.2, it is time for many of the file-oriented programming habits and assumptions carried forward from the MS-DOS environment to fall by the wayside. An application that wishes to take full advantage of the HPFS must allow for long, free-form, mixed-case filenames and paths with few restrictions on punctuation and must be sensitive to the presence of EAs and ACLs. After all, if EAs are to be of any use, it won't suffice for applications to update a file by renaming the old file and creating a new one without also copying the EAs. But the necessary changes for OS/2 Version 1.2 are not tricky to make. A new API function, DosCopy, helps applications create backups--it essentially duplicates an existing file together with its EAs. EAs can also be manipulated explicitly with DosQFileInfo, DosSetFileInfo, DosQPathInfo, and DosSetPathInfo. A program should call DosQSysInfo at run time to find the maximum possible path length for the system, and ensure that all buffers used by DosChDir, DosQCurDir, and related functions are sufficiently large. Similarly, the buffers used by DosOpen, DosMove, DosGetModName, DosFindFirst, DosFindNext, and like functions must allow for longer filenames. Any logic that folds cases in filenames or tests for the occurrence of only one dot delimiter in a filename must be rethought or eliminated. The other changes in the API will not affect the average application. The functions DosQFileInfo, DosFindFirst, and DosFindNext now return all three sets of times and dates (created, last accessed, last modified) for a file on an HPFS volume, but few programs are concerned with time and date stamps anyway. DosQFsInfo is used to obtain volume labels or disk characteristics just as before, and the use of DosSetFsInfo for volume labels is unchanged. There are a few totally new API functions such as DosFsCtl (analogous to DosDevIOCtl but used for communication between an application and an FSD), DosFsAttach (a sort of explicit mount call), and DosQFsAttach (determines which FSD owns a volume); these are intended mainly for use by disk utility programs. In order to prevent old OS/2 applications and MS-DOS applications running in the DOS box from inadvertently damaging HPFS files, a new flag bit has been defined in the EXE file header that indicates whether an application is HPFS-aware. If this bit is not set, the application will only be able to search for, open, or create files on HPFS volumes that are compatible with the FAT file system's 8.3 naming conventions. If the bit is set, OS/2 allows access to all files on an HPFS volume, because it assumes that the program knows how to handle long, free-form filenames and will take the responsibility of conserving a file's EAs and ACLs. Summary The HPFS solves all of the historical problems of the FAT file system. It achieves excellent throughput even in extreme cases--many very small files or a few very large files--by means of advanced data structures and techniques such as intelligent caching, read-ahead, and write-behind. Disk space is used economically because it is managed on a sector basis. Existing application programs will need modification to take advantage of the HPFS's support for extended attributes and long filenames, but these changes will not be difficult. All application programs will benefit from the HPFS's improved performance and decreased CPU use whether they are modified or not. This article is based on a prerelease version of the HPFS that was still undergoing modification and tuning. Therefore, the final release of the HPFS may differ in some details from the description given here--Ed. B-Trees and B+ Trees Most programmers are at least passingly familiar with the data structure known as a binary tree. Binary trees are a technique for imposing a logical ordering on a collection of data items by means of pointers, without regard to the physical order of the data. In a simple binary tree, each node contains some data, including a key value that determines the node's logical position in the tree, as well as pointers to the node's left and right subtrees. The node that begins the tree is known as the root; the nodes that sit at the ends of the tree's branches are sometimes called the leaves. To find a particular piece of data, the binary tree is traversed from the root. At each node, the desired key is compared with the node's key; if they don't match, one branch of the node's subtree or another is selected based on whether the desired key is less than or greater than the node's key. This process continues until a match is found or an empty subtree is encountered (see Figure A). Such simple binary trees, although easy to understand and implement, have disadvantages in practice. If keys are not well distributed or are added to the tree in a non-random fashion, the tree can become quite asymmetric, leading to wide variations in tree traversal times. In order to make access times uniform, many programmers prefer a particular type of balanced tree known as a B- Tree. For the purposes of this discussion, the important points about a B-Tree are that data is stored in all nodes, more than one data item might be stored in a node, and all of the branches of the tree are of identical length (see Figure B). The worst-case behavior of a B-Tree is predictable and much better than that of a simple binary tree, but the maintenance of a B-Tree is correspondingly more complex. Adding a new data item, changing a key value, or deleting a data item may result in the splitting or merging of a node, which in turn forces a cascade of other operations on the tree to rebalance it. A B+ Tree is a specialized form of B-Tree that has two types of nodes: internal, which only point to other nodes, and external, which contain the actual data (see Figure C). The advantage of a B+ Tree over a B- Tree is that the internal nodes of the B+ Tree can hold many more decision values than the intermediate-level nodes of a B-Tree, so the fan out of the tree is faster and the average length of a branch is shorter. This makes up for the fact that you must always follow a B+ Tree branch to its end to get the data for which you are looking, whereas in a B-Tree you may discover the data at an intermediate node or even at the root. FAT File System High Performance File System Maximum filename 11 (in 8.3 format) 254 length Number of dot (.) One Multiple delimiters allowed File attributes Bit flags Bit flags plus up to 64Kb of free-form ASCII or binary information Maximum path 64 260 length Minimum disk space Directory entry Directory entry (length overhead per file (32 bytes) varies) + Fnode (512 bytes) Average wasted 1/2 cluster 1/2 sector (256 bytes) space per file (typically 2Kb or more) Minimum allocation Cluster (typically Sector (512 bytes) unit 4Kb or more) Allocation info Centralized in FAT Located nearby each for files on home track file in its Fnode Free disk space info Centralized in FAT Located near free on home track space in bitmaps Free disk space 2Kb (1/2 cluster 4Kb (8 sectors) described per byte @ 8 sectors/cluster) Directory structure Unsorted linear list, Sorted B-Tree must be searched exhaustively Directory location Root directory on Localized near seek home track, others center of volume scattered Cache replacement Simple LRU Modified LRU, strategy sensitive to data type and usage history Read-ahead None in MS-DOS 3.3 or Always present, earlier, primitive sensitive to data type read-ahead optional and usage history in MS-DOS 4 Write-behind Not available Used by default, but can be defeated on per-handle basis Getting the Most from Expanded Memory with an EMS Function Library Douglas Boling The future of personal computing may ultimately belong to OS/2 operating systems. There are, however, tens of millions of DOS1 operating system users who are demanding more from their applications than ever before. To quench the thirst of these users, application developers need to maximize the use of the available resources of DOS-based machines. One of the resources that must be maximized is the Lotus(R), Intel(R), Microsoft(R), AST(R) (LIM) Expanded Memory Specification. The Expanded Memory Specification, or EMS as it has become known, documents a standard way to implement up to 32Mb of paged RAM in a DOS environment. What is exciting about EMS is that it not only provides access to additional RAM, it also introduces a method of managing that additional memory. Expanded memory has undergone many revisions. As the specification has matured, many of the basic tasks have been implemented in more than one function. The job of the developer is to choose the proper function for each situation. This article discusses the groups of functions defined in EMS Version 4.0 and compares different functions performing similar tasks. It also presents a library of routines that makes the use of expanded memory simpler. Included in the toolkit are routines designed to implement functions of EMS Version 4.0 using a Version 3.2 driver. All routines are written using the Microsoft Macro Assembler (MASM) Version 5.0. The functions designated by the EMS are implemented by a device driver called the Expanded Memory Manager (EMM). These functions will be discussed by functional category. The categories, which are listed in Figure 1, are information, allocation, mapping, name support, save/restore mapping context, operating system/environment, and warm boot. One function, Move Exchange Memory Region (function 24), does not fit into any of the categories, so it will be discussed separately. Information Functions The flexibility of the EMS means that it can be implemented in many different ways. A particular implementation can vary in the number and location of the physical pages, the size of the page frame segment, and the amount of memory available. This variability makes it vitally important for you to know the expanded memory environment of any application. The EMM has nine functions that provide information about the expanded memory environment (see Figure 1). The first action an application should take after determining that the EMM is loaded is to determine the status and version of the driver. Function 1, Get Status, lets an application know that the EMM is functioning properly. As with all functions, the status code is returned in AH. A value of 0 indicates the function was completed with no errors. The EMM version is determined by calling function 7, Get Version. The version number is returned as a BCD byte in AL. The page frame segment can be found by calling Get Page Frame Address (function 2). The page frame is an artifact of earlier versions of the EMS. Versions before EMS 4.0 supported only four consecutive physical pages, which were designated as the page frame. By calling function 2, an application could locate all four physical pages. A physical page is 16Kb and a page frame is a 64Kb location. Version 4.0 allows physical pages to reside anywhere in the first 1Mb of memory. In some instances, the EMM may not be able to find a free 64Kb block to use for the page frame. In that circumstance, the EMM will return an error when function 2 is called. Because of the additional flexibility of physical pages in EMS 4.0, function 2 has been superseded by function 25, Get Mappable Physical Address Array. Therefore when using EMS 4.0, function 25 is the preferred method of determining the location of each physical page in the system. Function 25 returns an array containing the segment address and physical page number of all physical pages in the system. The array is sorted by the segment address in ascending order. This does not imply that the physical page numbers are in ascending order. Physical pages are numbered sequentially from page 0, located at the page frame segment, continuing through the highest address. Any physical pages mapped in conventional memory are numbered in order after pages above 640Kb (see Figure 2). Physical pages mapped in conventional memory are mapped with logical pages from the operating system handle (handle 0). This, however, is not true across all EMMs. The physical page numbers are defined by their call. Only pages 0-3 are guaranteed to be the same in all implementations. Function 3, Get Unallocated Page Count, returns the amount of expanded memory in the system along with the amount of expanded memory that is available for use. The ability to differentiate between not enough free memory and not enough total memory allows an application to accurately inform the user that other applications are using too much memory or that the machine simply does not have enough memory. Get Handle Pages (function 13) and Get All Handle Pages (function 14) provide a way to determine how many pages are assigned to each handle. Function 13 returns the number of pages assigned to a handle specified in DX, whereas function 14 returns an array that contains the number of pages assigned for all open handles. The array holds a two-word structure for each open handle. The structure contains the handle value and the number of pages assigned to that handle. The size of the array can be determined by calling function 12, Get Handle Count, to see how many handles are currently open. Function 26, Get Expanded Memory Hardware Information, shows how the expanded memory is implemented. This function is officially considered an operating system function and is therefore password protected. In reality, there is no reason for applications to need the information returned by this function. The function returns the raw page size, the context save area size, the number of alternate mapping sets, and the number of DMA register sets. The only item that an application might need is the size of the context save area. That number, however, is available by calling function 15 subfunction 2. In general, applications should not use function 26 since it is considered part of the operating system functions of the EMM. Allocation Functions Allocating memory can be done by calling either function 4, Allocate Pages, or function 27, Allocate Standard/Raw Pages. With a few exceptions, both functions perform identically. Function 4 requires that an application request at least one logical page, whereas function 27 allows an application to request a handle without reserving any pages. Requesting a handle with zero pages is useful for applications that use the handle name features of the EMM but do not need any additional memory. Function 27 also allows applications to reserve "raw" pages for use. The EMS 3.2 functions are always 16Kb pages. All additional 4.0 functions use raw pages. Each EMM has a raw page size of 16Kb. The size of the raw page is not defined by EMS 4.0 except that it is smaller than the standard page. Since the only way to determine the raw page size is to use the restricted function 26, it is advisable to avoid using raw pages. The Reallocate Pages function (function 18) lets an application adjust the number of pages reserved by a handle. This function enables applications to use expanded memory more efficiently since memory can be reserved as it is needed instead of all at once. If the number of pages is being reduced, the EMM uses function 6, Deallocate Pages, which deallocates logical pages starting with the highest logical page number. If the request is to increase the number of pages, the new page numbers are added sequentially after the highest logical page number. Because of this allocation strategy, applications expecting to use this function should use the lower numbered logical pages for resident code and permanent data. Mapping Functions Some of the most welcome enhancements made in Version 4.0 are the new methods for mapping logical pages, especially since this task is made more difficult now that EMS 4.0 allows more than four physical pages. Under EMS 3.2, the only way to map a page of memory was to call function 5, Map/Unmap Handle Page, to map one logical page to one physical page. Function 5 is still available under EMS 4.0; additionally, three new functions have been added to make logical page mapping easier. Function 17, Map/Unmap Multiple Handle Pages, allows an application to map more than one logical page in one call. A convenience of function 17 is the ability to select the physical page by its segment address instead of the physical page number. The segment address for each page is found using function 25. Logical pages can be mapped to physical pages located anywhere in memory; mapping pages in conventional memory, however, requires some caution. Physical pages located in conventional memory are subject to DOS memory management procedures. Applications must use DOS to allocate the block of memory where the physical page is located before that page can be remapped. Before a block of memory is released to DOS, the original mapping of that page must be restored. To restore the mapping, applications can either save and restore the mapping context of the pages or use the mapping functions to restore the mapping of the operating system logical pages. The logical pages mapped in conventional memory are owned by the operating system handle (handle 0). The pages are mapped so that the lowest logical page number is mapped to the lowest addressed physical page. An interesting feature of function 17 is the ability to unmap a logical page. Under some conditions it is advisable for applications to restrict access to their expanded memory pages. By specifying the logical page FFFFh to be mapped to a physical page, the logical page currently being mapped at that physical page is unmapped without replacing it with another logical page. The unmapped page can be remapped by using the standard map page functions or by restoring a context in which the page was previously mapped. Two additional mapping functions available in Version 4.0 are Alter Page Map and Jump (function 22) and Alter Page Map and Call (function 23). These functions enable an application to execute a module of code with the expanded memory page map initialized to a known state. These functions are especially useful in size-critical applications. Alter Page Map and Jump transfers control to a specified address after mapping logical pages listed in a mapping array. The control is transferred to the new address, with the state of the flags and the contents of all registers except AX being retained. Likewise, Alter Page Map and Call calls a subroutine after mapping a set of logical pages. On return from the subroutine, the EMM maps a second set of logical pages specified in another mapping array. The state of the flags and the contents of all registers except AX are retained across both the call and the return. The Alter Page Map and Call description in the EMS 4.0 documentation is ambiguous about one point, thus causing some confusion about the specified function of Alter Page Map and Call. The EMS 4.0 specification compares the function to a far call in 8086 assembly code, stating that the context of the page map is saved and restored before and after the call. The description of the calling parameters, however, makes no mention of the save and restore, only that the page map can be altered using the two map multiple structures referenced in the call. Because of this some board manufacturers have interpreted this function as not performing a page map save before the subroutine is called and therefore do not save and restore the mapping context during this function. Therefore, while the specification states that the context of the page map is saved, most drivers do not implement this portion of function 23. Developers must be careful not to assume that the context of the page map is saved by this function--applications should not assume that the mapping context has been saved before the subroutine call. Name Support Functions The handle name support added in EMS 4.0 provides a method of sharing expanded memory pages among different applications. Families of applications can use a standard handle name for pages that are shared among the applications. Get/Set Handle Name, Function 20, assigns a name consisting of an 8-byte string provided by the application to handles. The values of the bytes can range from 0 to 256. A handle name of all binary zeros is considered not to have a name. Handle names are cleared when first assigned or reassigned by the EMM. The name of a handle can be read by calling the function Get/Set Handle Name. Handle names must be unique; if an attempt is made to assign a name that is currently being used by another handle, the EMM will reject the name assignment. When a handle is deallocated, its name is then reset to zeros. To determine the names of all handles in the system, use function 21, Get Handle Directory. Subfunction 0 returns an array containing all active handles and their associated names. Handles without names have a name field containing zeros. The size of the array varies with the number of active handles. To determine the size of the array, multiply the number of open handles supported by the EMM by nine. The number of open handles is returned by Get Handle Count (function 12). To determine the handle for a particular name, an application can call the Search for Named Handle subfunction (function 21, subfunction 1). This subfunction gives applications a convenient way to determine if a handle exists for a shared memory application. Save/Restore Functions Any application that can interrupt another application must always save and restore the mapping context of the EMM. Some examples are interrupt handlers, device drivers, and resident utilities. There are three ways for applications to save and restore the mapping context under EMS 4.0. The simplest way to save and restore the mapping context is to use the matching functions Save Page Map (function 8) and Restore Page Map (function 9). Unlike the methods used by functions 15 and 16 (discussed below), functions 8 and 9 do not require the application to provide a space for the mapping register contents. To use these functions, an application simply calls function 8 to save the mapping context and function 9 to restore the context. There are some limitations to functions 8 and 9. First, these functions only save the mapping contexts of the 64Kb page frame whose address is returned by function 2. If physical pages outside the page frame are changed, one of the other save/restore functions must be used. Second, because the context is saved within the EMM, the number of saved mapping contexts that can be stored at one time is limited. When an application receives the save area full error, it must either wait until another time or use one of the other save/restore functions. Third, to keep track of multiple context saves, functions 8 and 9 require that an application provide a handle. Finally, an application can only save one mapping context at a time with function 8. Before an application can use function 8 again, it must call function 9 to restore the page map. Function 15, Get/Set Page Map, provides a way to save the complete mapping context of the EMM. This function avoids some of the pitfalls of the function 8/9 combination by requiring that the application provide the context save area. Since the application controls the save area, it has the responsibility of keeping track of separate context saves. By requiring the application to provide the save area, the EMM can be more lenient in the rules regarding the use of function 15. An application does not need a handle to use function 15 nor is an application restricted to one context save at a time. As long as the application provides different save areas for each call to function 15, it can continue to nest context saves. Function 16, Get/Set Partial Page Map, provides all of the flexibility of function 15 without the overhead that may be required by it. Since function 15 saves the entire mapping context of the EMM, the save area required may be quite large and the function may take a relatively long time to execute. Using function 16, an application can specify only the pages that are necessary to save. Since an application usually requires only a limited number of the physical pages, function 16 provides a way to customize each context save for minimum overhead. Warm Boot Functions The functions Get/Set Handle Attribute (function 19) and Prepare Expanded Memory Hardware for Warm Boot (function 29) provide support for expanded memory hardware that will retain information across a warm boot (WB). Imagine that there are unmapped pages below 640Kb. In that case, a warm boot will cause the system to crash. The WB call, however, lets the EMM put the board in a state that will survive a WB. Move Functions One of the more powerful functions in EMS 4.0 is Move/Exchange Memory Region (function 24). The move memory function is useful for applications that use expanded memory for storing blocks of data. The function simply requires an application to specify the source block, the destination block, and the size of the move. Function 24 does not alter the current mapping context. The move memory function allows an application to move blocks of data from expanded to expanded, expanded to conventional, conventional to expanded, or conventional to conventional memory. If the source and destination regions overlap, the move direction will be chosen to ensure that the destination receives the source region intact. The exchange memory subfunction performs an exchange of data regions. Unlike the move memory function, the exchange memory subfunction does not allow the two data regions to overlap. Operating Systems/Environment Two functions, Alternate Map Register Set (function 28) and Enable/Disable OS/E (function 30), allow an operating system to enhance the function of the expanded memory hardware and manager. These functions, along with function 26, Get Expanded Memory Hardware Information, are designated operating system/environment functions and should never be used by an application. To ensure operating system control over these functions, each of them can be password protected. By calling Disable OS/E Functions (function 30, subfunction 0), an operating system or environment enables the password protection of functions 26, 28, and 30. The first time this function is called, the EMM returns a password to the operating system. Until the password is returned to the EMM, any call to functions 26, 28, or 30 must use that password to access those functions. The EMM is required by the EMS to use a randomly generated password, since using a fixed value would defeat the purpose of the password protection. The password protection is disabled using the Enable OS/E function (function 30, subfunction 1). The Alternate Map Register Set function (function 28) allows an operating system to enhance the context-switching ability of the EMM. With nine subfunctions, function 28 is one of the more complex functions defined by EMS 4.0. EMS-compatible memory boards can have multiple sets of mapping registers. By changing the active map register set, an operating system or environment can quickly change the context using hardware instead of slowly saving and loading each map register. Function 28 provides direct control of which map register set is active. In addition, it provides a method for simulating map register sets in a way similar to the Get/Set Page Map function. Function 28 also provides control over an additional set of mapping registers devoted to DMA accesses to expanded memory. The DMA mapping registers provide a way for an operating system to switch to another task while waiting for a DMA process to be completed. EMS Simulators Because of the usefulness of memory management features of expanded memory, many software products have appeared on the market that use PC/AT type extended memory to emulate expanded memory. These simulators are handy since they impose the EMS memory management features on extended memory that, until recently, had no documented memory management guidelines. There are two types of these EMS stimulators. One EMS simulator is a pure software implementation that uses reserved areas in conventional RAM as physical pages. To "map" a logical page, the simulator copies the data from extended memory. Because the logical page data is copied from extended RAM, this type of simulator is relatively slow. Another problem is that this simulator cannot support data aliasing. Data aliasing is a feature that is implemented by addressing the same block of RAM in two different address spaces. It can be seen in expanded memory by mapping the same logical page to two different physical pages. If aliasing is supported, changing a byte in one physical page will automatically change the identical byte in the other physical page. Since the "copy type" EMS simulators copy data to each physical page when the page is mapped, there is no way for the data to be simultaneously updated in both copies of a logical page. Using the paging registers in the Intel 386, an EMM driver can be written that can implement all the EMS 4.0 functions. Because the logical-to-physical page mapping is done in hardware instead of simply copying the data, this type of simulator is fast and supports data aliasing. EMS Routines The library of routines in Figure 3 is designed to facilitate the use of expanded memory. GetEMSMem, SaveEMSMem, RestoreEMSMem, and FreeEMSMem provide all the basic functions needed for an application to use expanded memory. CheckAlias and MapbyName are handy for advanced applications. Finally, EMS32Upgrade implements functions 17, 22, and 23 of the EMS 4.0 using Version 3.2 calls. The GetEMSMem routine takes care of the overhead involved in requesting expanded memory. This routine determines if expanded memory is present, finds the location and size of the page frame, requests memory, maps it into the page frame, and names the handle. Determining if the EMM is loaded is the first task that any program using expanded memory must perform. The recommended technique for determining the presence of the EMM is to interrogate the EMM device driver as you would any other DOS device. The idea is to use the DOS file open and DOS I/O control commands to determine if a device with the name EMMXXXX0 is available and ready. Another technique for finding the EMM is to determine if the interrupt 67h vector is pointing to the segment of a device driver with the name EMMXXXX0. The FindEMM routine uses both of these methods for finding the driver. Whereas the Open Handle technique is the more formal of the two methods because it uses only DOS commands, the Get Interrupt Vector technique is actually simpler. One caution: if the application is a device driver or a TSR interrupting a DOS command, the Get Interrupt Vector technique must be used since DOS file commands are not available in these situations. After the requested number of logical pages are reserved using the Allocate Pages function (function 4), the size of the page frame is determined. If the EMM is Version 3.2, the page frame segment is assumed to be four pages. For Version 4.0, the Get Mappable Physical Address Array function (function 25) is called to determine the segments of all physical pages. The array is searched to find physical page 0, which is the start of the page frame. The routine then counts the number of consecutive page segments. GetEMSMem maps the logical pages to the page frame. If there are more logical pages than physical pages in the page frame, then only enough pages are mapped to fill the page frame. Finally, if a name was passed to the routine, the Get/Set Handle Name function (function 20) is called to set the handle name. The SaveEMSMem and RestoreEMSMem routines combine the simplicity of EMS functions 8 and 9 with the flexibility of function 15. The advantage of using the combination SaveEMSMem/RestoreEMSMem is that these routines keep track of multiple saves and restores while allowing the application to define the amount of memory for the save data. The routines keep track of separate saves by returning an index value to the caller after each call to SaveEMSMem. To restore the mapping context, call RestoreEMSMem with the index value provided by the SaveEMSMem call that is stored in BX. The routines use the Get/Set Page Map function (function 15) to perform the context save and restore. To keep track of the different save arrays, the data buffer is organized as a linked list. Each item in the list contains the context save area's length, index value, and the save array. FreeEMSMem is a simple routine that deallocates an expanded memory handle and all logical pages assigned to that handle. Although the routine does nothing but call EMS Deallocate Pages (function 6), it is included for completeness. The CheckAlias routine implements a simple technique for determining if the EMM supports data aliasing. The routine calls the EMM to allocate one logical page, then maps the page to two different physical pages. One physical page is modified and then compared to the other page. If the second page is modified by the write, then data aliasing is supported. MapbyName is a simple but handy routine that maps a logical page by specifying the name of a handle instead of the handle itself. The routine is implemented by two calls to the EMM. The first is a call to search for the handle given the name pointed to by DS:SI. The second is a call to the map page function using the handle returned by the first call. Driver Routines The added functionality of the 4.0 specification can be quite useful, so it is a shame to write applications for the lowest common denominator. But writing applications that require 4.0 drivers can limit the available customer base. The solution is to write routines that implement some of the features of EMS 4.0 using EMS 3.2 functions. In addition to the EMS routines, Figure 3 has a collection of routines that help bridge the gap between EMS 3.2 and EMS 4.0 drivers. The routines in Figure 3 implement the Map Multiple, Map and Jump, and Map and Call functions of the EMS 4.0 driver. Each routine duplicates the application interface used by its respective EMS 4.0 functions. This way applications can easily migrate away from these routines as the use of EMS 3.2 drivers diminishes. A dispatch routine EMS32Upgrade is included to call each of the emulation functions by their EMS 4.0 function numbers. The routine also calls functions 1 though 15 of EMS 3.2. The status code is returned in AH as it is when calling the EMM directly. If an EMS 4.0 routine is found, EMS32Upgrade simply passes the function call to the EMS 4.0 driver. The EMSMapMultiple routine provides the convenient function of mapping many pages with one call. MapMultiple, like function 17 in EMS 4.0, is called with the function and subfunction values in AX, a handle value in DX, the number of pages to be mapped in CX, and DS:SI pointing to a logical-to-physical map array. The array is composed of repeating structures containing a logical page number followed by a physical page number or physical page segment. Register AH should contain 50h to correspond to the function value in the EMS 4.0 function. AL contains the subfunction value--0 for structures containing the physical page numbers and 1 for structures containing physical page segments. The actual mapping is performed by calling function 5, Map/Unmap Handle Page. MapMultiple must convert physical page segments into page numbers since function 5 does not recognize page segments. To convert the physical page segment address to a page number, the routine subtracts the page frame segment address from the physical page segment, then divides the address by the size of a segment. The result of the divide is checked to ensure that there is no remainder and that the resulting quotient is between 0 and 3. If either of these two requirements is not met, the segment address is considered to be invalid and the routine terminates. The EMSMapandJump routine provides a way for an application to jump to a routine with the physical page map set to a known value. The routine is called with the AH containing the function value 55h, the handle in DX, and DS:SI pointing to the map and jump structure. AL contains a selector indicating whether the physical pages in the mapping array are selected by page number or by segment address. As in the MapMultiple routine, a value of 0 in AL indicates page number and a value of 1 indicates page segment address. The map and jump structure contains a far pointer to the destination address in segment offset format followed by a byte containing the number of pages to map and a far pointer to the logical-to-physical map array. The map array is in the same format as in the MapMultiple routine. Since the mapping structure of the MapandJump function is the same as the MapMultiple function, the MapMultiple routine is simply called to provide the mapping function of the MapandJump routine. To exit the procedure without disturbing the stack, the jump is performed by altering the return address of the call to EMS32Upgrade. This way the MapandJump routine returns to EMS32Upgrade, which performs a far return to the new address. One caution, the technique of altering the stack does not allow this routine to be called directly. The EMSMapandCall routine provides a way for an application to jump to a routine with the physical page map set to a known value. The routine is called with the AH containing the function value 56h, the handle in DX, and DS:SI pointing to the map and call structure. As before, AL contains the physical page selector--0 for physical page number and 1 for segment address. The map and call structure is more complex than the map and jump structure since the map and call function provides two separate mappings, one before the call and one after the return. The structure contains a far pointer to the subroutine, a byte containing the number of pages to map before the call, a far pointer to the mapping structure used before the call, a byte with the number of pages to map after the return, and a far pointer to the mapping structure to use after the return. As before, the map array is in the same format as in the MapMultiple routine. EXAMPLE.ASM (see Figure 4) is a simple program written to show the utility of the toolbox routines. The program, a simple TSR, captures an image of the screen so it can be recalled later. The saved screen buffer is stored in expanded memory to conserve conventional memory space, which is always shorter than necessary. To assemble EXAMPLE.EXE, use the following three statements: MASM EXAMPLE MASM EMSTOOLS LINK EXAMPLE EMSTOOLS The simplicity of EXAMPLE shows how easy it is to use expanded memory. Only four calls are made in order to use all of the expanded memory functions required by the TSR. The call to GetEMSMem checks for the driver and reserves the memory. SaveEMSMem and RestoreEMSMem are used by the resident routine to preserve the state of the page frame. Finally, one call is made directly to the EMM to map the expanded memory page containing the stored data. In a more complex program, the other toolbox routines can be used with the same ease as the routines used by EXAMPLE.EXE. As this article has demonstrated, the Expanded Memory Specification has evolved into a powerful tool for the DOS applications developer. The additional memory available, along with the functions to manage this memory, enable developers to continue to create full-featured applications for the DOS environment. The library of routines presented here provides some enhancements to the the functionality of EMS 4.0. The most powerful expanded memory tools, however, are the functions of EMS 4.0. Figure 1 Information 1. Get Status 2. Get Page Frame Address 3. Get Unallocated Page Count 7. Get Version 12. Get Handle Count 13. Get Handle Pages 14. Get All Handle Pages 25. Get Mappable Physical Address Array 26. Get Expanded Memory Hardware Information (OpSys Function) Allocation 4. Allocate Pages 6. Deallocate Pages 18. Reallocate Pages 27. Allocate Standard/Raw Pages Mapping 5. Map/Unmap Handle Page 17. Map/Unmap Multiple Handle Pages 22. Alter Page Map and Jump 23. Alter Page Map and Call Name Support 20. Get/Set Handle Name 21. Get Handle Directory Save/Restore Mapping Context 8. Save Page Map 9. Restore Page Map 15. Get/Set Page Map 16. Get/Set Partial Page Map Operating System/Environment 28. Alternate Map Register Set 30. Enable/Disable OS/E Warm Boot 19. Get/Set Handle Attribute 29. Prepare Expanded Memory Hardware for Warm Boot Move 24. Move/Exchange Memory Region Figure 3 ;==================================================================== ; Assembly language EMS routines ; (c) 1989 Microsoft Corporation ;==================================================================== .MODEL small .DATA ems_header db "EMMXXXX0",0 ;ASCIIZ EMS driver name fun25_array dw 96 dup (0) ;Array for physical page data fun25_array_end = $ ems32up_version db 0 ;Version number for dispatcher ems32up_calltbl dw offset EMSMapMultiple dw offset EMSMapandJump dw offset EMSMapandCall .CODE PUBLIC GetEMSMem PUBLIC SaveEMSMem PUBLIC RestoreEMSMem PUBLIC FreeEMSMem PUBLIC CheckAlias PUBLIC MapbyName PUBLIC EMS32Upgrade ;==================================================================== ;GetEMSMem reserves the amount of expanded memory requested in BX ; Entry: BX - Amount of memory to reserve ; SI - Pointer to name to assign to handle in ASCIIZ format ; Set SI = -1 for no name ; Exit: AL - EMM Version ; BX - Segment of page frame ; CX - Size of page frame ; DX - EMS handle. ;==================================================================== GetEMSMem proc push bp mov bp,sp push ax push bx push cx push dx push di push si push es ;-------------------------------------------------------------------- ;See if EMS driver installed. ;-------------------------------------------------------------------- call FindEMS ;See if EMM is loaded or ah,ah ;Check for error jne getp_error ;RC <> 0, error ;-------------------------------------------------------------------- ;Reserve pages. ;-------------------------------------------------------------------- mov bx,[bp-4] ;Get number of pages to res mov ah,43h ;EMS Allocate pages function int 67h ;Call EMS Driver or ah,ah ;Check for error jne getp_error ;If error, exit mov [bp-8],dx ;Save handle ;-------------------------------------------------------------------- ;Get version. ;-------------------------------------------------------------------- mov ah,46h ;Get expanded memory version int 67h or ah,ah jne getp_error mov [bp-2],al ;Save version mov dl,al ;--------------------------------------------------------------------- ;See how many physical pages and the number of pages in the page frame ;--------------------------------------------------------------------- mov cx,4 ;Assume EMM ver 3.2, 4 pages mov bx,cx ;Save number of pages in frame cmp dl,40h ;Check for version 4.0 jb getp_continue ;If before 4.0, use 4 pages mov ax,5801h ;Get number of entries in the int 67h ;map array or ah,ah ;Check for error jne getp_error mov ax,cx ;Copy number of entries sal ax,1 ;Mult number of entries by 4 sal ax,1 cmp ax,offset fun25_array_end - offset fun25_array jb getp_2 mov ah,0f2h ;Load array too large RC jmp getp_exit getp_2: mov bx,cx ;Copy number of physical pages mov ax,ds ;Set ES = DS mov es,ax mov di,offset fun25_array mov ax,5800h ;Get mappable page array int 67h ;Call driver or ah,ah jne getp_error xor ax,ax getp_loop2: cmp ax,[di+2] ;Search list for physical page je getp_3 ;zero. That is the start of add di,4 ;the page frame loop getp_loop2 mov ah,0f3h ;Page frame not found in list getp_error: jmp getp_exit getp_3: xor bx,bx ;Zero consecutive page count getp_loop3: inc bx ;Count the number of mov dx,[di] ;consecutive segments add dx,1024 ;Add num of paragraphs per seg cmp dx,[di+4] ;Compare with next segment jne getp_continue ;If not consecutive, exit add di,4 ;Point to next entry loop getp_loop3 getp_continue: mov [bp-6],bx ;Put page frame size into mov cx,[bp-4] ;stack and get num of pages ;-------------------------------------------------------------------- ;Map pages into page frame ;-------------------------------------------------------------------- cmp cx,bx ;See if number of pages larger jb getp_4 ;than the size of the frame mov cx,bx ;Use the page frame size getp_4: xor bx,bx ;Start with logical page 0 mov dx,[bp-8] ;Get back handle getp_loop4: mov al,bl ;at physical page 0 mov ah,44h ;EMS Map Page function int 67h ;Call EMS Driver or ah,ah ;Check for error jne getp_exit ;If error, exit inc bx ;Point to next logical page loop getp_loop4 ;-------------------------------------------------------------------- ;Get page frame segment ;-------------------------------------------------------------------- mov ah,41h ;EMS Get page frame segment int 67h ;Call EMS Driver or ah,ah ;Check for error jne getp_exit ;If error, exit mov [bp-4],bx ;Save page frame segment ;-------------------------------------------------------------------- ;Set the handle name ;-------------------------------------------------------------------- xor ax,ax ;Zero return code mov si,[bp-12] ;Get back name pointer cmp si,-1 ;if -1, no name je getp_exit mov dx,[bp-8] ;Get handle mov ax,5301h ;EMS Set Handle Name int 67h ;Call EMS Driver or ah,ah ;Check for error jne getp_exit ;If error, exit getp_exit: mov [bp-1],ah ;Save return code on stack pop es pop si pop di pop dx pop cx pop bx pop ax pop bp ret GetEMSMem endp ;==================================================================== ;SaveEMSMem Saves the context of EMS driver ; Entry: CX - Size of save area in bytes ; ES:DI - Pointer to save area ; Exit: AH - Return code ; DX - Index, used to identify saved state ;==================================================================== SaveEMSMem proc push bx push cx push di mov ax,4e03h ;Get save area size int 67h xor ah,ah ;Clear top byte of save size add ax,4 mov bx,di ;Copy buffer pointer cmp word ptr es:[di],4244h ;Check for initialized area je save_1 mov es:[di],4244h ;Load signature xor dx,dx mov es:[di+4],dx ;Init first index dec dx mov es:[di+2],dx ;Init queue end pointer save_1: ;-------------------------------------------------------------------- ;Find the first free area in the save buffer ;-------------------------------------------------------------------- inc di inc di add cx,bx ;Add base address to size but sub cx,ax ;subtract entry size xor dx,dx save_2: cmp dx,es:[di+2] ;Find max value of index jae save_3 ;in the queue mov dx,es:[di+2] save_3: cmp word ptr es:[di],-1 ;If = -1 then this is the je save_4 ;last entry. add di,es:[di] ;Point to next entry jc save_areafull ;Check for end of segment and cmp di,cx ;end of buffer ja save_areafull jmp short save_2 save_4: add di,4 ;Move past queue overhead inc dx ;Create new index push ax mov ax,4e00h ;Save page map int 67h or ah,ah pop ax jne save_exit mov es:[di-4],ax ;Update queue pointers mov es:[di-2],dx add di,ax sub di,4 mov word ptr es:[di],-1 save_exit: pop di pop cx pop bx ret save_areafull: mov ah,8ch ;Save area full jmp short save_exit SaveEMSMem endp ;==================================================================== ;RestoreEMSMem Restore the context of EMS driver ; Entry: DX - Index, value returned by SaveEMSMem when state saved ; DS:SI - Pointer to save area ; Exit: AH - Return code ;==================================================================== RestoreEMSMem proc pushf push cx push di push si mov ax,[si] ;Check for initialized area cmp ax,4244h jne restore_bad inc si ;Point to first entry inc si restore_1: cmp [si+2],dx ;Find index value je restore_2 cmp word ptr [si],-1 ;See if at end of queue je restore_bad add si,[si] jc restore_bad ;If overflow segment, error jmp short restore_1 restore_2: mov di,si ;Copy entry add si,4 ;Point to save array mov ax,4e01h ;Restore page map int 67h or ah,ah jne restore_exit sub si,4 add si,[di] restore_3: cmp word ptr [si],-1 je restore_4 mov cx,[di] ;Load size cld rep movsb ;Copy next entry jmp short restore_3 restore_4: mov word ptr [di],-1 restore_exit: pop si pop di pop cx popf ret restore_bad: mov ah,0a3h jmp short restore_exit RestoreEMSMem endp ;==================================================================== ;FreeEMSMem Deallocates EMS memory ; Entry: DX - handle to deallocate ;==================================================================== FreeEMSMem proc mov ah,45h ;Deallocate pages int 67h ret FreeEMSMem endp ;==================================================================== ;CheckAlias Checks for memory aliasing support by EMS driver ; Exit: AH - return code ; AL - 1 Aliasing not supported, 0 - Aliasing supported ;==================================================================== CheckAlias proc pushf push bx push cx push dx push di mov ah,41h ;Get page frame segment int 67h or ah,ah jne chkalias_exit1 mov cx,bx ;Copy page frame ;Allocate 1 logical page mov ah,43h ;EMS Allocate page mov bx,1 int 67h or ah,ah jne chkalias_exit1 ;Map logical page to physical pages 0 and 1 mov ax,4400h ;Map to page 0 mov bx,0 ;Logical page 0 int 67h or ah,ah jne chkalias_exit mov ax,4401h ;Map to page 1 mov bx,0 ;Logical page 0 int 67h or ah,ah jne chkalias_exit ;Write pattern to physical page 0, then compare to page 1 xor bl,bl ;Clear alias flag push es mov es,cx add cx,400h push cx mov di,100h ;Point 256 bytes inside page cld mov al,5ah ;Write to page 0 then check mov cx,8 ;for aliasing by scanning rep stosb ;for the same pattern on mov cx,8 ;page 1 pop es mov di,100h repe scasb pop es je chkalias_exit inc bl chkalias_exit: mov ah,45h ;Deallocate pages int 67h mov al,bl ;Copy alias flag chkalias_exit1: pop di pop dx pop cx pop bx popf ret chkalias_error: mov ax,8000h ;Indicate SW error jmp short chkalias_exit1 CheckAlias endp ;==================================================================== ;MapbyName Map expanded memory pages by name ; Entry: AL - Physical page ; BX - Logical page to map ; DS:SI - Pointer to Handle name in ASCIIZ format ; Exit: AH - Return code ; DX - EMS Handle ;==================================================================== MapbyName proc push cx mov cx,ax ;Save physical page number mov ax,5401h ;EMS find handle function int 67h ;Call EMS driver or ah,ah ;Check for error jne mbn_exit ;If error, exit mov al,cl ;Get back physical page mov ah,44h ;EMS Map page function int 67h ;Call EMS driver pop cx mbn_exit: ret MapbyName endp ;==================================================================== ; FindEMS determines if the EMM is loaded ;==================================================================== FindEMS proc call check_ems_hdl ;Check for driver using jnc find_1 ;open handle technique rcl ah,1 ;If not enough handles, jc find_driver_error ;try interrupt technique call check_ems_int jnc find_1 ;If manager found, continue find_driver_error: mov ah,0f1h ;Set error code jmp short find_exit find_1: mov ah,40h ;Check status int 67h find_exit: ret FindEMS endp ;==================================================================== ; Test for EMS driver (Open Handle Technique) ;==================================================================== check_ems_hdl proc mov dx,offset ems_header mov ax,3d00h ;Open device read only int 21h jnc cemsh_device_open cmp ah,4 ;If file or path not found jb cemsh_not_found ;no EMS driver mov ah,01h ;Too many open handles or jmp short cemsh_error ;access denied cemsh_device_open: mov bx,ax ;Copy handle mov ax,4400h ;Get device information int 21h jc cemsh_not_foundclose test dx,80h ;Test file/device bit je cemsh_not_foundclose ;0 = file not device mov ax,4407h ;Get output status int 21h jc cemsh_not_foundclose ;If error, no device cmp al,0ffh ;Check for ready status jne cemsh_not_foundclose ;If not ready, no device mov ah,3eh ;Close handle int 21h ;BX contains handle clc cemsh_exit: ret cemsh_not_foundclose: mov ah,3eh ;Close handle int 21h ;BX contains handle cemsh_not_found: mov ah,81h ;Not found error code cemsh_error: stc jmp short cemsh_exit check_ems_hdl endp ;==================================================================== ; Test for EMS driver (Get Interrupt technique) ;==================================================================== check_ems_int proc push es ;Test for ems driver push di mov ax,3567h ;Get EMS vector int 21h mov di,0ah ;Using the segment from the mov si,offset ems_header ;67h vector, look at offset mov cx,8 ;0ah. Compare the next 8 cld ;bytes with the expected repe cmpsb ;EMS header. If they are pop di ;the same, EMS driver pop es ;found jne cemsi_error clc cemsi_exit: ret cemsi_error: stc jmp short cemsi_exit check_ems_int endp ;==================================================================== ;EMS32Upgrade ; Entry: AX - EMS 4.0 compatible function number ; Other registers configured as needed by the function ; Exit: AH - Return code ;==================================================================== EMS32Upgrade proc far pushf push bp mov bp,sp push ax ;Save function number mov al,EMS32up_version ;Get version or al,al ;See if initialized jne ems32_2 ;Yes, version already found mov ah,46h ;EMS Get version int 67h ;Call EMM or ah,ah ;Check for error jne short ems32_exit mov EMS32up_version,al ;Set version ems32_2: cmp al,40h ;Check for version 4.0 je ems32_call_driver cmp al,32h ;Check for version 3.2 je ems32_3 mov ah,0ffh ;Set bad version error code jmp short ems32_exit ems32_3: mov ax,[bp-2] ;Get function number cmp ah,4eh ;See if function 1 to 15 jbe ems32_call_driver ;If so, call EMM push bx mov bx,offset ems32_return push bx ;Put return address on stack mov bx,ax ;Convert function into mov bl,bh ;jump table index xor bh,bh sub bl,50h or bl,bl je ems32_4 sub bl,4 cmp bl,2 jbe ems32_4 mov ah,84h ;Unsupported function code jmp short ems32_exit ems32_4: shl bx,1 add bx,offset ems32up_calltbl push [bx] ;Push subroutine addr on stack mov bx,[bp-4] ;Get original BX retn ;Call function ems32_return: add sp,2 ;Pull BX off stack jmp short ems32_exit ems32_call_driver: mov ax,[bp-2] ;Restore AX cmp ah,55h ;See if map and jump jne ems32_5 ;If so, clean up stack since add sp,2 ;EMM will not return to mov [bp+4],ax ;this routine mov ax,[bp+2] ;Use AX and flags values to mov [bp+6],ax ;overwrite return address pop bp pop ax ;Clean off extra flags reg pop ax ;Get back AX popf ;Restore Flags ems32_5: int 67h ;Call EMM ems32_exit: add sp,2 ;Clear off original AX pop bp popf ret ;==================================================================== ;EMSMapMultiple Simulates the Map Multiple function of the EMS 4.0 ; driver ; Entry: AL - subfunction. 0 = Use physical page numbers ; 1 = Use physical page segments ; CX - Size of mapping array ; DX - EMS Handle ; DS:SI - Pointer to array of mapping structures (1 structure per ; page) ; Exit: AH - Return code ; ; Page mapping structure: ; log_to_phys_struc STRUC ; log_page_number dw ? ; phy_page_number dw ? ; log_to_phys_struc ENDS ;==================================================================== mapm_pfp equ [bp-2] ;Pointer to page frame EMSMapMultiple proc push bp mov bp,sp sub sp,2 ;Create room for local var push bx push cx push di push si cmp al,1 ;Check for legal subfunction jbe mapm_1 mov ah,8fh ;Subfunction code invalid jmp short mapm_exit mapm_1: xor ah,ah ;Clear return code. or cx,cx ;See if mapping 0 pages je mapm_exit ;If so, exit. xor ah,ah mov di,ax ;Copy subfunction mov ah,41h ;EMS Get Page Frame segment int 67h ;Call EMS driver or ah,ah ;Check for error jne mapm_exit mov mapm_pfp,bx ;Save page frame pointer mapm_loop1: mov ax,[si+2] ;Get physical page or di,di ;See if map by segment or num je mapm_4 sub ax,mapm_pfp ;Sub starting addr of frame jae mapm_3 ;If positive, continue mapm_2: mov ah,8bh ;Page segment illegal RC jmp short mapm_exit ;Error, goto exit mapm_3: push dx ;Save handle xor dx,dx ;Clear high word mov bx,400h ;Divide by segment size div bx or dx,dx ;Check for remainder ;If found pop dx ;the segment was not on a jne mapm_2 ;proper boundary mapm_4: mov bx,[si] ;Get logical page mov ah,44h ;EMS Map page int 67h ;Call EMS driver or ah,ah ;Check for error jne mapm_exit add si,4 loop mapm_loop1 mapm_exit: pop si pop di pop cx pop bx add sp,2 pop bp ret EMSMapMultiple endp ;==================================================================== ;EMSMapandJump Simulates the Map and Jump function of the EMS 4.0 ; driver ; Entry: AL - Subfunction. 0 = Use physical page numbers ; 1 = Use physical page segments ; DX - EMS Handle ; DS:SI - Pointer to map and jump structure ; Exit: AH - Return code ; ; Map and Jump structure: ; map_and_jump_struc STRUC ; target_addr dd ? ; log_phys_map_len db ? ; log_phys_map_ptr dd ? ; map_and_jump_struc ENDS ; ; Page mapping structure: ; log_phys_map_struc STRUC ; log_page_number dw ? ; phy_page_number dw ? ; log_phys_map_struc ENDS ;==================================================================== EMSMapandJump proc push bx push cx push si push ds xor cx,cx ;Clear size mov cl,ds:[si+4] ;Get size of map array lds si,ds:[si+5] ;Get pointer to map array call EMSMapMultiple ;Call MapMultiple to map pages or ah,ah ;Check for error pop ds pop si pop cx pop bx jne mapandjump_exit ;If so, error code in AH. Exit mov ax,ds:[si] ;Copy jump address over return mov [bp+4],ax ;address on the stack mov ax,ds:[si+2] mov [bp+6],ax xor ax,ax ;Zero return code mapandjump_exit: ret EMSMapandJump endp ;==================================================================== ;EMSMapandCall Simulates the Map and Call function of the EMS 4.0 ; driver ; Entry: AL - Subfunction. 0 = Use physical page numbers ; 1 = Use physical page segments ; 2 = Get Stack space size ; DX - EMS Handle ; DS:SI - Pointer to map and call structure ; Exit: AH - Return code ; ; Map and Call structure: ; map_and_call_struc STRUC ; target_addr dd ? ; new_page_map_len db ? ; new_page_map_ptr dd ? ; old_page_map_len db ? ; old_page_map_ptr dd ? ; map_and_call_struc ENDS ; ; Page mapping structure: ; log_phys_map_struc STRUC ; log_page_number dw ? ; phy_page_number dw ? ; log_phys_map_struc ENDS ;==================================================================== EMSMapandCall proc push ax mov ax,4e03h ;Get size of save array int 67h or ah,ah pop bx ;Get back function jne mac_0 cmp bl,2 ;Check for subfunction 2 jne mac_1 add al,24 ;Add additional room on stack mac_0: jmp mac_exit1 ;on the stack mac_1: push si ;Save array pointer push ds push di ;Save registers needed for push es ;save page map call sub sp,ax ;Create room on stack mov di,ss ;Point to save area on stack mov es,di mov di,sp push ax ;Save size of save area mov ax,4e00h ;Save page map int 67h ;Call EMM or ah,ah je mac_2 mac_save_error: pop di ;Get save area size add sp,di ;Remove save area from stack jmp short mac_exit mac_2: mov ax,[bp-2] ;Get original AX for push cx ;subfunction xor cx,cx ;Clear size mov cl,ds:[si+4] ;Get size of new array lds si,ds:[si+5] ;Get pointer to new map array call EMSMapMultiple ;Call MapMultiple to map pages pop cx mov ds,[bp-10] mov si,[bp-8] ;Restore registers or ah,ah ;Check for error jne mac_save_error ;If so, error code in AH mov es,[bp-14] mov di,[bp-12] mov ax,[bp+2] ;Get flags off stack push ax popf ;Restore flags push bp ;Save my BP mov bp,[bp] ;Restore original BP mov ah,0 ;Set return code call dword ptr ds:[si] ;Call user routine pop ax ;Get back BP used by routine xchg bp,ax ;Put returned BP on stack in mov [bp],ax ;place of original BP pushf ;Put all 16 bits of the flags pop ax ;register in AX mov [bp+2],ax ;Save new flag state on stack mov [bp-12],di ;Save new ES:DI mov [bp-14],es mov di,si ;Save new DS:SI push ds pop es mov si,ss ;Point to save area on stack mov ds,si mov si,sp inc si ;Move past save area size inc si mov ax,4e01h ;Restore page map int 67h pop si ;Get size of save area add sp,si ;Remove save area from stack push es pop ds mov si,di or ah,ah jne mac_exit push cx ;Save working registers push si push ds mov si,[bp-8] ;Get pointer to structure off mov ds,[bp-10] ;the stack xor cx,cx ;Clear size mov ax,[bp-2] ;Restore subfunction mov cl,ds:[si+9] ;Get size of old array lds si,ds:[si+10] ;Get pointer to old map array call EMSMapMultiple ;Call MapMultiple to map pages pop ds pop si pop cx mac_exit: pop es pop di add sp,4 ;Clear off old SI and DS mac_exit1: ret EMSMapandCall endp EMS32Upgrade endp end Figure 4 ;==================================================================== ; Sample program for EMS Toolbox Routines ; (c) 1989 Microsoft Corporation ;==================================================================== DOSSEG .MODEL small .STACK 512 .DATA EMShandle dw 0 ;EMS handle pageframe_seg dw 0 active db 0 ;Flag to indicate int9 active key_code db 0 ;Code of key pressed cursor_pos dw 0 ;Saved position of cursor save_index dw 0 save_area db 100 dup (0) save_area_end = $ .CODE EXTRN GetEMSMem:PROC EXTRN SaveEMSMem:PROC EXTRN RestoreEMSMem:PROC int09h dd 0 ;Saved interrupt 9 vector ;-------------------------------------------------------------------- ;Keyboard Interrupt. This routine checks for the hotkey then saves ; or restores the video page as necessary. ;-------------------------------------------------------------------- kbdint proc far assume cs:_text,ds:nothing,es:nothing push ax mov ah,2 ;Get keyboard shift state int 16h cmp al,08h ;See if ALT key pressed jne goto_kbdbios in al,60h ;Read keyboard port cmp al,13h ;See if 'R' key pressed je kbd0 cmp al,2Eh ;See if 'C' key pressed jne goto_kbdbios kbd0: push ds push ax mov ax,@data ;Point DS to data segment mov ds,ax pop ax assume ds:_data mov key_code,al ;Save key value (C or R) cmp active,0 je kbd1 pop ds goto_kbdbios: pop ax jmp cs:int09h kbd1: inc active sti push bx ;Save registers push cx push dx push di push si push es mov di,@data mov es,di ;Point to save area mov di,offset save_area mov cx,offset save_area_end - offset save_area call SaveEMSMem ;Save state of EMS driver or ah,ah ;Check for error, if jne kbd_exit ;error, exit mov save_index,dx mov ax,4400h ;EMS Map page 0 xor bx,bx ;Logical page 0 mov dx,EMShandle int 67h or ah,ah ;Check for error, if jne kbd_restore ;error, exit mov ah,0fh ;Get display mode int 10h ;Check for valid modes 2,3,7 cmp al,7 je kbd2 cmp al,2 jb kbd_restore cmp al,3 ja kbd_restore kbd2: push ax mov ah,3 ;Save cursor position int 10h mov cursor_pos,dx pop ax mov es,pageframe_seg xor di,di ;Saved scr at offset 0 cmp key_code,2eh ;See if capture function je kbd3 mov di,1000h ;Save current scr at 1000h kbd3: call save_scr ;Save current screen call kb_reset ;Reset keyboard controller cmp key_code,2eh ;See if capture function je kbd_restore ;If so, were done push ds mov ds,pageframe_seg xor si,si call restore_scr ;Display saved screen pop ds kb4: xor ah,ah ;Get key int 16h cmp al,27 ;Check for ESC key. If jne kb4 ;found exit. Else get ;another key. push d ;Restore original screen mov ds,pageframe_seg mov si,1000h call restore_scr pop ds kbd_restore: mov si,offset save_area ;Restore EMS mapping ;context mov dx,save_index call RestoreEMSMem mov ah,2 ;Set cursor position mov dx,cursor_pos int 10h kbd_exit: mov active,0 ;Clear active flag pop es pop si pop di pop dx pop cx pop bx kbd_exit1: pop ds pop ax iret kbdint endp ;-------------------------------------------------------------------- ;Save screen ;-------------------------------------------------------------------- save_scr proc near stosw ;Save video mode mov ax,1234h ;Buffer initialized flag stosw mov cx,25 ;25 rows xor dx,dx saves1: push cx mov cx,80 ;80 columns xor dl,dl saves2: mov ah,2 ;Set cursor position int 10h mov ah,8 ;Read char and attribute int 10h stosw ;Store in EMS buffer inc dl ;Point to next column loop saves2 pop cx inc dh ;Point to next row loop saves1 ret save_scr endp ;-------------------------------------------------------------------- ;Restore screen ;-------------------------------------------------------------------- restore_scr proc near lodsw ;Get video mode xor ah,ah ;Set mode cmp word ptr [si],1234h ;See if initialized jne restore_exit int 10h inc si inc si mov cx,25 ;25 rows xor dx,dx ;Start at row 0 restores1: push cx mov cx,80 ;80 columns xor dl,dl ;Start at column 0 restores2: push cx mov ah,2 ;Set cursor position int 10h lodsw ;Get char and attr from buffer mov bl,ah ;Copy attribute mov cx,1 ;Write 1 character mov ah,9 ;Write char and attribute int 10h inc dl ;Point to next column pop cx loop restores2 pop cx inc dh ;Point to next row loop restores1 restore_exit: ret restore_scr endp ;-------------------------------------------------------------------- ;KB_RESET resets keyboard and signals end-of-interrupt to the 8259 ;-------------------------------------------------------------------- kb_reset proc near assume cs:_text in al,61h ;get control port value mov ah,al ;save it in AH or al,80h ;set bit 7 out 61h,al ;send reset value mov al,ah ;get original value out 61h,al ;send to enable keyboard cli ;suspend interrupts mov al,20h ;get EOI value out 20h,al ;send EOI to 8259 sti ;enable interrupts ret kb_reset endp ;-------------------------------------------------------------------- ;Initialization. Reserve EMS memory and hook into keyboard interrupt ;-------------------------------------------------------------------- main: mov ax,@data ;Set DS to data seg mov ds,ax mov bx,1 ;Get 1 page mov si,-1 call GetEMSMem ;Allocate EMS memory or ah,ah jne ems_error mov EMShandle,dx ;Save EMS handle mov pageframe_seg,bx ;Save EMS page frame push es mov ax,3509h ;Get and Set int 9 vector int 21h mov word ptr int09h,bx mov word ptr int09h[2],es mov ax,2509h push ds push cs pop ds mov dx,offset kbdint int 21h pop ds pop es mov dx,ds add dx,10h mov bx,es sub dx,bx mov ax,3100h ;TSR int 21h ems_error: mov al,ah ;Copy EMS return code mov ah,4ch ;terminate with return code int 21h END main A Complete Guide to OS/2 Interprocess Communications and Device Monitors Richard Hale Shaw The protected mode operating environment in the OS/2 operating system allows each program to run in a different session independently of other programs. Yet this same protection mechanism prevents a program from readily sharing data and communicating with other processes. Fortunately, OS/21 offers several interprocess communications facilities (IPC) that allow two processes to signal, synchronize, serialize resources, and transfer data. This article explores OS/2 IPC and introduces OS/2 character device monitors. OS/2 has come a long way. When I wrote what would become the first article in this series, most OS/2 developers were still using OS/2 Version 1.0 or beta copies of Version 1.1 from the Microsoft(R) OS/2 Software Development Kit. With the addition of Microsoft Word Version 5 for OS/2, I have written, developed, and edited this article and its sample programs entirely under OS/2--something I have not been able to do in the past. I can run each tool--word processor, program editor, compiler, test programs--in different Presentation Manager windows, switching between them with a mouse click. This capability is another example of how productive an environment OS/2 can be. In previous articles, I discussed OS/2's protected mode operating environment and how it is organized in screen groups which are composed of one or more sessions. A session and a screen group are usually synonymous. The exception is the Presentation Manager's screen group, which can run more than one session simultaneously. Each session consists of one or more processes (roughly synonymous with programs), each of which has one or more threads of execution. Since each session is independent of the others, a process can run without concern that it will be unnecessarily interrupted by others or that another process will wantonly hang the system. However, this same protection and independence makes it difficult for one process to communicate directly with another one. OS/2 isolates the data of one program from the data of another program. You can't just cast a pointer to a common data area as you would under a real-mode environment like the MS-DOS(R) operating system, therefore sharing data is more complex under OS/2. Fortunately, OS/2 offers a rich set of interprocess communications (IPC) facilities that allow for flexible communications between well-behaved, cooperating programs. In this article, I'll discuss the forms of IPC, then each of the IPC facilities provided by OS/2. I'll also present a sample program for each IPC that will show you how to use them in an application program. And I'll introduce you to OS/2 character device monitors, which allow an application to intercept data from a device driver long before it reaches another application. Under OS/2, you can create pop-up programs with keyboard monitors, a subject that I will also cover. An OS/2 IPC Overview Clearly, there are times when it is advantageous for threads in different processes to communicate. For instance, a process may need to signal another that an event has occurred, or it may need to synchronize its activities with those of another process. At other times, several processes may need to share a resource that only one process at a time may use, or a process may need to transfer data to another process. The most primitive form of IPC is signaling, which is used when a thread in one process needs to notify a thread in another process that an event has occurred. Although you should use semaphores for signaling most of the time, you can also use the OS/2 signaling facility to pass signals between processes. OS/2 signals are crude, but they do allow a process to notify another of an event. Signals are also used to notify an application that the user is trying to terminate the process by typing Control-C or Control-Break. Synchronization, which is required when a thread needs to coordinate its activities with those of another thread, is another form of IPC. Semaphores are the simplest and easiest OS/2 tools to use for this purpose, but you can also use queues and shared memory. Yet another form of IPC is serialization, which allows one thread to ensure that it has exclusive use of, or owns, a resource that only one process can access or use at a time. Semaphores are usually the perfect solution to providing exclusivity as long as every process that wants to use the resource must first gain control of the semaphore. RAM semaphores are used to signal, serialize resources, and synchronize threads in the same process. For interprocess signaling, serialization, and synchronization, you'll want to use OS/2 system semaphores most of the time. (OS/2 also offers Fast-Safe RAM semaphores, which are usually used with dynamic-link libraries.) Interprocess data transfer is another form of IPC. It is used when a thread in one process needs to transfer data to a thread in another process. Of course, data transfer isn't a problem between threads in the same process, since they can access the same global data. OS/2's shared memory, pipes, and queues are available to facilitate interprocess data transfer. Pipes are the easiest to implement and use if the data transfer is going to take place between two related processes (that is, where one process is the parent of the other). Shared memory is slightly more involved, since the application must perform a modest amount of memory management; but it allows you to transfer data between two programs as if each had the same variables in its global data area. Queues are a powerful form of IPC under OS/2; they require more work from your program, but they allow several sender processes to transfer data to a single receiver process. Named pipes, the most powerful form of OS/2 IPC, are almost limitless in their use--even across a network. I've given the discussion of named pipes special treatment in the sidebar "Named Pipes: A Welcome Latecomer." System Semaphores A RAM semaphore is a 4-byte long variable. You can define a semaphore external to any function in the application and make it globally available to all threads in the process. There are no limits to the number of RAM semaphores you can use in an application. Unfortunately, only the threads in the process that defines a RAM semaphore can access it. While you can place a RAM semaphore in shared memory and allow threads in different processes to access it there, you needn't bother: use system semaphores instead. System semaphores, used for signaling, synchronization, or serialization, are the most versatile IPC mechanism that OS/2 offers. Like their RAM semaphore cousins, system semaphores have two distinct states, set and clear. You can use the same application program interface (API) services on system semaphores that you use to manipulate RAM semaphores. Using these services ensures that the action of setting or clearing a system semaphore is a single atomic OS/2 instruction that the OS/2 system scheduler cannot preempt. A thread must always reference a semaphore by means of a semaphore handle. The address of a RAM semaphore is its semaphore handle, so all you have to do is define it and initialize it to 0L to use it. System semaphores must be created with the DosCreateSem function or opened with the DosOpenSem function before they can be used. Then OS/2 will return a handle for the semaphore. Only one process can create a particular system semaphore, but any other process can open the semaphore once it has been created. Thus every process can obtain the semaphore's handle. The naming convention that OS/2 uses for system semaphores is essentially the same one used for shared memory and queues. It requires a pseudo-filename which, in the case of system semaphores, is preceded by a pseudo-directory name, \SEM. For instance, a thread can create a system semaphore called \SEM\SEM0. The \SEM directory doesn't really exist; it's a means of identifying SEM0 as a system semaphore. To create a system semaphore, a thread must pass the semaphore's name to the DosCreateSem function, which returns the semaphore handle. The creating thread must specify whether other processes will be able to change or only monitor the semaphore. Then other processes can call DosOpenSem, which will open the semaphore and return a handle for them to use. Note that OS/2 Version 1.1 allows a maximum of 256 system semaphores at any time, so be judicious in their use. When a thread is finished with a system semaphore, it should call DosCloseSem to close it. If the thread owns the system semaphore at the time, it should always clear the semaphore before closing it. Closing the semaphore tells OS/2 that the thread is no longer using it. OS/2 will return an error if the thread attempts to use a system semaphore after closing it, even if the thread was the one that created the semaphore. Once the last thread closes it, OS/2 will discard the semaphore. As already mentioned, a thread can use a system semaphore handle with any of the API functions used for manipulating RAM semaphores. Of those functions, DosSemClear and DosSemSet are best for signaling, synchronization, and mutual exclusion. DosSemRequest will cause the calling thread to wait, or block, while another thread owns the semaphore. Once its owner clears the semaphore with DosSemClear, DosSemRequest will set the semaphore and the blocked thread will unblock and return. There are three additional functions that you can use to make a thread wait for a change in a semaphore's state. These functions allow a thread to synchronize its actions with the actions of another thread. DosSemWait will wait for a thread to call DosSemClear and clear a semaphore (like DosSemRequest does except DosSemWait doesn't set the semaphore). DosSemSetWait does the same thing but sets the semaphore before it begins to block. DosMuxSemWait lets you construct a list of semaphores and returns when any semaphore in the list has been cleared. Sample Semaphore SSEM.C (see Figure 1) demonstrates the use of system semaphores. It shows how to create or open a system semaphore and how to use it with DosSemWait, DosSemRequest, DosSemSet, and DosSemClear. It also illustrates a simple example of synchronization between processes. What's unique about SSEM is that to use it, you should run two or more instances of it in separate sessions. The first instance will always create the system semaphore, set it, and enter a program loop. Each time the program goes through the loop, it prints a message and clears the semaphore. Then it looks to see if the operator has pressed a key and, if so, closes the semaphore and terminates itself. If the operator has not pressed a key, the process sleeps for a while and requests ownership of the semaphore. When the semaphore becomes available, SSEM begins the loop again. A subsequent instance of SSEM, unable to create the semaphore because it already exists, will open the semaphore instead. Then it will wait until some other instance of SSEM clears the semaphore and enters the loop described above. Thus multiple instances of the same program will take turns owning the semaphore. The program doesn't really use the semaphore to protect a resource, but using the semaphore does force the program to indirectly serialize its use of the OS/2 video resources. Only one instance of the program can write its message at a time--when the semaphore is available. This sample should give you a better idea of how system semaphores work. Anonymous Pipes UNIX system developers originally introduced pipes as a form of operating system IPC. Pipes get their name from their resemblance to a water pipe, in which water transfers in one direction from one location to another. In an operating system context, a pipe allows a process to send data to another process, as if the first is writing to a file that the second is reading. You can also use pipes for redirection, since, in many cases, STDIN and STDOUT are indistinguishable from file handles. OS/2 anonymous pipes are a very efficient, versatile means of interprocess data transfer and demand little from the programmer or the application. While anonymous pipes are flexible to use, their application is limited: you can only use them between related processes, where one process is the parent (or an ancestor) of another. Thus one process can create an anonymous pipe and start or spawn another process that can read from it or write to it. I will discuss how to use anonymous pipes, but move on rather quickly to more powerful forms for transferring data in IPC. A pipe is a first in, first out (FIFO) stream in which one process inserts data at one end and another takes the data out at the other end. It resembles a file to the processes that use it but it is actually a memory buffer that OS/2 manages. A process can write data to a pipe and another process can read data from the pipe as if either were manipulating a disk file. Indeed, several processes can write data to the pipe, but only one process can read from it. OS/2 manages the pipe and handles the scheduling and synchronization of the data. Under OS/2, pipe handles, like file handles, can be inherited by a child process. Thus, a parent process can pass a pipe handle to a child process as a command line parameter or via shared memory and the child can use the handle. The parent process can also use a pipe to read the child's standard output or write to its standard output. This is done by closing either the standard input or output handle and substituting a pipe handle in its place. When the child uses either standard input or standard output, it will actually be using the pipe handle. For instance, a parent process can call DosClose to close STDOUT. By calling DosDupHandle, the parent process creates a duplicate of the pipe's write handle and forces it to be recognized as STDOUT. When the child process begins, all output that it writes to STDOUT will actually go to the pipe where the parent process can read it. This will become clearer to you in the PIPE sample program. To create an anonymous pipe, a process must call DosMakePipe. This API function returns two handles to the pipe, one for reading and one for writing. The receiver can pass one handle to a child process as a command line parameter or by means of shared memory (see the section on shared memory below). Then one process can use DosRead to read from the pipe and the other can use DosWrite to write to it. DosWrite will not return until the data passed to it has been written to the pipe, so it may have to wait until a DosRead by the receiver has made enough room in the pipe for it to complete its task and return. There are a few additional differences between pipes and files other than that pipes do not involve disk I/O: for one, a process cannot seek in a pipe, but it doesn't have to maintain a file pointer. For another, an application needn't worry about whether the data in the pipe will be overwritten, since OS/2 manages the pipe. Also, a pipe is inherently faster than a disk file since OS/2 stores the entire pipe in memory. On the other hand, although a file is limited only by the available disk space, a pipe can never be larger than 64Kb; the creating process can suggest a pipe size to OS/2 when it calls DosMakePipe, but OS/2 may dynamically change the size of the pipe as it is used. An application doesn't have to worry about filling up the pipe unless data is being read from the pipe too slowly or written to the pipe too quickly. Alternatively, a thread can use DosWriteAsync and DosReadAsync so that it won't be blocked while the pipe is full or being written to. But these functions create their own threads and require a huge stack to accommodate any combination of API calls. It's more reasonable to create your own thread that performs these operations asynchronously using calls to DosWrite or DosRead rather than using DosWriteAsync and DosReadAsync. Since data is written to a pipe in FIFO order, it is up to the processes involved to synchronize themselves. If the data written to the pipe varies in length, your applications will have to work out some sort of protocol, or procedure for knowing how to interpret the data. Once an application finishes using the pipe, it should close it with DosClose. When all the processes using the pipe have closed their pipe handles, OS/2 will delete the resources used to manage the pipe. Sample Pipe Program PIPE.C (see Figure 2) illustrates a simple use of an anonymous pipe. The program also demonstrates a technique that an application might use when it wants to start another process in the same session and give the operator the ability to monitor the new process's output. When you run PIPE, it divides the screen into two halves (upper and lower) and displays the child program's output in the upper half while interacting with the operator in the lower half. It does this by closing the STDOUT handle and substituting it with a pipe handle. Thus, the child process thinks it is writing to STDOUT, when its output is actually going to an anonymous pipe. The parent process uses a separate thread to read the pipe and display the output in the lower half of the screen. To run PIPE, enter PIPE <programname> [args...] and follow PIPE with the name of the program (and any arguments) whose output will be displayed in the upper half of the screen. PIPE begins by creating an anonymous pipe and closing STDOUT. Then it calls DosDupHandle to duplicate the pipe's write handle as the new STDOUT. Using the pipe handle as the new STDOUT does not hinder PIPE itself, since it uses VIO for its own video output. But if the child program uses VIO or STDERR, they will not be transferred to PIPE for printing on the screen. Next, PIPE clears the screen and divides it in two (by using VioWrtNCell to draw a line horizontally across the center of the screen). Then it creates a buffer that includes both the program to run and its arguments and calls DosExecPgm to run the child process asynchronously, allowing the child and PIPE to run at the same time in the same session. After this, PIPE creates a new thread of execution to read the pipe input and print it on the screen. Contained in the function readchild, this thread enters a program loop where it uses a call to DosRead to read from the pipe. When DosRead returns, the thread strips the pipe input of carriage returns and line feeds and scrolls the upper portion of the screen up as it prints each line. If it finds any incomplete lines, it moves them to the beginning of the buffer, sleeps briefly, and returns to the DosRead at the top of the loop. Simultaneously, the main thread also enters a loop, where it calls KbdStringIn to read a line from the user. As the thread gets each line, it scrolls the lower half of the screen up, prints the line, and returns to the top of the loop. If the line typed was QUIT, the thread breaks out of the loop. Then it calls DosCwait to see if the child process has completed. If not, it calls DosKillProcess to terminate the child process and terminates itself. You can use PIPE with any program that writes its output to STDOUT. I've written a simple test program, TEST.C, for testing the PIPE program. TEST.C, which can be downloaded from any of the MSJ bulletin boards, prints a list of messages to STDOUT in an infinite loop. Shared Memory Shared memory is an OS/2 IPC mechanism that allows more than one process to use the same memory segment at the same time. It's useful for interprocess data sharing and data transfer, since one process can transfer data to another by copying it to a shared memory segment. Then another process can read it and modify it, and the original process can read it back. An unlimited number of processes can access shared memory, all of whom can read from or write to the shared memory segment. Other than a small amount of code required to set up shared memory among processes, a program can access a shared memory segment by means of a far pointer, making shared memory access relatively transparent to an application. This flexibility usually demands some method for establishing mutual exclusion; otherwise one process may overwrite the data of another. Because of this you should usually plan on using a system semaphore to manage each shared memory segment. There are three ways of establishing shared memory. In each one, a process--the owner of the shared memory--allocates a shared memory segment that a second process will be able to access. Using the first method, the owner process gives the second process access to the shared memory by creating a memory selector that the second process can use. This is known as givable memory; it requires that the owner know the process ID of the second process in order to create the new selector. Under the second approach, known as getable memory, the owner passes its own memory selector to the second process, which can then get its own selector and access the memory segment. An alternative technique involves named shared memory. Under this scheme, a process can create a shared memory segment that is associated with a specific name. Then any process that knows the name of the shared memory can access it directly. Of the three approaches to shared memory, I think named shared memory is the most convenient and flexible to use. However, the first two methods are terrific for limiting shared memory access to a few specific processes and work particularly well with queues. To use unnamed shared memory, a process must first call DosAllocSeg and allocate the shared segment, specifying the segment size and whether the segment is shareable. DosAllocSeg will return a selector to the segment. OS/2 relates selectors and processes, so that one process cannot use another's memory selector. Thus if the process knows the process ID of the second process, it can create givable shared memory by passing the selector and ID to DosGiveSeg, which will return a new selector the other process can use. Finally, the process must pass this selector to the other process, as a command line parameter or using some other mechanism. Queues lend themselves perfectly to this scheme, since you can create a pointer from the shared memory selector and pass the pointer through the queue. Or the first process can pass the original selector to the other process, which can use DosGetSeg to access the unnamed shared memory. If your application doesn't know the ID of the other process, then you will want to use named shared memory. With named shared memory, your application uses a name for the shared memory segment which resembles the pseudo-filename used by system semaphores. An example would be \SHAREMEM\MEM; the SHAREMEM pseudo-directory name indicates that a shared memory segment is being created. The application passes this shared memory name to DosAllocShrSeg, which returns a selector to the shared segment. As with system semaphores, any other process can open a shared memory segment once it has been created. The other processes must call DosGetShrSeg, which will return a selector to the named shared segment. Under either scheme, a process should call DosFreeSeg to free the shared memory once it has finished with it. OS/2 will not discard the segment until the last process using it has called DosFreeSeg. As with system semaphores, a process can continue to access a shared segment even after the segment is free and the process that created the segment has terminated. Once a process has obtained the selector to a shared memory segment, it can convert the selector into a far pointer using the MAKEP macro and access the shared memory directly. Shared Memory Program SHMEM.C (see Figure 3), a sample shared memory program, uses named shared memory. There is no need for a program for unnamed shared memory, since the sample program for queues serves adequately for both. As with the sample system semaphore program, you can run multiple copies of the shared memory program. Each instance of the program competes for access to a piece of named shared memory by using a system semaphore. Once an instance of the program gains access to this memory, it looks to see if another instance of the program has placed a message there and, if so, prints it. Then it writes its own message to the shared memory segment, releases the semaphore (and ownership of the shared memory), and blocks until it can gain access to the semaphore again. As with SSEM, an instance of SHMEM will terminate if you press a key while its session is in the foreground. You can run more than one instance of SHMEM by running them in different sessions. Each instance prints messages left by other instances, but only one will create or own the shared memory. The messages identify the owner and other users of the shared memory, as well as their process IDs. Queues Queues are one of OS/2's most powerful and complex IPC mechanisms for data transfer. They are more flexible than anonymous pipes and potentially more powerful than shared memory. Queues are usually used in conjunction with shared memory and provide a framework that allows you to easily implement a shared memory manager. Used properly, they eliminate a lot of the work associated with shared memory data transfers. Traditionally, queues are FIFO mechanisms in which one process inserts data at one end and another process takes it out at the other. In such cases, a process reading the queue must accept elements in the order in which they are written to it. OS/2 queues, on the other hand, can be ordered on a FIFO, last in, first out (LIFO), or priority basis. With priority order, the sender can assign a priority number to each element. The element with the highest priority is always first in the queue. If two elements have the same priority, they are placed in the queue in FIFO order. In addition, multiple processes can send data to the queue, whereas only one process--the queue owner--can read from it. Unlike anonymous pipes, queues can be accessed by unrelated processes, although a process must know the name of the queue to access it. A process writes data to a queue as a distinct set of data elements. The sender, not the receiver, controls the size of the data it receives. You will typically use shared memory to transfer data via a queue: the queue transfers elements that contain information about the data being transferred. A sender will pass a pointer to the shared memory as part of a queue element. The receiver can then use this pointer to access the shared memory data. A receiver or a queue owner has far more control over the queue elements than does the receiving process of a pipe. It can remove the queue elements in almost any order and can choose to leave some elements in the queue while retrieving others. In addition, the queue owner can decide to free the shared memory when it wishes or keep it around for a while. Unlike anonymous pipes, queues require their own set of API functions for sending and receiving data: you cannot use DosRead and DosWrite with queues. OS/2 limits queues to 409, with a total of 3268 data elements at a time. Queues are relatively easy to use, but there are a few steps involved in accessing them. First, the receiving process (the queue owner) must create the queue with DosCreateQueue, specifying the name of the queue and the queue order. DosCreateQueue will then return a queue handle. The queue name is a pseudo-filename like those used for system semaphores and named shared memory. An example is \QUEUES\QNAME. Once the queue owner creates the queue, other processes can open it via DosOpenQueue, which will return a queue handle. This function also returns the process ID of the queue owner, so that a sender can create a shared memory selector for the queue owner. When a process is finished with the queue, it should close it using DosCloseQueue. OS/2 will maintain the queue as a resource until the owner closes the queue, at which time it disposes of the queue and any elements remaining in it. Attempts by a process to write to the queue after that will fail. A sender can reopen a queue after closing it as long as the queue owner has not closed the queue. As I mentioned earlier, queue data elements are typically stored in shared memory segments. There are, however, several ways that an application can pass data through a queue. The most common approach is to use unnamed shared memory, with the sender allocating a segment every time it places an element in the queue (these are segments the size of a queue element, not 64Kb segments). The sender writes data to the segment, creates a selector to the shared memory that can be used by the queue owner, and converts the selector into a pointer. Then it calls DosWriteQueue, passing it a priority value and the pointer to the shared memory. The sender can then free the shared memory segment if it no longer needs it, since OS/2 will not discard the segment until the queue owner frees it. Alternatively, the application can allocate one shared memory segment and divide it into pieces that the sender and queue owner will reuse. With this approach, the sender passes a pointer to the portion of the shared memory segment where the data resides, rather than a pointer to the segment itself. Although it is more work for the programmer to create a scheme to manage portions of the shared segment, the resulting program should run faster, since the constant allocation and reallocation of memory found in the first scheme is absent. There is, however, a way to pass data via a queue without using shared memory. If the sender is only transferring small pieces of data that are no more than 2 bytes in size (such as a series of tokens or even selectors), the application can forgo the use of shared memory entirely. The DosWriteQueue function includes a 2-byte request code that OS/2 reserves for use by the application. An application can use this code to pass 2-byte data elements through the queue to the receiving process without implementing either of the two shared memory schemes outlined above. Regardless of the method you use, the receiver must use DosReadQueue to read the elements from the queue. This function will return the process ID of the sender, the request code described in the previous paragraph, the length of the data element, a pointer to the data element, and its priority. DosReadQueue can wait until an element is in the queue or return immediately if no data elements are available. While the receiver should typically devote a thread to reading the queue, it can avoid this by providing a semaphore handle to DosReadQueue that OS/2 will clear when data elements are in the queue. The receiver can check this semaphore from time to time to see if a queue element has arrived while it was tending to other cases. The receiver can use DosPeekQueue to examine elements in the queue without removing them, and both sender and receiver can call DosQueryQueue to determine the number of elements in the queue. In addition, the receiver can flush the queue with DosPurgeQueue. Of course, it's the receiver's responsibility to free any shared memory segments passed with each element. Queue Programs The sample queue takes the shared memory example a step farther, moving these applications into the world of client-server relationships. This is why I have written two programs, one for the client and one for the server. The server's job is simple: if it gets a message from a client process, it must print that message. The client, not much more complex than the server, periodically sends messages to the server. The processes communicate the messages using queues and shared memory. If you study the listing for QSERVER.C (see Figure 4), you will see that the server begins by creating the queue. Then, it enters a program loop and blocks on a call to DosReadQueue. Once a queue element is available, the server returns from DosReadQueue and calls DosQueryQueue to get the count of the number of elements in the queue. Next it prints the message contained in shared memory, the element priority, the process ID of the sender, and the element count. Then the server frees the shared memory that the message occupies. If the operator has pressed a key in the server's session, it breaks out of the program loop, closes the queue and exits the program. Otherwise, it returns to the top of the loop and the DosReadQueue call. QCLIENT.C contains the source code for the client process. The client begins by opening the queue. If the server hasn't yet created the queue, the client will sleep for a second and try again. As you can see, the order in which you run the programs doesn't matter. Once the client has opened the queue, it enters a program loop where it will send messages to the server. It begins by allocating a shared memory segment. Then it uses DosGiveSeg and the MAKEP macros to create a pointer to the shared memory that the queue owner--the server process--can use. The client calls DosWriteQueue to place the pointer, the length of the message, and the element priority in the queue. The client immediately frees the shared memory segment, which won't be discarded until the server frees it. Then the client prints a message to show that it successfully placed a message in the queue to the server. If the operator has pressed a key, it closes the queue and exits the program. Otherwise, it increments the priority (or resets it to 0) and sleeps for a while before continuing at the top of the program loop. Although you can run only one instance of the server program, you can run a client program in every other session. If you are willing to forgo the acknowledgment from a client process that it is sending messages, you can run DETACHed instances of clients that can also communicate with the server. In fact, you can DETACH the server, which will invisibly accept messages from several clients. One note about priorities: try pressing Control-S in the server's session while the client processes are generating messages. Then, after a few moments, press another key to get the server restarted. The server will continue with the highest-priority elements first, gradually working down to lower-priority elements until it catches up. Signals OS/2 signals are IPC mechanisms that notify a process that an external event has occurred. Signals are routinely used by OS/2 to communicate with a process. For instance, if a user presses Control-C or Control-Break to terminate a process, OS/2 will generate the signal SIG_CTRLBREAK or the signal SIG_CTRLC. If the process has installed a signal handler, it can field these signals in any manner it wishes. Otherwise, the command processor parent of the process will receive the signal, translate it into the SIGKILLPROCESS termination signal, and pass it to the process. OS/2 may also terminate a process by sending SIGKILLPROCESS to it directly (if you select Close in the System Icon pulldown menu on a Presentation Manager window, the window's command processor will send SIGKILLPROCESS to the application). OS/2 also sends a SIGKILLPROCESS signal to a child process when the parent process uses DosKillProcess to terminate the child. Signals can also be used in OS/2 for simple communications between two applications. OS/2 provides three signal flags, SIG_PFLG_A, SIG_PFLG_B, and SIG_PFLG_C, that an application can use to signal another. Signal handling under OS/2 is similar to interrupt handling under DOS2. Under DOS, an external event generates an interrupt to the CPU. The CPU then executes a predefined interrupt handling routine, which DOS or the machine BIOS provides. A DOS application can substitute its own interrupt handler to process interrupts while the application is running. Then it will (we hope) reset the predefined interrupt handler when it is through or about to terminate. Under OS/2, external events generate signals. OS/2 provides a predefined signal handler for each type of signal, but an application can substitute or register its own signal handler instead. Then the application can reset the original signal handler when it is done. However, since a signal handler is specific to an application, it doesn't matter if the application doesn't replace the signal handler before terminating. An application can use DosSetSigHandler to set up its own signal-handling routine by specifying the address of the signal handler function and the type of signal to be trapped. An application must register a signal handler with OS/2 for each type of signal. If the process has not registered a signal handler by the time the signal is generated, OS/2 will pass the signal handler to the default signal handler--usually the command processor for the session, either CMD.EXE or the Presentation Manager shell (depending how the application was started). When OS/2 passes a signal to an application that has registered its own signal handler for that signal, it switches the application's main thread to the code of the signal-handling routine. Since signals are time-critical events that require an immediate response, OS/2 will interrupt the main thread even if it is executing a critical section of code (that is, one defined by a call to DosEnterCritSec and a subsequent call to DosExitCritSec). If the main thread is waiting for the return of an API function when it is switched to the signal handler, OS/2 will force the thread to abort the call rather than wait for the function to complete. Once the main thread has processed the signal, OS/2 will switch it back to its own code. If an API function is aborted, the function call will return an error code. The exceptions are API functions that perform file I/O: OS/2 places calls to these functions in a blocked state when they are called. Thus OS/2 can switch the calling thread back to the middle of an API file I/O function without aborting the function call. The lesson to be learned from these applications is that if you are writing an application that uses a signal handler, do not use the main thread for any work that would be corrupted if a signal interrupts it. To be safe, have the main thread start one thread of execution that executes the remaining code and starts any other threads. Then the main thread should install the signal handler and indefinitely block on a call to DosSleep. You can use DosHoldSignal to have the main thread temporarily ignore a signal while it is performing some uninterruptible processing, but you should not do this as a general practice. An application should never install a signal handler that will ignore SIG_CTRLC, SIG_CTRLBREAK, or SIGKILLPROCESS, even if it ignores the signal flags. For instance, if the signal handler ignores SIGKILLPROCESS, you will not be able to terminate it without rebooting your machine. Upon receiving any of these three signals, the process should free its resources, notify any child processes to terminate, and terminate itself. You can use DosExitList to ensure that this occurs properly. A well-written OS/2 application can, however, use the three signal flags for communicating with other processes. These are not flags that communicate a state (as semaphores do), nor are they flags that a process can manipulate. Instead, these flags signal a process that something has happened and allow it to act accordingly. If you are writing an application that will handle signals, you can install a signal handler with DosSetSigHandler. This function can specify five different ways of reacting to a specific signal. The process can: ■ replace the current signal handler with another ■ acknowledge the signal ■ return an error to the sender (indicating that the receiver has refused to accept the signal) ■ ignore the signal ■ remove the signal handler To call DosSetSigHandler, your program must specify the signal, the action to take, and the address of the signal handler function being installed. If you are supplying the latter, DosSetSigHandler will return the address of the current signal handler and its action, so that your application can restore these later. The signaling process should use DosFlagProcess to signal the receiver process, but to use this function it must know the process ID of the receiver. In addition, DosFlagProcess allows the sender to send a 2-byte unsigned value to the receiver. The content of this argument is left to the discretion of the application; this is not unlike the 2-byte value that a queue sender can pass to a queue owner. Once the signal has been generated, OS/2 will preempt the receiver's primary thread and direct it to execute the code of the new signal handler. The signal handler should immediately issue a call to DosSetSignalHandler to acknowledge the signal. This action resets the signal handler and allows the process to accept the signal again (the receiver can't receive that signal again until it has cleared the signal handler). If the signal handler does not acknowledge the signal, DosFlagProcess will return an error code to the sender indicating that the receiver refused to accept the signal. Once the receiver process no longer needs to handle the new signal, DosSetSignalHandler should be used to restore the previous signal handler. The signal handler routine always has the same form: void APIENTRY sighandler (USHORT arg, USHORT num); (This line was broken due to space limitations, but should run on one line--Ed.) You can use any name you wish for the function and arguments, but you must declare the function this way. OS/2 will supply the contents of the two arguments. You never actually call the signal handler function in your code--when your program receives the appropriate signal, OS/2 will set up the stack as if the function had been called and jump to the first line of code inside the function. The first argument specifies the type of signal that was sent. If it is a flag signal, the second argument will be the discretionary value supplied to DosFlagSignal by the sender. Otherwise, OS/2 will set this argument to 0. When the receiver's signal handler is invoked, OS/2 will load the CS, IP, SS, SP, and flag registers with new values to represent the code and stack for the signal-handling routine. The signal handler should terminate with a far return so that OS/2 can resume execution of the interrupted thread. Fortunately, if you are writing your application in C, this process is transparent to the application and your code, since the function was declared with the APIENTRY type modifier. OS/2 will execute the far return for you when it reaches the end of the function. What happens if your program receives a signal while the signal handler is active? If the receiver's signal handler hasn't acknowledged or cleared the signal yet, DosFlagProcess will return an error to the signal-sending process. If the receiver's signal handler has acknowledged the signal but is still active (that is, execution has not returned to the primary thread), the OS/2 error handler will trap the second occurrence of the signal. In other words, the receiver will not know that the signal was sent, and DosFlagProcess will not return an error to the sender (not yet, at least). However, if the sender generates the signal a third time while the receiver's error handler is still active after acknowledging the first signal, DosFlagProcess will return ERROR_SIGNAL_PENDING to the receiver. Consequently, OS/2 can only handle two signals per process at a time. Sample Signal Program The sample SIG.C program (see Figure 5) shows you how to create a process that can signal another process. In keeping with the sample semaphore and shared memory programs, I have used one body of source code to create a program that you can run simultaneously in different sessions. The first instance of SIG will always be the receiver. When you run it, SIG will display its process ID and attempt to create a system semaphore. If successful, it will start a keyboard thread. The keyboard thread blocks on a call to KbdCharIn and terminates the process immediately upon its return. The user can then terminate the receiver instance of SIG at any time by pressing a key. Since the KbdCharIn call is in a separate thread, the main thread can install the signal handler and then sleep indefinitely. This eliminates the possibility of the keyboard call being interrupted by a jump to the signal handler code. After the main thread has started the keyboard thread, it installs the signal handler. The signal-handling routine will process all signals except the one used to detect a broken pipe (DosSetSigHandler will return an error if you attempt to install a signal handler for SIG_BROKENPIPE without opening the pipe first). Once it has installed the signal handler, the main thread enters a loop in which it sleeps for 10 seconds , wakes up, and sleeps again. To generate some signals, you must run at least one other instance of the program in a different session. When you run a signaling instance of SIG, you must pass it the process ID of the receiver instance as a command line parameter. Therefore, if the receiver prints a process ID of 50 when you start it, run each subsequent instance with: SIG 50 After printing its own process ID, each signaler attempts to create the semaphore. When the signaler fails (since the receiver has already created the semaphore) it begins to execute the sender code in the program. It reads the receiver's process ID from the command line and enters the program loop, where it calls DosFlagProcess repeatedly to signal the receiver, switching to a different signaling flag each time, and prints a message to the screen with each pass. As it goes through the loop, it checks for any keyboard input and terminates if the user has pressed a key. Then it sleeps briefly before returning to the top of the loop. The receiver process remains idle until it receives a signal. When it receives a signal, OS/2 switches the main thread to the code of the signal handler function, sighandler. This function immediately acknowledges the signal with a call to DosSetSigHandler, prints a message identifying the signal, and prints a subsequent message showing whether it successfully acknowledged the signal. Then the function returns and OS/2 switches the thread back to its own code. Note that you can't terminate the receiver with a Control-C or Control-Break. The receiver will acknowledge these signals but refuse to terminate. You can, however, end the program by selecting Close from the OS/2 Presentation Manager system icon or by selecting Task/Close from the Presentation Manager Task Manager window. If you do this, the program will print a message acknowledging the signal before terminating. You can also end the program by pressing a key when the receiver's session is in the foreground. You might want to experiment with running several instances of SIG, each signaling the first instance. This experiment should give you a good idea of how signaling works and how to use it. Finally, I should mention that although signaling is fast, the system semaphore is the preferred means of setting a flag that can be used by two different processes. If speed is critical and the other caveats associated with signals are not a problem, by all means go ahead and use them. But for most signaling, system semaphores are a more elegant solution. Device Monitors Character device monitors are not a form of OS/2 IPC. They do, however, allow an application to monitor and manipulate the data sent to and from a character device. In this way you can create a process that can monitor or modify the data stream between a device driver and its associated subsystem, whether or not the monitor process is in the foreground session. While monitors are not a part of OS/2 IPC, they do allow one application to control and manipulate the data that another application receives from a device. The prototypical example of a device monitor is a keyboard monitor. You can use an OS/2 keyboard monitor to create a program that activates when the user presses a hot key, just as with many of the popular DOS terminate-and-stay-resident programs. I'll limit the discussion of monitors here to what you need to know to create programs that do just that. A device monitor only monitors a device's interaction with the screen group in which you install it. You can, however, install a monitor in more than one screen group at a time. If you install a keyboard monitor in one of the Presentation Manager's sessions, it will monitor the keyboard device stream for the entire Presentation Manager screen group. You can use monitors on any character device, including the keyboard, the mouse, and the printer, but you cannot monitor a block device (like a disk drive) or the asynchronous communications device driver. Usually, you can control only the data stream flowing between the device driver and the API subsystem for that driver. However, a printer monitor will let you control the data flowing in either direction. When you install a device monitor, OS/2 places it in the data stream between the device driver and the buffers used by the associated API subsystem. A keyboard monitor, for example, will receive data from the KBD$ device driver and pass data to the keyboard (KBD) subsystem. If you install more than one monitor, OS/2 will pass the data in the stream from one monitor to the next. This collection of monitors is known as the monitor chain. Together a set of monitors can filter the data placed in a session's API buffer for the device being monitored. There are several steps that a program must take to create and install a character device monitor. The monitor process should issue a call to DosMonOpen first, which will return a device handle for the data stream to be monitored. Then the program should call DosMonReg, which will register the process as a device monitor for the data stream. A call to DosMonReg causes the device driver to create a monitor dispatcher. The monitor dispatcher will divert the data stream from the device driver to the monitor and from the monitor to the API buffer (or the next monitor in the chain); DosMonReg will also register the monitor's input and output buffers with the monitor dispatcher. Once you've registered the monitor, the device driver's monitor dispatcher will automatically place records from the data stream into the monitor's input buffers. The monitor examines the data in its buffers, and can either modifiy it or leave it alone. Then it must efficiently transfer the data from its input buffer to its output buffer by means of calls to DosMonRead and calls to DosMonWrite. The monitor dispatcher will automatically transfer the data to the API buffers or the next monitor in the chain. Note that a monitor can optionally consume a data record if it does not want it passed on to the application. When that happens, the application never receives the data. Because a monitor can affect the device information received by applications, you should take care in designing and writing one. You can use DosMonReg to set the monitor's position in the monitor chain (first, last, or next available). OS/2 will always place the first monitor to register for the first position at the front of the monitor chain. If another monitor subsequently registers for first position, OS/2 will place it after any other monitor that has already registered for that position. Likewise, OS/2 will place the first monitor that registers for last position last, and subsequent monitors vying for that position will be next to last, and so on. If a monitor registers for the next available position, it will be placed after any monitors that requested first position and before any that requested last position. You can usually place a keystroke monitor almost anywhere in the monitor chain so long as other monitors do not remove or modify the data for which it's looking. A monitor should be efficient; it should read from, modify, and write to the data stream as quickly as possible. In addition, a monitor should never take any action that might disrupt the data stream. As a rule, you should dedicate one thread of the monitor process to read to and write from the data stream and another thread to react to the data content. DosMonOpen should be called only once in a program. Therefore if your application is going to use multiple monitors, place the monitor code from the call to DosMonReg through the DosMonRead/DosMonWrite loop in a new thread. The portion of the thread that performs the DosMonRead and DosMonWrite calls should not have to wait for I/O services or perform operations that may block it. If the monitor thread is blocked, the monitor will stop the entire data stream, halting the flow of data from the device driver to every process in the screen group until the monitor thread becomes unblocked. If the monitor hangs, it can hang the entire screen group. A monitor thread's priority should be set to help ensure that the OS/2 task scheduler dispatches the monitor thread before it dispatches any threads accessing the API buffer. The monitor should run at the lower levels of the time-critical (the highest) priority category. If the monitor's priority is too low, it will waste CPU cycles and not deliver data from the device driver fast enough. Then the threads reading the API buffer will block longer than necessary, since they will be waiting for the arrival of the data in the buffer. Note that I said to place the monitor at the lower level of the time-critical priority category. Threads that service the hardware device drivers usually use the top of the time-critical category. The monitor must wait for these threads to deliver data from the device to the monitor chain. If you set the monitor at the same level, it will use too many CPU cycles, since the dispatcher will schedule it on an equal basis with the device driver threads. Furthermore, if the time-critical priority category is overloaded with monitors, the device drivers will not be able to meet the demands of the devices. Keyboard Monitor The sample MONITOR.C program (see Figure 6) shows how you can install a simple keyboard monitor. This monitor examines the data stream from the keyboard device driver and displays a pop-up screen whenever the user presses the Alt-F10 hot key. The pop-up screen waits for another key press, then ends the pop up. If the user presses the Ctrl-F10 termination key, the monitor removes itself from the keyboard monitor chain and terminates itself. MONITOR consists of two threads. The main thread begins by setting a RAM semaphore and launching the monitor thread. The monitor thread uses the RAM semaphore to signal the main thread to create the pop up. After the main thread starts the monitor thread, it enters a program loop and blocks on a call to DosSemRequest. When the monitor thread clears the semaphore, the main thread will become active. It will call VioPopUp to create a pop-up screen, print a message, and enter another program loop. In the next program loop, the main thread repeatedly checks for a key press from the operator and breaks out of the loop if a key is available. If not, the thread sleeps for one second. Each time it goes through the loop, the thread increments a counter; when 20 seconds have passed (or rather, twenty 1-second sleep intervals), the thread will break out of the loop. Once out of the loop, the thread will end the pop-up screen and block on the RAM semaphore again. Using the inner loop allows this thread to wait until a user presses a key without waiting indefinitely while a pop-up screen is active. Although OS/2 will turn over the pop-up session to a pending VioPopUp call after 30 seconds, it makes no sense for the thread to own this resource for more than 20 seconds. If the monitor thread clears the program_active variable and the RAM semaphore, the main thread will break out of the main program loop and then terminate the program. The monitor thread itself is a fairly bland example of a keyboard monitor. First, it calls DosMonOpen to get a device handle for the keyboard device driver. Then the thread calls DosGetInfoSeg to get the program's screen group number, which is needed by DosMonReg. It registers its input and output buffers with DosMonReg and raises the thread's priority with DosSetPrty. Then the thread enters the main monitor loop, reading keyboard data from the KBD$ device driver with DosMonRead and writing the data to the KBD API buffer with DosMonWrite (assuming no other monitors are installed). Between each read and write, the monitor thread checks to see if the scan key for a keystroke is the hot key required to generate the pop-up screen in the main thread. If so, the monitor thread sets a variable, popup_active, clears the RAM semaphore, and returns to the top of the loop. By clearing the RAM semaphore, the monitor thread activates the main thread and uses the popup_active variable to determine whether the pop-up screen is active. If it is active, the monitor thread will pass the key to the API buffers, even if it is the hot key. If the monitor thread discovers the termination key in the keyboard data stream, it clears the program_active variable, breaks out of the monitor loop, and closes the monitor with a call to DosMonClose. Just before the thread terminates itself, it clears the RAM semaphore. Clearing the RAM semaphore causes the main thread to unblock and shut down the process. You can run MONITOR from any session or screen group, but you'll get the best results if you DETACH it so that it runs as a background process. You can also DETACH it from a windowed command prompt in the Presentation Manager's screen group. Doing that allows you to create a pop-up program that will appear even if you are currently using a session in the PM's screen group. OS/2 provides enough forms of IPC--ranging from primitive signaling to sophisticated named pipes--to meet almost any programming requirement. This allows for a great deal of flexibility when developing applications that will need to share data, such as servers and communications programs. The sample programs provided with this article were designed specifically to demonstrate IPC mechanisms. The challenge is to take the code, build on it, and adapt it to fit the needs of your development project. Figure 1 SSEM.MAK # make file for ssem.c # COPT=/Lp /W3 /Zpiel /G2s /I$(INCLUDE) /Alfw ssem.exe: ssem.c ssem.mak cl $(COPT) ssem.c /link /co /noi llibcmt markexe windowcompat ssem.exe SSEM.C /* ssem.c RHS 5/1/89 This program demonstrates the use of system semaphores. To use it, run more than one copy of the program in different sessions with: SSEM */ #define INCL_DOS #define INCL_KBD #define INCL_ERRORS #include<os2.h> #include<stdio.h> #define SEMNAME "\\sem\\ssem.sem" void error_exit(USHORT err, char *msg); PID Os2GetPid(void); void main(void); void main(void) { HSYSSEM semhandle; USHORT retval; int creator = FALSE; KBDKEYINFO kbdkeyinfo; PID pid; unsigned count = 0; pid = Os2GetPid(); /* first process will successfully execute this code and create the semaphore */ if(retval = DosCreateSem(CSEM_PUBLIC,&semhandle,SEMNAME)) { if(retval != ERROR_ALREADY_EXISTS) error_exit(retval,"DosCreateSem"); /* second process will open the semaphore previously created */ else if (retval = DosOpenSem(&semhandle,SEMNAME)) error_exit(retval,"DosOpenSem"); DosSemWait(semhandle,SEM_INDEFINITE_WAIT); DosSemRequest(semhandle,SEM_INDEFINITE_WAIT); } else { /* then first process will wait here until second process executes the code in the loop below */ DosSemSet(semhandle); creator = TRUE; } while(TRUE) /* both processes will continue to execute in this loop */ { printf("%s (%u) %u\n",(creator ? "Creating Thread" :\ "User Thread"), pid,count++); DosSemClear(semhandle); KbdCharIn(&kbdkeyinfo,IO_NOWAIT,0); /* check for key press */ if(kbdkeyinfo.fbStatus & FINAL_CHAR_IN) /* if pressed, break out */ break; DosSleep(150L); DosSemRequest(semhandle,SEM_INDEFINITE_WAIT); } DosCloseSem(semhandle); /* close the semaphore */ DosExit(EXIT_PROCESS,0); /* and exit */ } PID Os2GetPid(void) /* returns process id */ { PIDINFO pidinfo; DosGetPID(&pidinfo); /* get process id */ return pidinfo.pid; /* return it */ } void error_exit(USHORT err, char *msg) { printf("Error %u returned from %s\n",err,msg); DosExit(EXIT_PROCESS,0); } Figure 2 PIPE.MAK # make file for pipe.c # COPT=/Lp /W3 /Zpiel /G2s /I$(INCLUDE) /Alfw pipe.exe: pipe.c pipe.mak cl $(COPT) pipe.c /link /co /noi llibcmt markexe windowcompat pipe.exe PIPE.C /* pipe.c RHS 5/2/89 pipe example: this program will reroute the stdout of a child process to the pipe, then it will print the child's output to the upper portion of the screen while you can type into the lower portion of the screen To run: PIPE <child program name> [child program arguments] */ #define INCL_DOS #define INCL_KBD #define INCL_VIO #define INCL_ERRORS #include<os2.h> #include<mt\stdio.h> #include<mt\process.h> #include<mt\string.h> #define PIPESIZE 80 #define STDIN 0 #define STDOUT 1 #define FAILBUFLEN 128 #define ENV 0L #define THREADSTACK 800 char readbuf[PIPESIZE+5]; HFILE readhandle, writehandle, newSTDOUT = STDOUT; USHORT cell = 0x1f20; char readchild_stack[THREADSTACK]; void error_exit(USHORT err, char *msg); PID Os2GetPid(void); void main(int argc, char **argv); void readchild(void); void main(int argc, char **argv) { USHORT retval, cell2 = 0x1fc4, i; STRINGINBUF stringinbuf; char failbuf[FAILBUFLEN]; RESULTCODES resultcodes; char keybuf[PIPESIZE], *p; PID pid,pidasync; if(argc < 2) /* must have one argument */ error_exit(0,"main"); /* create pipe */ if(retval = DosMakePipe(&readhandle,&writehandle, PIPESIZE)) error_exit(retval,"DosMakePipe"); DosClose(STDOUT); /* close stdout */ /* pipe write handle now stdout */ if(retval = DosDupHandle(writehandle,&newSTDOUT)) error_exit(retval,"DosDupHandle"); /* clear the screen */ VioScrollUp(0,0,0xffff,0xffff,0xffff,(PBYTE)&cell,0); VioWrtNCell((PBYTE)&cell2,80,11,0,0); /* draw line to split screen */ memset(readbuf,0,sizeof(readbuf)); /* clear buffer */ strcpy(readbuf,argv[1]); /* add program name */ if(!(strstr(strupr(readbuf),".EXE"))) /* tack on .EXE */ strcat(readbuf,".EXE"); p = (&readbuf[strlen(readbuf)]+1); for( i = 2; i < argc; i++) /* add program arguments */ { strcat(p,argv[i]); strcat(p," "); } /* run the child program */ if(retval = DosExecPgm(failbuf,FAILBUFLEN,EXEC_ASYNC, readbuf,ENV,&resultcodes, readbuf)) error_exit(retval,"DosExecPgm"); pidasync = resultcodes.codeTerminate; /* start pipe read thread */ if(_beginthread(readchild,readchild_stack, THREADSTACK,NULL) == -1) error_exit(-1,"_beginthread"); while(TRUE) { stringinbuf.cb = sizeof(keybuf)-1; /* await operator input */ if(retval = KbdStringIn(keybuf,&stringinbuf,IO_WAIT,0)) error_exit(retval,"KbdStringIn"); keybuf[stringinbuf.cchIn] = '\0'; /* scroll screen up */ VioScrollUp(12,0,23,0xffff,1,(PBYTE)&cell,0); /* write command */ VioWrtCharStr(keybuf,strlen(keybuf),23,0,0); /* clear prompt line */ VioWrtNCell((PBYTE)&cell,80,24,0,0); if(!strcmp(strupr(keybuf),"QUIT")) /* if QUIT command, get out */ break; } /* see if child finished */ if(retval = DosCwait(DCWA_PROCESSTREE,DCWW_NOWAIT, &resultcodes,&pid,resultcodes.codeTerminate)) { if(retval == ERROR_CHILD_NOT_COMPLETE) /*if not, kill it*/ { if(retval = DosKillProcess(DKP_PROCESSTREE, pidasync)) error_exit(retval,"DosKillProcess"); } else if(retval != ERROR_WAIT_NO_CHILDREN) error_exit(retval,"DosCwait"); } DosExit(EXIT_PROCESS,0); /* get out */ } void readchild(void) /* this thread reads the pipe, and prints the lines */ { USHORT retval,bytesread,i, lines; char *p = readbuf; while(TRUE) { /* read the pipe */ if(retval = DosRead(readhandle,p, PIPESIZE-(p-readbuf),&bytesread)) error_exit(retval,"DosRead"); bytesread += (p-readbuf); for(i = lines =0; i < bytesread; i++) /* remove CRs and LFs */ { if(readbuf[i] == '\n') lines++; if(readbuf[i] == '\r' || readbuf[i] == '\n') readbuf[i] = '\0'; } /* print each line found */ for(p = readbuf, i = 0; i < bytesread && lines; lines--) { VioScrollUp(0,0,10,0xffff,1,(PBYTE)&cell,0); VioWrtCharStr(p,strlen(p),10,0,0); i += strlen(p); p += strlen(p); for( ; !(*p) && i < bytesread; i++, p++); DosSleep(1L); } if(*p && !lines && i < bytesread) /* if anything is leftover */ { memmove(readbuf,p,bytesread-i); /*move it up*/ p = &readbuf[bytesread-i]; } else p = readbuf; } } PID Os2GetPid(void) /* returns process id */ { PIDINFO pidinfo; DosGetPID(&pidinfo); /* get process id */ return pidinfo.pid; /* return it */ } void error_exit(USHORT err, char *msg) { char buf[100]; sprintf(buf,"Error %u returned from %s\r\n",err,msg); VioWrtTTY(buf,strlen(buf),0); DosExit(EXIT_PROCESS,0); } Figure 3 SHMEM.MAK # make file for shmem.c # COPT=/Lp /W3 /Zpiel /G2s /I$(INCLUDE) /Alfw shmem.exe: shmem.c shmem.mak cl $(COPT) shmem.c /link /co /noi llibcmt markexe windowcompat shmem.exe SHMEM.C /* shmem.c RHS 5/2/89 This program demonstrates the use of shared memory. To use it, run more than one copy of the program in different sessions with: SHMEM */ #define INCL_DOS #define INCL_KBD #define INCL_ERRORS #include<os2.h> #include<stdio.h> #include<memory.h> #define SHAREDMEM "\\sharemem\\shmem.mem" #define SEMNAME "\\sem\\ssem.sem" #define SHAREDMEMSIZE 200 void error_exit(USHORT err, char *msg); PID Os2GetPid(void); void main(void); void main(void) { HSYSSEM semhandle; SEL selector; USHORT retval; int creator = FALSE; KBDKEYINFO kbdkeyinfo; PCH p; PID pid; pid = Os2GetPid(); /* first process will successfully execute this code and create the semaphore */ if(retval = DosCreateSem(CSEM_PUBLIC,&semhandle,SEMNAME)) { if(retval != ERROR_ALREADY_EXISTS) error_exit(retval,"DosCreateSem"); /* second process will open the semaphore previously created */ else if (retval = DosOpenSem(&semhandle,SEMNAME)) error_exit(retval,"DosOpenSem"); if(retval = DosGetShrSeg(SHAREDMEM,&selector)) error_exit(retval,"DosGetShrSeg"); DosSemWait(semhandle,SEM_INDEFINITE_WAIT); DosSemRequest(semhandle,SEM_INDEFINITE_WAIT); } else { /* then first process will wait here until second process executes the code in the loop below */ if(retval = DosAllocShrSeg(SHAREDMEMSIZE, SHAREDMEM,&selector)) error_exit(retval,"DosAllocShrSeg"); DosSemSet(semhandle); creator = TRUE; } p = MAKEP(selector,0); /* create pointer to shmem */ if(!creator) memset(p,'\0',SHAREDMEMSIZE); while(TRUE) /* both processes will continue to execute in this loop*/ { if(*p) printf("%s (%u) received: %s\n",\ (creator ? "Owner" : "User"),pid,p); sprintf(p,(creator ? "from %u, the owner of this shared \ memory...Hello!" :"from %u, \ a user of this shared memory...Hello!"),pid); DosSemClear(semhandle); KbdCharIn(&kbdkeyinfo,IO_NOWAIT,0); /* check for key press */ if(kbdkeyinfo.fbStatus & FINAL_CHAR_IN) /* if pressed, break out */ break; DosSleep(150L); DosSemRequest(semhandle,SEM_INDEFINITE_WAIT); } DosCloseSem(semhandle); /* close the semaphore */ DosFreeSeg(selector); DosExit(EXIT_PROCESS,0); /* and exit */ } PID Os2GetPid(void) /* returns process id */ { PIDINFO pidinfo; DosGetPID(&pidinfo); /* get process id */ return pidinfo.pid; /* return it */ } void error_exit(USHORT err, char *msg) { printf("Error %u returned from %s\n",err,msg); DosExit(EXIT_PROCESS,0); } Figure 4 Q.H /* q.h RHS 5/1/89 queue server/client common header file */ #define QUEUENAME "\\queues\\qserver.que" QSERVER.MAK # make file for qserver.c # COPT=/Lp /W3 /Zpiel /G2s /I$(INCLUDE) /Alfw qserver.exe: qserver.c qserver.mak q.h cl $(COPT) qserver.c /link /co /noi llibcmt markexe windowcompat qserver.exe QSERVER.C /* qserver.c RHS 5/1/89 This program demonstrates the use of queues and creates a server process that client processes can pass messages to. To use it, start one instance of this program with: QSERVER Then start multiple instances of QCLIENT.C with: QCLIENT */ #define INCL_DOS #define INCL_DOSQUEUES #define INCL_KBD #define INCL_ERRORS #include<os2.h> #include<stdio.h> #include"q.h" void error_exit(USHORT err, char *msg); void main(void); void main(void) { USHORT retval, qcount = 0; KBDKEYINFO kbdkeyinfo; PCH p; HQUEUE qhandle; QUEUERESULT qresult; USHORT ellength; BYTE priority; if(retval = DosCreateQueue(&qhandle,QUE_PRIORITY,QUEUENAME)) error_exit(retval,"DosCreateQueue"); printf("Server has opened queue, awaiting messages...\n"); while(TRUE) { if(retval = DosReadQueue(qhandle,&qresult,&ellength, (PVOID FAR *)&p,0x0000,DCWW_WAIT,&priority,0L)) { if(retval != ERROR_QUE_EMPTY) /* if error was not from empty queue */ error_exit(retval,"DosReadQueue"); continue; } DosQueryQueue(qhandle,&qcount); printf( "Server: (%u pending) message received from %u, priority %02u: %s\n",qcount, qresult.pidProcess, priority,p); DosFreeSeg(SELECTOROF(p)); KbdCharIn(&kbdkeyinfo,IO_NOWAIT,0); /* check for key press*/ if(kbdkeyinfo.fbStatus & FINAL_CHAR_IN) /* if pressed, break out */ break; } DosCloseQueue(qhandle); DosExit(EXIT_PROCESS,0); /* and exit */ } void error_exit(USHORT err, char *msg) { printf("Error %u returned from %s\n",err,msg); DosExit(EXIT_PROCESS,0); } QCLIENT.MAK # make file for qclient.c # COPT=/Lp /W3 /Zpiel /G2s /I$(INCLUDE) /Alfw qclient.exe: qclient.c qclient.mak q.h cl $(COPT) qclient.c /link /co /noi llibcmt markexe windowcompat qclient.exe QCLIENT.C /* qclient.c RHS 5/1/89 This program demonstrates the use of queues. To use it, you should first start QSERVER.C with: QSERVER Then you can run multiple copies of this program with: QCLIENT */ #define INCL_DOS #define INCL_KBD #define INCL_ERRORS #include<os2.h> #include<string.h> #include<stdio.h> #include"q.h" char *p = "Hello!"; #define MAX_PRIORITY 15 void error_exit(USHORT err, char *msg); PID Os2GetPid(void); void main(void); void main(void) { USHORT retval; KBDKEYINFO kbdkeyinfo; HQUEUE qhandle; BYTE priority = 0; SEL selector,qownersel; PID qowner; PCH ptr; int len; PID pid; pid = Os2GetPid(); while(TRUE) { if(!(retval = DosOpenQueue(&qowner,&qhandle,QUEUENAME))) break; else { DosSleep(1000L); continue; } } printf("Client (%u) has opened queue, now messaging...\n",pid); while(TRUE) { if(retval = DosAllocSeg(len = (strlen(p)+1),&selector, SEG_GIVEABLE)) error_exit(retval,"DosAllocSeg"); ptr = MAKEP(selector,0); strcpy(ptr,p); if(retval = DosGiveSeg(selector,qowner,&qownersel)) error_exit(retval,"DosGiveSeg"); ptr = MAKEP(qownersel,0); if(retval = DosWriteQueue(qhandle,0,len, (PBYTE)ptr,priority)) error_exit(retval,"DosWriteQueue"); DosFreeSeg(selector); printf("Client (%u) sent message, priority %u\n", pid,priority); KbdCharIn(&kbdkeyinfo,IO_NOWAIT,0); /* check for key press */ if(kbdkeyinfo.fbStatus & FINAL_CHAR_IN) /* if pressed, break out */ break; if(++priority > MAX_PRIORITY) priority = 0; DosSleep(50L); } DosCloseQueue(qhandle); DosExit(EXIT_PROCESS,0); /* and exit */ } void error_exit(USHORT err, char *msg) { printf("Error %u returned from %s\n",err,msg); DosExit(EXIT_PROCESS,0); } PID Os2GetPid(void) /* returns process id */ { PIDINFO pidinfo; DosGetPID(&pidinfo); /* get process id */ return pidinfo.pid; /* return it */ } Figure 5 SIG.MAK # make file for sig.c # COPT=/Lp /W3 /Zpiel /G2s /I$(INCLUDE) /Alfw sig.exe: sig.c sig cl $(COPT) sig.c /link /co /noi llibcmt markexe windowcompat sig.exe SIG.C /* sig.c RHS 5/1/89 signal handler example This program lets you observe how one process can use the OS2 signaling mechanism to pass signals between two processes. To use it, first run the program in one session with: SIG Note the process ID it prints. This will be the receiver of the signals. Then you can run subsequent instances of SIG in other sessions by passingthem the process ID of the first instance as a command line argument: SIG n where 'n' is the process ID printed by the first instance. */ #define INCL_DOS #define INCL_VIO #define INCL_KBD #define INCL_ERRORS #include<os2.h> #include<mt\stdio.h> #include<mt\memory.h> #include<mt\string.h> #include<mt\stdlib.h> #include<mt\process.h> #define SEMNAME "\\sem\\ssem.sem" #define THREADSTACK 400 char keyboard_thread_stack[THREADSTACK]; unsigned pid = 0; unsigned signaler = FALSE; USHORT prevaction0, prevaction1, prevaction2, prevaction3, prevaction4, prevaction5; PFNSIGHANDLER prevhandler0, prevhandler1, prevhandler2, prevhandler3, prevhandler4, prevhandler5; #define MAXSIGS 3 char *signames[MAXSIGS] = {"Flag A", "Flag B", "Flag C"}; USHORT signum[MAXSIGS] = {PFLG_A, PFLG_B, PFLG_C }; void main(int argc, char **argv); PID OS2GetPid(void); void APIENTRY sighandler(USHORT arg, USHORT num); void error_exit(USHORT err, char *msg); void keyboard_thread(void); void APIENTRY sighandler(USHORT arg, USHORT num) { USHORT retval; PFNSIGHANDLER prevhandler = NULL; USHORT prevaction = 0; /* acknowledge immediately */ retval = DosSetSigHandler((PFNSIGHANDLER)sighandler, &prevhandler,&prevaction,SIGA_ACKNOWLEDGE,num); switch(num) { case SIG_CTRLC: printf("%u received ^C...",pid); break; case SIG_KILLPROCESS: printf("%u notified of pending termination, terminating...",pid); DosExit(EXIT_PROCESS,0); break; case SIG_CTRLBREAK: printf("%u received ^Break...",pid); break; case SIG_PFLG_A: /* FLAG_A received */ printf("%u received signal, Flag A, arg=%u...",pid,arg); break; case SIG_PFLG_B: printf("%u received signal, Flag B, arg=%u...",pid,arg); break; case SIG_PFLG_C: printf("%u received signal, Flag C, arg=%u...",pid,arg); break; default: printf("%u received unknown signal (%u), arg=%u", pid, num, arg); break; } if(retval) printf("unable to acknowledge signal, retval=%u.\n",retval); else printf("acknowledged.\n"); return; } void main(int argc, char **argv) { unsigned retval,receiver_pid, signal = 0; HSYSSEM semhandle; KBDKEYINFO kbdkeyinfo; pid = OS2GetPid(); printf("%s loaded, pid=%u\n",argv[0],pid); if(retval = DosCreateSem(CSEM_PUBLIC,&semhandle,SEMNAME)) { if(retval != ERROR_ALREADY_EXISTS) error_exit(retval,"DosCreateSem"); signaler = TRUE; /* set flag */ if(argc != 2) error_exit(0,"main"); receiver_pid = atoi(argv[1]); } else { if(_beginthread(keyboard_thread,keyboard_thread_stack, THREADSTACK,NULL) == -1) error_exit(-1,"_beginthread"); /* set up signal handler to be sighandler function, pass address of prevhandler and prevaction even though there weren't any previous. */ if(retval = DosSetSigHandler(sighandler,&prevhandler0, &prevaction0,SIGA_ACCEPT,SIG_CTRLC)) error_exit(retval,"DosSetSigHandler(0)"); if(retval = DosSetSigHandler(sighandler,&prevhandler1, &prevaction1, SIGA_ACCEPT,SIG_KILLPROCESS)) error_exit(retval,"DosSetSigHandler(1)"); if(retval = DosSetSigHandler(sighandler,&prevhandler2, &prevaction2,SIGA_ACCEPT,SIG_CTRLBREAK)) error_exit(retval,"DosSetSigHandler(2)"); if(retval = DosSetSigHandler(sighandler,&prevhandler3, &prevaction3,SIGA_ACCEPT,SIG_PFLG_A)) error_exit(retval,"DosSetSigHandler(3)"); if(retval = DosSetSigHandler(sighandler,&prevhandler4, &prevaction4, SIGA_ACCEPT,SIG_PFLG_B)) error_exit(retval,"DosSetSigHandler(4)"); if(retval = DosSetSigHandler(sighandler,&prevhandler5, &prevaction5,SIGA_ACCEPT,SIG_PFLG_C)) error_exit(retval,"DosSetSigHandler(5)"); } while(TRUE) { if(signaler) /* if another copy is out there */ { /* signal it (not its children) with FLAG_A */ if(retval = DosFlagProcess(receiver_pid, FLGP_PID,signum[signal], pid)) error_exit(retval,"DosFlagProcess"); else printf("Sender (%u): sent signal to %u:%s\n", ` pid receiver_pid, signames[signal]); if(++signal >= MAXSIGS) signal = 0; /* check for key press */ retval = KbdCharIn(&kbdkeyinfo,IO_NOWAIT,0); if(!retval || (retval != ERROR_SIGNAL_PENDING)) /* if pressed, break out */ if(kbdkeyinfo.fbStatus & FINAL_CHAR_IN) break; DosSleep(100L); /* sleep again */ } else DosSleep(10000L); /* don't wake too often */ } } PID OS2GetPid(void) { PLINFOSEG ldt; SEL gdt_descriptor, ldt_descriptor; /* infoseg descriptors */ if(DosGetInfoSeg(&gdt_descriptor, &ldt_descriptor)) return 0; ldt = MAKEPLINFOSEG(ldt_descriptor); return ldt->pidCurrent; } void error_exit(USHORT err, char *msg) { printf("Error %u returned from %s\n",err,msg); DosExit(EXIT_PROCESS,0); } void keyboard_thread(void) { KBDKEYINFO kbdkeyinfo; while(TRUE) { if(!KbdCharIn(&kbdkeyinfo,IO_WAIT,0)) DosExit(EXIT_PROCESS,0); } } Figure 6 MONITOR.MAK # make file for monitor.c # COPT=/Lp /W3 /Zpiel /G2s /I$(INCLUDE) /Alfw monitor.exe: monitor.c monitor.mak cl $(COPT) monitor.c /link /co /noi llibcmt markexe windowcompat monitor.exe MONITOR.C /* monitor.c RHS 5/1/89 This program demonstrates the use of character device monitors on the keyboard. It creates a keyboard monitor that you can run from any session or the DETACHed session. Once you run the monitor with: MONITOR you can activate the monitor with Alt-F10. This will create a pop-up screen which will wait for any key press to end. You can terminate the monitor with Ctrl-F10. */ #define INCL_DOS #define INCL_VIO #define INCL_KBD #define INCL_ERRORS #include<os2.h> #include<mt\stdio.h> #include<mt\conio.h> #include<mt\process.h> unsigned long semhandle = 0L; unsigned char HotKey = 0x71; /* HotKey = Alt-F10 */ unsigned char TerminateKey = 0x67; /* TerminateKey = Ctrl-F10 */ unsigned program_active = TRUE; unsigned popup_active = FALSE; MONIN monInbuf; MONOUT monOutbuf; #define THREADSTACK 400 char monitor_stack[THREADSTACK]; #define MONINBUFSIZE (sizeof(monInbuf)-sizeof(monInbuf.cb)) #define MONOUTBUFSIZE (sizeof(monOutbuf)-sizeof(monOutbuf.cb)) #define RELEASE 0x40 /* bit mask to distinguish */ #define MAXMILLISECONDS 20000L #define SLEEPTIME 1000L typedef struct _keypacket /* KBD monitor data record */ { unsigned mnflags; KBDKEYINFO cp; unsigned ddflags; } KEYBUFF; void error_exit(USHORT err, char *msg); void main(void); void keyboard_monitor(void); void main(void) { USHORT popupwait = (VP_WAIT | VP_OPAQUE); KBDKEYINFO kbdkeyinfo; long milliseconds; DosSemSet(&semhandle); if(_beginthread(keyboard_monitor,monitor_stack,THREADSTACK, NULL) == -1) error_exit(-1,"_beginthread"); while(TRUE) { DosSemRequest(&semhandle,SEM_INDEFINITE_WAIT); /* wait for semaphore */ if(!program_active) /* time to terminate? */ break; /* then get out */ VioPopUp(&popupwait,0); printf("You made it...press a key"); /* wait 20 seconds for keystroke, then break out */ for( milliseconds = 0L; milliseconds < MAXMILLISECONDS; milliseconds += SLEEPTIME) { KbdCharIn(&kbdkeyinfo,IO_NOWAIT,0); /* check for key press */ if(kbdkeyinfo.fbStatus & FINAL_CHAR_IN) /* if pressed, break out */ break; DosSleep(SLEEPTIME); /* sleep for a while */ } VioEndPopUp(0); /* end the popup */ popup_active = FALSE; } DosExit(EXIT_PROCESS,0); /* kill the process */ } void keyboard_monitor(void) { USHORT retval; SEL gdt_descriptor, ldt_descriptor; /* infoseg descriptors */ KEYBUFF keybuff; unsigned count; HKBD KBDHandle; PGINFOSEG gdt; PLINFOSEG ldt; monInbuf.cb = MONINBUFSIZE; monOutbuf.cb = MONINBUFSIZE; /* obtain a handle for registering buffers */ if(retval = DosMonOpen ( "KBD$", &KBDHandle )) error_exit(retval,"DosMonOpen"); if(DosGetInfoSeg(&gdt_descriptor, &ldt_descriptor)) error_exit(retval,"DosGetInfoSeg"); gdt = MAKEPGINFOSEG(gdt_descriptor); ldt = MAKEPLINFOSEG(ldt_descriptor); /* register the buffers to be used for monitoring */ if(retval = DosMonReg ( KBDHandle, (PBYTE)&monInbuf, (PBYTE)&monOutbuf, MONITOR_BEGIN, gdt->sgCurrent)) error_exit(retval,"DosMonReg"); /* raise thread priority */ DosSetPrty(PRTYS_THREAD, PRTYC_TIMECRITICAL, 0, 0); for(keybuff.cp.chChar = 0; ; ) /* loop thru all keyboard in */ { count = sizeof(keybuff); /* read keyboard */ if(retval = DosMonRead ((PBYTE)&monInbuf, IO_WAIT, (PBYTE)&keybu (PUSHORT)&count )) error_exit(retval,"DosMonRead"); /* if make on our key */ if(keybuff.cp.chChar == 0) if(!popup_active && (keybuff.cp.chScan == \ HotKey) && !(keybuff.ddflags & RELEASE)) { popup_active = TRUE; DosSemClear(&semhandle); continue; } else if((keybuff.cp.chScan == TerminateKey) \ && !(keybuff.ddflags & RELEASE)) { program_active = FALSE; break; } if(retval = DosMonWrite((PBYTE)&monOutbuf, (PBYTE)&keybuff,count)) error_exit(retval,"DosMonWrite"); } DosMonClose(KBDHandle); /* close the monitor */ DosSemClear(&semhandle); DosExit(EXIT_THREAD,0); /* kill the thread */ } void error_exit(USHORT err, char *msg) { printf("Error %u returned from %s\n",err,msg); DosExit(EXIT_PROCESS,0); } Figure A NPSERVER.MAK # make file for npserver.c # #COPT=/Lp /W3 /Zp /Zl /G2s /Ox /I$(INCLUDE) /Alfw COPT=/Lp /W3 /Zpiel /G2s /I$(INCLUDE) /Alfw npserver.exe: npserver.c npserver.mak cl $(COPT) npserver.c /link /co /noi llibcmt markexe windowcompat npserver.exe NPSERVER.C /* npserver.c RHS 5/1/89 This program demonstrates the use of named pipes by creating a server process which will create more than instance of a named pipe. To see how this works, run this program with: NPSERVER Then run multiple instances of the NPCLIENT program in other sessions. You can also run the NPRMODE program in the compatibility box to talk to the server. */ #define INCL_DOS #include<os2.h> #include<mt\string.h> #include<mt\stdio.h> #include<mt\process.h> #include"nmpipe.h" #define MAXSERVERS 5 #define THREADSTACKSIZE 1200 unsigned long semhandle = 0L; typedef struct _servers { char stack[THREADSTACKSIZE]; int threadID; } SERVER; SERVER servers[MAXSERVERS]; USHORT pipeopenmode = PIPE_ACCESS_DUPLEX; USHORT pipemode = (PIPE_WAIT | PIPE_READMODE_BYTE | PIPE_TYPE_BYTE |\ MAXSERVERS); void main(void); void error_exit(USHORT err, char *msg); void server_thread(int servernum); void main(void) { int i; DosSemSet(&semhandle); for( i = 0; i < MAXSERVERS; i++) /* create server threads */ { if(servers[i].threadID = _beginthread(server_thread, servers[i].stack,THREADSTACKSIZE,(void *)i) == -1) error_exit(-1,"_beginthread"); DosSleep(100L); } DosSemRequest(&semhandle,SEM_INDEFINITE_WAIT); /* wait for semaphore */ DosExit(EXIT_PROCESS,0); /* kill the process */ } void server_thread(int servernum) { char readbuf[PIPESIZE]; HPIPE pipehandle; USHORT retval; USHORT bytesread; printf("Server instance %d creating Named pipe...\n",servernum); /* create pipe instance */ if(retval = DosMakeNmPipe(NAMEDPIPE,&pipehandle,pipeopenmode, pipemode, PIPESIZE,PIPESIZE,1000L)) error_exit(retval, "DosMakeNmPipe"); printf("Server instance %d waiting for Client to open pipe...\n", servernum); if(retval = DosConnectNmPipe(pipehandle)) /* wait until connected */ error_exit(retval,"DosConnectNmPipe"); printf("Server instance %d: Client has opened pipe, \ waiting for message...\n",servernum); while(TRUE) { /* read a message */ if(retval = DosRead(pipehandle,readbuf,PIPESIZE,&bytesread)) error_exit(retval,"DosRead"); printf("Server instance %d received: \'%s\'...acknowledging\n",servernum,readbuf); /* acknowledge it */ if(retval = DosWrite(pipehandle,ACKNOWLEDGE, strlen(ACKNOWLEDGE)+1, &bytesread)) error_exit(retval,"DosWrite"); DosSleep(250L); } DosSemClear(&semhandle); DosClose(pipehandle); } void error_exit(USHORT err, char *msg) { printf("Error %u returned from %s\n",err,msg); DosExit(EXIT_PROCESS,0); } Figure B NPCLIENT.MAK # make file for npclient.c # #COPT=/Lp /W3 /Zp /Zl /G2s /Ox /I$(INCLUDE) /Alfw COPT=/Lp /W3 /Zpiel /G2s /I$(INCLUDE) /Alfw npclient.exe: npclient.c npclient.mak cl $(COPT) npclient.c /link /co /noi llibcmt markexe windowcompat npclient.exe NPCLIENT.C /* npclient.c RHS 5/1/89 This program creates a client process which will communicate with an instance of the named pipe created by NPSERVER. After running NPSERVER, you can run several instances of this program with: NPCLIENT */ #define INCL_DOS #define INCL_KBD #define INCL_ERRORS #include<os2.h> #include<string.h> #include<stdio.h> #include"nmpipe.h" char writebuf[PIPESIZE]; void main(void); void error_exit(USHORT err, char *msg); PID Os2GetPid(void); void main(void) { HPIPE pipehandle; USHORT retval; USHORT bytesread; USHORT action; PID pid; pid = Os2GetPid(); printf("Client %u trying to open named pipe...\n",pid); while(TRUE) /* open the pipe */ { if(retval = DosOpen(NAMEDPIPE,&pipehandle,&action, 0L,FILE_NORMAL,FILE_OPEN, OPEN_SHARE_DENYNONE | OPEN_ACCESS_READWRITE, 0L)) { printf("%u: Error %u trying to open %s, sleeping...\n",pid,retval,NAMEDPIPE); if(retval == ERROR_PIPE_BUSY) while(TRUE) { if(!(retval = DosWaitNmPipe(NAMEDPIPE,1000L))) break; if(retval != ERROR_SEM_TIMEOUT) error_exit(retval,"DosWaitNmPipe"); } printf("Trying again...\n"); } else break; } printf("Client %u: server has connected pipe, sending message...\n", pid); while(TRUE) { strcpy(writebuf,"Anything I want?"); /* write a message */ if(retval = DosWrite(pipehandle,writebuf, strlen(writebuf)+1,&bytesread)) error_exit(retval,"DosWrite"); /* read acknowledgment */ if(retval = DosRead(pipehandle,writebuf,PIPESIZE,&bytesread)) error_exit(retval,"DosRead"); printf("%u: Server has acknowledged: \'%s\'\n",pid,writebuf); DosSleep(200L); } DosClose(pipehandle); DosExit(EXIT_PROCESS,0); } PID Os2GetPid(void) /* returns process id */ { PIDINFO pidinfo; DosGetPID(&pidinfo); /* get process id */ return pidinfo.pid; /* return it */ } void error_exit(USHORT err, char *msg) { printf("Error %u returned from %s\n",err,msg); DosExit(EXIT_PROCESS,0); } Figure C NPRMODE # make file for nprmode.c # #COPT=/W3 /Zp /Zl /G2s /Ox /I$(INCLUDE) COPT=/W3 /Zpiel /G2 /I$(INCLUDE) nprmode.exe: nprmode.c nprmode.mak cl $(COPT) nprmode.c /link /co NPRMODE.C /* nprmode.c RHS 5/1/89 This program opens the named pipe created by NPSERVER from the DOS compatibility environment. After starting NPSERVER in a protected mode session, you can bring the DOS box session into the foreground and run this program with: NPRMODE */ #include<string.h> #include<stdio.h> #include<stdlib.h> #include<fcntl.h> #include<io.h> #include<share.h> #include<sys\types.h> #include<sys\stat.h> #include"nmpipe.h" #if !defined(TRUE) #define TRUE 1 #endif char message[80]; void main(void); void main(void) { char *mess = "Message from the DOS box!"; int pipehandle, err, count = strlen(mess); if((pipehandle = sopen(NAMEDPIPE,(O_BINARY | O_RDWR), SH_DENYNO)) == -1) { printf("sopen failed, errno = %d\n",errno); exit(0); } while(TRUE) { strcpy(message,mess); if((err = write(pipehandle,message,count)) == -1) { printf("write failed, errno = %d\n",errno); exit(0); } count = 79; if((err = read(pipehandle,message,count)) == -1) { printf("read failed, errno = %d\n",errno); exit(0); } if(err) { message[err] = NULL; printf("Message received from the protected world: \"%s\"\n",message); } else printf("No message from protected world\n"); } close(pipehandle); exit(0); } Figure D /* nmpipe.h RHS 5/1/89 shared header file for named pipe source */ #define NAMEDPIPE "\\PIPE\\NMDPIPE" #define QUIT_COMMAND "QUIT" #define ACKNOWLEDGE "You got it!" #define PIPESIZE 100 Find FIles Under Presentation Manager and Windows with a Handy Utility Kevin P. Welch Increased disk capacity has made searching for lost or misplaced files a difficult task. Locating a small file on a system that uses several large hard disks and a network file server can be time-consuming, since the file could be tucked away in some long-forgotten subdirectory or on some seldom-used volume. I solved this problem when I was working in the MS-DOS(R) operating system by using several commercial and public domain utilities designed to assist in managing disks and their constantly expanding contents. When my primary computing environment switched from MS-DOS1 to the Windows environment and then to OS/2 Presentation Manager, few of those long-familiar utilities survived the transition. Faced with the continued prospect of missing files, I decided to create a file management utility to help solve this problem. The File Finder program described here is a result of this work. It lets you quickly search your hard disk for misplaced files in either Windows2 or OS/2 Presentation Manager. When you run FINDER.EXE, the dialog box in Figure 1 will be displayed. The first field of interest in this dialog box is the search specification, or pattern field. When you press the Search button, the program will search all your fixed disk drives for files that satisfy the criteria (including wild-card characters) you specified. The resulting list of files will then be displayed with their respective subdirectory names, file size, and creation date and times in the list box immediately below the search specification field. You can limit the search to a particular set of drives by selecting those of interest from the multiple-selection list box in the lower right-hand corner of the dialog box. Selecting fewer drives will speed up the search process (especially when excluding network drives) and potentially reduce the number of files found. As an additional benefit, the File Finder allows you to select listed files and load them, much as you would using the Windows MS-DOS Executive or the OS/2 File Manager. Each time you select a file, the File Finder automatically determines if the file is a directly executable program or, in the case of the Windows version, if it contains data supported by some other application. In the Windows version, the actual classification of each file is determined by parsing the contents of the Windows initialization file WIN.INI. If the file extension is listed as a Windows program (usually EXE, COM, and BAT files), it will be considered directly executable: [windows] programs=exe com bat If the file is known to the system as data, the corresponding Windows application will be automatically associated with the file using the full pathname as its first parameter: [extensions] wri=write.exe ^.wri art=scrap.exe ^.art crd=cardfile.exe ^.crd If there is insufficient memory, both the Windows and OS/2 versions of File Finder will generate a warning message informing you that insufficient memory is available to complete the requested search operation. You can correct this situation by providing a more detailed search specification or selecting fewer disk drives. Creating the File Finder To build the File Finder, use a Microsoft C Compiler Version 5.1 and the Microsoft Windows Version 2.10 Software Development Kit or the Microsoft OS/2 Version 1.10 Software Development Kit (SDK) to create the following source files (see Figures 2a and 2b for code fragments from the Windows and Presentation Manager versions, respectively). The complete source files can be downloaded from any of the MSJ bulletin boards. FINDER make file FINDER.H header file FINDER.C source file FINDER.RC resource file FINDER.ICO icon file FINDER.LNK link file FINDER.DEF module file Note that although the Windows and OS/2 versions of File Finder use a similar-looking set of files, their actual contents are quite different.When you have defined these files, using either version, you can build FINDER.EXE by entering the MAKE FINDER command. In most cases the compilation should proceed without any fanfare and you will be well on your way to using the program. If you do have problems compiling or linking the program, you might want to check your environment variables, the search path, and the location of the required run-time libraries used by the C compiler and the appropriate SDK. Windows File Finder The Windows version of the File Finder is a unique application in that it doesn't have a main window function. In its place, the WinMain function (the entry point for the application) brings up the File Finder dialog box. The FinderDlgFn becomes in effect the main window function, assisted in part by the Windows dialog box manager. If you read carefully through the FinderDlgFn source code, you will observe that most of the work involves initializing the dialog box, searching for files, and verifying whether a selected file is a program or data. When the dialog box is first displayed, the FinderDlgFn function will receive a WM_INITDIALOG message. This message causes the dialog box to be centered inside the screen and all system disk drives to be displayed in the lower right-hand corner list box. As a convenience to the user, all disk drives labeled C or above are automatically selected. Finally, the program defines an icon for the File Finder dialog box, using the SetClassWord function. This last operation might require a little further explanation. In Windows, dialog boxes are based on a predefined window class, #32770. Each time you create a dialog box, Windows creates another instance of this window using the resource template you specified as a model. Each of the child windows (known as controls) are automatically created and positioned inside this window, according to the coordinates specified in the dialog box template. Using dialog boxes saves you considerable development time, but the overwhelming benefit comes from their highly refined keyboard user interface. At times this user interface is so appealing that you might want to use a dialog box as a top-level window. That is the technique used by the File Finder. Unfortunately, there are several drawbacks to this approach. The first and perhaps most significant is that Windows automatically locks your application data segment whenever a dialog box is displayed. Although there are some good reasons for this, it does mean that an application based on a top-level dialog box will always have a fixed data segment--something that you normally try to avoid. The second drawback involves the dialog box class icon. Since dialog boxes cannot be made iconic in normal operation, Windows does not define an icon for the #32770 window class. The File Finder application gets around this limitation by manually defining an icon using the SetWindowWord function. Although satisfactory in most situations, this approach creates another unfortunate side effect: if any other application uses the same technique, its dialog box icons will automatically change to reflect the most recently defined one. Unfortunately, the real solution is more complicated and requires a choice. You could either hide the dialog box temporarily and create an iconic window, or you could write your own dialog box subsystem that is not subject to these limitations. Following initialization of the dialog box, the next event of significance to the File Finder is the receipt of the ID_SEARCH command. This command resets the contents of the file list box and starts a recursive directory search covering all the selected drives. Note that the redraw flag of the file list box is temporarily set to FALSE. This speeds up the search operation greatly by eliminating any unnecessary list box update operations. The actual search operation is performed by the Directory function. This function retrieves a directory entry for each normal file in the specified subdirectory using the _dos_findfirst and _dos_findnext functions in the Microsoft C run-time library. After all the files that satisfy the specified criteria are found, the search is recursively repeated in each subdirectory. This entire process is repeated until the whole drive has been searched or the list box becomes full. The last event of significance to the File Finder is the selection of a file from the file list box. Whenever a file is selected, it is automatically evaluated to determine if it is a program or data, as previously mentioned. If as a result of this evaluation the Run button is enabled, the file can be successfully loaded. Double-clicking on the file or pressing the Run button will load either the selected program or the application responsible for the information. Although there are several mechanisms for starting other applications inside Windows, the C run-time function spawnl is used in this particular case. OS/2 File Finder The OS/2 version of the File Finder (written by Jerry Weldon) is similar to the Windows version, consisting of a single dialog box centered inside the desktop. When the dialog box is initially displayed, all fixed and network disk drives are automatically selected. The actual search operation is performed almost identically to the search in Windows. Like the Windows version, the Directory function is called recursively, successively searching each subdirectory for files that match the search criteria until the entire drive has been traversed. This process is repeated for each of the selected disk drives. When a disk file is selected following a search operation, the File Finder automatically retrieves the name and checks the extension. If the file has an EXE extension, the Run button is enabled. OS/2 1.1 does not have a facility to associate file extensions with applications, unlike Windows, but this limitation will be removed by the High Performance File System to be provided with the next release of OS/2. The only other differences in the OS/2 version are the association of an icon with the main dialog box and the use of a monospace font in the file list box. As with Windows, Presentation Manager dialog boxes are not normally associated with icons. However, a more reliable mechanism exists in Presentation Manager to accomplish this task than in Windows. In the main function, the dialog box is manually loaded, and an icon is explicitly associated with it using a WM_SETICON message. The use of a fixed pitch or monospace font in the file list box is necessitated by the lack of tab stops to align the various columns. The list box is declared in the resource file with the LS_OWNERDRAW style. Specifying this style enables the list box owner to take control of some of the text display and formatting operations normally handled by the control. In this case the dialog box (being the owner of the list box) receives a WM_MEASUREITEM message when the list box is created. The dialog box responds to this message by defining a monospace Courier font and returning a value specifying the height of each text line. Each time an item is displayed in the list box, the dialog box receives a WM_DRAWITEM message. The dialog box message processing function intercepts this message, selects the Courier font into the presentation space of the list box, and returns FALSE, enabling the list box to display the text using the newly selected font. Enhancements After using the File Finder for a while and reviewing the source code, you will undoubtedly come up with many ideas for enhancement. One that I like is the inclusion of something akin to the File Finder in every Open-File dialog box. When using more than one application, I invariably need to open other files located elsewhere on my computer. Rather than using the Open-File dialog box to search manually for the desired file, I often bring up the File Finder and start a quick search. Once the errant file is located, I load it into the application using the Open-File dialog box. Although there are many styles of Open-File dialog boxes in use today, most could be easily adapted to incorporate file search capabilities. For example, I incorporated the File Finder into one of my own dialog boxes in the manner shown in Figure 3. Ideas for enhancements usually occur as the need arises. You could extend the program to support multiple search expressions, specific file attributes, or hidden subdirectories. More complicated operations are certainly possible and could be easily incorporated into either the Windows or Presentation Manager version of the File Finder program. Figure 2a ■ ■ ■ /* FinderDlgFn( hWnd, wMsg, wParam, lParam ) : BOOL This function is responsible for processing all the messages relating to the file finder dialog box. This mainly involves the definition and retrieval of the various events generated by the user. */ ■ ■ ■ case ID_FILES : /* file selected */ { int nItem; PSTR pszExt; BOOL bEnable; char szExt[16]; char szDir[64]; char szFile[64]; char szProg[64]; /* process listbox notification code */ switch( HIWORD(lParam) ) { case LBN_SELCHANGE : /* selection is changing */ /* initialization */ bEnable = FALSE; /* reteve index to selected item */ nItem = SendMessage( LOWORD(lParam), LB_GETCURSEL, 0, 0L ); if ( nItem >= 0 ) { /* retrieve text of selected item */ SendMessage( LOWORD(lParam), LB_GETTEXT, nItem, (LONG)(LPSTR)szFile ); /* continue if file name selected */ if ( szFile[0] == ' ' ) { /* retrieve current directory */ do { SendMessage( GetDlgItem(hDlg,ID_FILES), LB_GETTEXT, --nItem, (LONG)(LPSTR)szDir ); } while( szDir[0] == ' ' ); /* define full path name */ sprintf( szPath, "%s\\%s", szDir, strtok(&szFile[2]," ") ); /* check if extension defined */ if ( strrchr(szPath,'.') ) { /* retrieve extension */ strcpy( szExt, strrchr(szPath,'.')+1 ); /* retrieve list of program extensions */ GetProfileString( "windows", "programs", "", szProg, sizeof(szProg) ); /* continue verification if not a program */ if ( !Present(szProg,szExt) ) { /* retrieve extension profile string */ GetProfileString( "extensions", szExt, "", szApp, sizeof(szApp) ); /* check if extension listed */ if ( szApp[0] ) { bEnable = TRUE; *strchr(szApp,' ') = 0; } } else { bEnable = TRUE; strcpy( szApp, szPath ); szPath[0] = 0; } } } } /* enable run button */ EnableWindow( GetDlgItem(hDlg,ID_RUN), bEnable ); break; case LBN_DBLCLK : /* double-click */ /* perform run if button enabled */ if ( IsWindowEnabled(GetDlgItem(hDlg,ID_RUN)) ) if ( spawnl(P_NOWAIT,szApp,szApp,szPath,NULL) >= 0 ) ShowWindow( hDlg, SW_MINIMIZE ); break; default : /* something else */ break; } } break; case ID_SEARCH : /* search using path */ case IDOK : { WORD wItem; WORD wNumber; HWND hListBox; BOOL bContinue; HCURSOR hOldCursor; char szDrive[8]; char szPattern[64]; char szFileSpec[64]; /* disable run button & change cursor to hourglass */ EnableWindow( GetDlgItem(hDlg,ID_RUN), FALSE ); hOldCursor = SetCursor( LoadCursor(NULL,IDC_WAIT) ); /* retrieve search pattern & erase current file list */ GetDlgItemText( hDlg, ID_PATTERN, szPattern, sizeof(szPattern) ); SendMessage( GetDlgItem(hDlg,ID_FILES), WM_SETREDRAW, FALSE, 0L ); SendMessage( GetDlgItem(hDlg,ID_FILES), LB_RESETCONTENT, 0, 0L ); /* perform search on each selected drive */ bContinue = TRUE; hListBox = GetDlgItem( hDlg, ID_DRIVES ); wNumber = (WORD)SendMessage( hListBox, LB_GETCOUNT, 0, 0L ); for ( wItem=0; (bContinue)&&(wItem<wNumber); wItem++ ) if ( SendMessage(hListBox,LB_GETSEL,wItem,0L) ) { /* define file specification */ SendMessage( hListBox, LB_GETTEXT, wItem, (LONG)(LPSTR)szDrive ); sprintf( szFileSpec, "%c:\\%s", szDrive[2], szPattern ); /* create directory listing using pattern */ bContinue = Directory( szFileSpec, _A_NORMAL, GetDlgItem(hDlg,ID_FILES) ); } /* activate redrawing */ SendMessage( GetDlgItem(hDlg,ID_FILES), WM_SETREDRAW, TRUE, 0L ); /* check for null search operation */ if ( SendMessage( GetDlgItem(hDlg,ID_FILES), LB_GETCOUNT, 0, 0L ) == 0L ) MessageBox( hDlg, "No files found matching criteria!", "File Finder", MB_OK|MB_ICONHAND ); /* restore cursor & repaint display */ SetCursor( hOldCursor ); InvalidateRect( GetDlgItem(hDlg,ID_FILES), NULL, TRUE ); } break; case ID_RUN : /* run selection */ /* start application & make finder iconic */ if ( spawnl(P_NOWAIT,szApp,szApp,szPath,NULL) >= 0 ) ShowWindow( hDlg, SW_MINIMIZE ); break; case ID_QUIT : /* close dialog */ case IDCANCEL : EndDialog( hDlg, TRUE ); break; default : /* something else */ break; } break; default : bResult = FALSE; break; } /* return result */ return( bResult ); } Figure 2b ■ ■ ■ /* FinderDlgProc This function is responsible for processing all the messages which relate to the file finder dialog box. This mainly involves the definition and retrieval of the various events generated by the user. */ ■ ■ ■ /* define search pattern */ WinSetDlgItemText( hwnd, IDD_PATTERN, "*.*" ); /* initialize drive list */ DosQCurDisk( &usCurDrive, &flDrives ); szDrive[0] = '['; szDrive[1] = '-'; szDrive[3] = '-'; szDrive[4] = ']'; szDrive[5] = '\0'; for ( szDrive[2] = 'A'; szDrive[2] <= 'Z'; szDrive[2]++ ) { if ( flDrives & 1 ) WinSendDlgItemMsg( hwnd, IDD_DRIVES, LM_INSERTITEM, MPFROMSHORT( LIT_END ), MPFROMP( szDrive ) ); flDrives >>= 1; } /* select all fixed drives */ hwndListBox = WinWindowFromID( hwnd, IDD_DRIVES ); cszList = SHORT1FROMMR( WinSendMsg( hwndListBox, LM_QUERYITEMCOUNT, 0L, 0L ) ); for ( iItem = 0; iItem < cszList; iItem++ ) { WinSendMsg( hwndListBox, LM_QUERYITEMTEXT, MPFROM2SHORT( iItem, sizeof(szDrive) ), MPFROMP( szDrive ) ); if ( szDrive[2] >= 'C' ) WinSendMsg( hwndListBox, LM_SELECTITEM, MPFROMSHORT( iItem ), MPFROMSHORT( TRUE ) ); } } break; case WM_CONTROL: switch ( SHORT1FROMMP( mp1 ) ) { case IDD_PATTERN: if ( SHORT2FROMMP( mp1 ) == EN_CHANGE ) /* enable/disable search button */ WinEnableWindow( WinWindowFromID( hwnd, IDD_SEARCH ), WinQueryWindowTextLength( WinWindowFromID( hwnd, IDD_PATTERN ) ) ); else fPassToDef = TRUE; break; case IDD_FILES: switch ( SHORT2FROMMP( mp1 ) ) { case LN_SELECT: /* an item was selected */ { USHORT iSelection; /* selected file index */ CHAR szFile[64]; /* name of selected file */ USHORT cchFile; /* file name length */ BOOL fExecable; /* is file executable? */ /* get name of selected file */ fExecable = FALSE; iSelection = SHORT1FROMMR( WinSendDlgItemMsg( hwnd, IDD_FILES, LM_QUERYSELECTION, MPFROMSHORT( LIT_FIRST ), 0L ) ); if ( iSelection != LIT_NONE ) { WinSendDlgItemMsg( hwnd, IDD_FILES, LM_QUERYITEMTEXT, MPFROM2SHORT( iSelection, sizeof(szFile) ), MPFROMP( szFile ) ); /* is file executable? */ if ( szFile[0] == ' ' ) { cchFile = strlen( strtok( &szFile[2], " " ) ); if ( cchFile > 4 && strcmp( &szFile[2+cchFile-4], ".EXE" ) == 0 ) fExecable = TRUE; } } /* if file is executable, enable run button */ WinEnableWindow( WinWindowFromID( hwnd, IDD_RUN ), fExecable ); } break; case LN_ENTER: /* equivalent to run button clicked */ WinSendDlgItemMsg( hwnd, IDD_RUN, BM_CLICK, 0L, 0L ); break; default: fPassToDef = TRUE; break; } break; default: fPassToDef = TRUE; break; } break; case WM_COMMAND: switch ( COMMANDMSG( &usMsg )->cmd ) { case IDD_SEARCH: /* search using path */ case DID_OK: { HPOINTER hptrOld; /* handle to old cursor */ USHORT iItem; /* temporary loop variable */ HWND hwndListBox; /* handle to list box */ BOOL fContinue; /* boolean search flag */ CHAR szDrive[8]; /* drive specification string */ CHAR szPattern[64]; /* search pattern string */ CHAR szFileSpec[64]; /* file specification string */ /* disable run button & change cursor to hourglass */ WinEnableWindow(WinWindowFromID( hwnd, IDD_RUN), FALSE ); hptrOld = WinQueryPointer( HWND_DESKTOP ); WinSetPointer( HWND_DESKTOP, WinQuerySysPointer( HWND_DESKTOP, SPTR_WAIT, FALSE ) ); /* retrieve search pattern & erase current file list */ WinQueryDlgItemText( hwnd, IDD_PATTERN, sizeof(szPattern), szPattern ); WinSendDlgItemMsg(hwnd, IDD_FILES, LM_DELETEALL, 0L, 0L); /* perform search on each selected drive */ fContinue = TRUE; hwndListBox = WinWindowFromID( hwnd, IDD_DRIVES ); iItem = LIT_FIRST; while ( (iItem = SHORT1FROMMR( WinSendMsg( hwndListBox, LM_QUERYSELECTION, MPFROMSHORT( iItem ), 0L ) )) != LIT_NONE && fContinue ) { /* define file specification */ WinSendMsg( hwndListBox, LM_QUERYITEMTEXT, MPFROM2SHORT( iItem, sizeof(szDrive) ), MPFROMP( szDrive ) ); sprintf(szFileSpec, "%c:\\%s", szDrive[2], szPattern ); /* create directory listing using pattern */ fContinue = Directory( szFileSpec, FILE_NORMAL, WinWindowFromID( hwnd, IDD_FILES ) ); } ■ ■ ■ case IDD_RUN: { USHORT iSelection; /* selected file index */ CHAR szFile[64]; /* name of executable file */ CHAR szDir[64]; /* name of directory */ CHAR szPath[64]; /* full path name of file */ CHAR szFail[64]; /* if DosExecPgm fails */ RESULTCODES resc; /* result of child process */ USHORT usErr; /* error code */ /* get name of selected file */ iSelection = SHORT1FROMMR( WinSendDlgItemMsg( hwnd, IDD_FILES, LM_QUERYSELECTION, MPFROMSHORT( LIT_FIRST ), 0L ) ); if ( iSelection != LIT_NONE ) { WinSendDlgItemMsg( hwnd, IDD_FILES, LM_QUERYITEMTEXT, MPFROM2SHORT( iSelection, sizeof(szFile) ), MPFROMP( szFile ) ); /* define full path name */ do { WinSendDlgItemMsg( hwnd, IDD_FILES, LM_QUERYITEMTEXT, MPFROM2SHORT( --iSelection, sizeof(szDir) ), MPFROMP( szDir ) ); } while ( szDir[0] == ' ' ); sprintf( szPath, "%s\\%s", szDir, strtok( &szFile[2], " " ) ); ■ ■ ■ Writing Faster Graphics Programs by Using Offscreen Bitmap Manipulation Kevin Ruddell Working in an interactive environment, such as the OS/2 Presentation Manager, it is not enough to write a program that produces correct output. The length of the response time must also be considered. If it is too long, users are likely to become frustrated. The ideal program, then, is one that is snappy, responsive, and accurate. This article considers an assortment of techniques you can use to make your Graphics Programming Interface (GPI) Presentation Manager program run more quickly. I will discuss them as they apply to a particular program, JIGSAW.C, which implements a jigsaw puzzle. JIGSAW has gone through many versions, starting with a rather slow feasibility test, but it is now a lot of fun to play. A recent version of JIGSAW was shipped with the Microsoft(R) OS/2 Software Development Kit, Version 1.1, and with the Microsoft OS/2 Presentation Manager Toolkit, Version 1.1. To play JIGSAW, you load a bitmap from a file. JIGSAW uses BitBlt (bit block transfer) with clip paths to create a collection of picture fragments that are the puzzle pieces. Each piece is associated with a single retained segment and an auxiliary data structure, used for drawing and for selection with the mouse. To speed the program's responsiveness to user requests, the real work is done in a second thread, with work requests transmitted from the main thread in the form of messages. This arrangement makes it possible for the user to override a lengthy drawing operation with a higher-priority request (for instance, program termination or magnification change). To scroll and zoom in on or back from the puzzle image, JIGSAW changes the default viewing transform. An individual piece is moved by changing its segment transform. To minimize flicker (and increase speed), drawing operations are generally done offscreen and the result copied to the screen. Some of the data structures used in JIGSAW are shown in Figure 1. (Full source code listings are available for downloading from any of the MSJ bulletin boards--Ed.)Note the double-linked list used to record piece-related data. Using the double-linked list, the program traverses each piece in the puzzle starting at pslTail, going from the top to the bottom of the scene (see Figures 2-5). Clipping The use of clipping in Presentation Manager limits the effect of Gpi or Win calls to part of a picture. The clipping boundary can be specified in world space, model space, presentation page space, and device space. The only form of clipping that will be discussed here is the one in world space--the clip path. A path is a closed boundary defined by Gpi drawing calls. You define a clip path by defining a path, then calling GpiSetClipPath. The path is specified in world space, and any subsequent drawing calls only change the pels that lie within or on the clip path after translation to device space (see Figures 6-8). BitBlt BitBlt is a high-performance method of copying bitmaps. In Presentation Manager, there are two forms of BitBlt: GpiBitBlt and GpiWCBitBlt. Each employs source and target rectangles, along with options for specifying the mixing function and the compression mode. Translation and scaling factors are applied to the bitmap so as to map the source rectangle to the destination rectangle. GpiBitBlt works entirely in device coordinates, whereas GpiWCBitBlt works in device coordinates in the source and in world coordinates in the target. If you are placing drawing orders in a retained segment, you must use GpiWCBitBlt; but if you are not, you may use either form. The advantage of using GpiWCBitBlt is that it automatically takes account of coordinate space transforms; the advantage of GpiBitBlt is that it gives you device-level control of where your BitBlt goes. Note that the source argument of GpiWCBitBlt is a handle to a bitmap that must not be currently selected into a presentation space, whereas the source argument to GpiBitBlt is a handle to a presentation space that must have a bitmap selected into it (See Figure 9). One further note on GpiWCBitBlt is that for OS/21 Versions 1.1 and 1.2, a rotation in the transforms does not cause the data to be rotated. Instead, the target rectangle is transformed to device space and is then replaced by the smallest upright rectangle containing the transformed rectangle (see Figures 10, 11, and 12). Rectangles With clipping in effect, increasing the boundaries of the BitBlt rectangles beyond the dimensions of the clip path has no effect on the drawing except to slow it down. An early version of a given program might ignore this fact and use BitBlt to transfer the whole bitmap through the clip path. Once the program is debugged, you can speed it up by trimming the source and target rectangles. In the case of JIGSAW, such trimming caused a sixfold speedup in an early version. Determining the bounding rectangles of a piece in model space and in device space is illustrated in Figures 13 and 14. Whenever the dimensions of the BitBlt source and target rectangles in device coordinates differ, BitBlt must spend extra time stretching or compressing the bitmap. You can eliminate this extra time by seeing to it that the source and target rectangles are the same size. This is easy to do with GpiBitBlt, since both rectangles are expressed in device coordinates. When using GpiWCBitBlt, you can use coordinate conversion to see how a rectangle in world space maps to a rectangle in device space. Because of rounding effects, it is not always the case in OS/2 Versions 1.1 and 1.2 that the device space rendering of a rect-angle in a segment retains precisely the same dimensions when the translation part of the segment transform applied to it (lM31 and lM32) is changed. Changing the size by even one pel causes stretching or compression to occur. In the case of JIGSAW, therefore, if you move a piece around the screen by changing its segment transform and then use GpiWCBitBlt, sometimes the piece will be drawn quickly and sometimes slowly. Using Memory Alignment You can determine the horizontal and vertical alignment factors for the display driver by calling WinQuerySysValue with SV_CXBYTEALIGN and SV_CYBYTEALIGN, respectively. BitBlt copies data between memory locations, and you can speed up the transfer by aligning your data on the appropriate memory boundaries. If you are doing a BitBlt to the screen and the sides of your tightest-possible rectangle are not memory-aligned, you can expand them until they are aligned. Note that, in the case of the screen, the alignment should be performed using desktop window-relative coordinates (see Figure 15). On most currently available hardware, main memory accesses are significantly faster than display memory accesses. For that reason, and also to minimize flicker, it is often desirable to perform drawing operations off the screen and use BitBlt to copy the result to the screen. In JIGSAW, when a piece is selected, the offscreen image is erased in a tight box around the selected piece. Then, all nonselected pieces overlapping the erased area are redrawn. This intermediate state is saved for later use, and the selected piece is drawn on top. If this erasing and drawing is done on the screen, the user sees the intermediate states of erasure and healing (redrawing of the screen) in stages. This is rather ugly (see Figure 16). However, if the offscreen intermediary is used, the user just sees the selected piece appear on the surface, with no other disturbance to the image. When you scroll using WinScrollWindow, you should advance by multiples of the display driver alignment factor whenever possible. This allows the Presentation Manager to move the bitmap data in memory-aligned chunks. Clip Paths and Masks If performing a BitBlt through a clip path gives you what you want and, as in the case of JIGSAW, you will reuse the result many times, you should consider using masks. A mask is a bitmap that is AND'ed or OR'ed to another bitmap in order to change part of the other bitmap and leave the rest undisturbed. The idea is that you do the work of performing BitBlt through a clip path twice to prepare masks. These masks can then be used for BitBlts without clipping, stretching, or compression, achieving the same result as previously but with much greater speed. The first mask is created by filling the mask with 1s, using BitBlt with the raster operation set to ROP_ONE and no clip path. Then, a clip path is defined and filled with 0s by means of BitBlt with the raster operation set to ROP_ZERO. When BitBlt is used to copy this bitmap to the screen with a raster operation of ROP_SRCAND (logical AND), the effect is to punch a hole in the screen bitmap in the shape of the clip path. The second mask is created by filling the mask with 0s, using BitBlt with the raster operation set to ROP_ZERO and no clip path. A clip path is defined and filled with data, using BitBlt with the raster operation set to ROP_SRCCOPY. BitBlt will copy this bitmap to the screen with a raster operation of ROP_SRCPAINT (logical OR), so it fills the hole in the screen bitmap left by the first mask (see Figure 17). Segments Gpi-retained segments are a high-level programming construct; they allow you to store selected Gpi calls in numbered bundles for later use. Capable of being created, edited, and deleted, the segments have multiple uses. In JIGSAW, they are used for correlation; in earlier versions they were used in painting, discussed below. The pieces in JIGSAW are drawn to fit together in world space, with one piece assigned to each retained segment. Initially, the segment transforms are set to identity (no scaling, rotation, or translation). When a piece is moved, the translation part of its segment transform (lM31 and lM32) is changed to reflect the desired displacement. Correlation In JIGSAW, the user moves a piece by moving the mouse pointer on top of it, depressing the left mouse button, dragging the mouse pointer to the new location, and releasing the left mouse button. The process of converting the mouse coordinates when the mouse button is clicked to the identifier of a particular segment is an example of correlation. To correlate, you supply a correlation point to OS/2 Presentation Manager (PM). PM creates an imaginary rectangle (the pick aperture) centered on the correltion point. PM then simulates drawing segments and reports when it draws inside the pick aperture. In JIGSAW, the pick aperture is limited to the correlation point--which is a one-by-one rectangle set by GpiSetPickApertureSize. OS/2 runs through the drawing orders in the indicated segments and registers a hit for those segments that draw within the pick aperture when a call is being made to GpiCorrelateChain, GpiCorrelateFrom, or GpiCorrelateSegment. In the case of JIGSAW, there are typically no more than one or two pieces under any given point on the screen. The most straightforward approach is to make each segment part of the segment chain and then call GpiCorrelateChain. This approach is simple, and you may want to use it in an early version of your program to get it working, but it wastes time, since it involves running through all of the drawing orders in all of the segments to see if one of them intersects the pick aperture. A cleverer approach is to keep track of the bounding box for each segment and only do a correlation check on the segments whose bounding boxes intersect the pick aperture. For this purpose, the double-linked list in priority order is useful. JIGSAW runs through the list from high to low priority, applying the bounding box test to each segment and, if necessary, calling GpiCorrelateSegment (see Figure 18). A refinement of this method of correlation does away with the call to GpiCorrelateSegment by using the knowledge of the shape of each piece stored in the masks. The hole-punching mask contains 1s in the background and 0s in the foreground, so it is easy to tell whether a given pel is on a piece (see Figure 19). As with correlation, there is no point in executing drawing orders that are clipped to produce no visible output. If part of the picture is being healed, say, as part of selecting a piece, then a simple visibility test using the bounding box of each piece will tell you if there is a need to draw it (see Figure 20). Transforms In OS/2 Versions 1.1 and 1.2, the linear translation components of transforms are stored internally to a higher precision than is available to an application. This extra internal precision minimizes rounding errors when combining transforms with the TRANSFORM_ADD or TRANSFORM_PREEMPT options. Another effect of higher internal precision is that if you call GpiQueryDefaultViewMatrix to obtain the current default viewing transformation matrix and immediately call GpiSetDefaultViewMatrix with the same matrix and the TRANSFORM_REPLACE option, you may actually change the default viewing transformation. In the case of JIGSAW, this change produced a one-pel difference in alignment between some parts of the image drawn before and after the change. The remedy was to create the default viewing transformation by means of incremental updates and immediately call GpiQueryDefaultViewMatrix and GpiSetDefaultViewMatrix, discarding the extra internal precision, prior to any drawing (see Figure 21). Second Message Queue In an interactive application, it is important that the system stay responsive to user requests. In the early versions of JIGSAW, some of the drawing operations took many seconds to complete. To solve this problem, a second thread should be used to do the actual drawing, with the primary thread's message queue receiving the original requests--and that in fact was done in later versions. The primary thread can then interrupt drawing or other lengthy operations as needed and recovery can proceed in an orderly fashion. Messages provide a convenient and simple means of transferring requests between threads. In JIGSAW, the second thread has a message queue that receives messages posted by the primary thread. Synchronization Care must be taken in accessing the memory shared by the primary and secondary threads. When possible, variables should be accessed by one thread or the other, but not by both. Semaphores should be used when access to shared memory is required. RAM semaphores themselves can serve as flags. These flags can be freely accessed by both threads by means of OS/2 semaphore calls (see Figure 20). Apart from the critical section issues of accessing shared variables, you also need to preserve the serial nature of a user's drawing requests. If some operations that happen to be fast are handled by the primary thread, and others are put in the secondary thread, the user may see out-of-sequence updates to the screen, producing a result that is messy or incorrect, or both. Merging Messages The most straightforward scheme for handling messages is to retrieve each message from the queue with WinGetMsg and process it. In some cases, however, there will be several consecutive messages all of the same type waiting in the queue, and their combined effect can be quickly determined (for example, throw away all but the last mouse move message). You can use WinPeekMsg with the PM_NOREMOVE option to examine the queue without disturbing it, make a decision, and then remove selected entries from the queue using WinPeekMsg with the PM_REMOVE option. Using JIGSAW as a guide and implementing the techniques I've presented, you should be able to make your GPI programs run more quickly. Because JIGSAW now uses small, equal-sized source and target rectangles with GpiBitBlt, scrolls in memory-aligned steps, draws with masks, uses bounding box tests for painting and correlation, and merges messages in its second message queue, it is much faster than before. For example, the first version of JIGSAW took 32 seconds to correlate a mouse click, erase the selected piece, and heal the erased area. Later versions took only a fraction of a second to complete the same operation. The moral of the story is that when wisely used, PM can yield excellent performance, not to mention happy users. Figure 1 /*---------- inter-thread messages -------------------*/ #define UM_DRAW WM_USER+2 /* draw the current picture */ #define UM_SIZING WM_USER+5 /* perform sizing by recalculating the default viewing transform */ #define UM_ZOOM WM_USER+6 /* zoom the picture by recalculating the default viewing transform */ #define UM_REDRAW WM_USER+8 /* redraw the entire client window */ #define UM_JUMBLE WM_USER+9 /* scatter the pieces on the window */ #define UM_LEFTDOWN WM_USER+11 /* correlate and prepare to drag */ #define UM_MOUSEMOVE WM_USER+12 /* remove, reposition, and redraw */ /*-------------------- segment list ------------------------*/ typedef struct _SEGLIST { /* sl */ LONG lSegId; /* segment identifier */ struct _SEGLIST FAR * pslPrev; /* points to lower-priority neighbor */ struct _SEGLIST FAR * pslNext; /* points to higher-priority neighbor */ POINTL ptlLocation; /* piece location, world coordinates */ RECTL rclCurrent; /* segment bounding box, model coords */ RECTL rclBitBlt; /* segment bounding box, world coords */ POINTL aptlBitBlt[4]; /* BitBlt parameters */ LONG lAdjacent[8]; /* adjacent segments in original pict */ POINTL aptlSides[12]; /* control points for piece outline */ HDC hdcHole; /* punch hole in target */ HPS hpsHole; HBITMAP hbmHole; HDC hdcFill; /* used to fill hole */ HPS hpsFill; HBITMAP hbmFill; } SEGLIST ; typedef SEGLIST FAR *PSEGLIST; /* psl */ typedef PSEGLIST FAR *PPSEGLIST; /* ppsl */ PSEGLIST pslHead = NULL; /* head of the list */ PSEGLIST pslTail = NULL; /* tail of the list */ PSEGLIST pslPicked = NULL; /* picked segment's list member */ /*------------------- Miscellaneous ----------------------------*/ SEL selStack; /* async thread stack selector */ POINTL ptlOffset; /* ptlBotLeft conv to device coords */ POINTL ptlBotLeft = { 0, 0}; /* puzzle boundaries, world space */ POINTL ptlTopRight = { 3000, 3000}; HPS hpsBitmapSize=NULL; /* bitmap sized to the current size */ HDC hdcBitmapSize=NULL; HBITMAP hbmBitmapSize=NULL; BITMAPINFOHEADER bmpBitmapSize={12L, 0, 0, 0, 0}; HPS hpsBitmapBuff=NULL; /* image composed here, copied to scrn */ HDC hdcBitmapBuff=NULL; HBITMAP hbmBitmapBuff=NULL; BITMAPINFOHEADER bmpBitmapBuff={12L, 0, 0, 0, 0}; LONG lByteAlignX, lByteAlignY; /* memory alignment constants */ Figure 8 PSEGLIST psl; POINTL aptlSides[12]; ■ ■ ■ GpiBeginPath( psl->hpsHole, 1L); /* define a path */ GpiMove( psl->hpsHole, &aptlSides[11]); GpiPolySpline( psl->hpsHole, 12L, aptlSides); GpiEndPath( psl->hpsHole); GpiSetClipPath( psl->hpsHole, 1L, SCP_AND); /* make it the clip path */ GpiBitBlt( psl->hpsHole /* fill it with zeros */ , NULL , 2L , &psl->aptlBitBlt[2] , ROP_ZERO , BBO_IGNORE); Figure 9 /* create a shadow bitmap of the original, set to current output size */ ptlOffset = ptlBotLeft; /* hottom left corner in device coord */ GpiConvert( hpsClient, CVTC_WORLD, CVTC_DEVICE, 1L, &ptlOffset); aptl[0] = ptlBotLeft; /* current output rect, device coord */ aptl[1] = ptlTopRight; GpiConvert( hpsClient, CVTC_WORLD, CVTC_DEVICE, 2L, aptl); aptl[0].x -= ptlOffset.x; /* put bottom left at (0,0) */ aptl[0].y -= ptlOffset.y; aptl[1].x -= ptlOffset.x - 1; /* correct for coordinate conversion */ aptl[1].y -= ptlOffset.y - 1; aptl[2].x = 0L; aptl[2].y = 0L; aptl[3].x = bmpBitmapFile.cx; /* bitmap dimensions */ aptl[3].y = bmpBitmapFile.cy; GpiSetBitmap( hpsBitmapSize, hbmBitmapSize); GpiBitBlt( hpsBitmapSize /* copy the bitmap */ , hpsBitmapFile , 4L , aptl , ROP_SRCCOPY , BBO_IGNORE); Figure 13 /* Determine the bounding rect of a segment in model space. */ VOID SetRect ( PSEGLIST psl) { MATRIXLF matlf; GpiResetBoundaryData( hpsClient); GpiSetDrawControl( hpsClient, DCTL_DISPLAY, DCTL_OFF); GpiSetDrawControl( hpsClient, DCTL_BOUNDARY, DCTL_ON); GpiDrawSegment( hpsClient, psl->lSegId ); GpiSetDrawControl( hpsClient, DCTL_BOUNDARY, DCTL_OFF); GpiSetDrawControl( hpsClient, DCTL_DISPLAY, DCTL_ON); GpiQueryBoundaryData( hpsClient, &(psl->rclCurrent)); } /* Determine the bounding rect of a segment in model space. Use special knowledge that the only non-identity part of the segment transform is the translation part. */ VOID SetRect ( PSEGLIST psl) { MATRIXLF matlf; GpiQuerySegmentTransformMatrix( hpsClient /* get model xform of , psl->lSegId segment */ , 9L , &matlf ); psl->rclCurrent = psl->rclBitBlt; /* world space bounding rect */ psl->rclCurrent.xLeft += matlf.lM31; /* apply translation */ psl->rclCurrent.yBottom += matlf.lM32; psl->rclCurrent.xRight += matlf.lM31; psl->rclCurrent.yTop += matlf.lM32; } Figure 14 RECTL rcl; PSEGLIST psl; SetRect( psl); rcl = psl->rclCurrent; /* get model space bounding box of piece */ GpiConvert( hpsClient, CVTC_MODEL, CVTC_DEVICE, 2L, (PPOINTL)&rcl); rcl.xRight++; /* adjust rectangle for conversion to device space */ rcl.yTop++; Figure 15 #define ROUND_DOWN_MOD( arg, mod) \ { \ if( arg < 0) \ { \ arg -= mod - 1; \ arg += arg % mod; \ } else \ arg -= arg % mod; \ } #define ROUND_UP_MOD( arg, mod) \ { \ if( arg < 0) \ { \ arg -= arg % mod; \ } else \ { \ arg += mod - 1; \ arg -= arg % mod; \ } \ } lByteAlignX = WinQuerySysValue( HWND_DESKTOP, SV_CXBYTEALIGN); lByteAlignY = WinQuerySysValue( HWND_DESKTOP, SV_CYBYTEALIGN); WinMapWindowPoints( hwndClient, HWND_DESKTOP, aptl, 2); /* client to scrn */ ROUND_DOWN_MOD( aptl[0].x, lByteAlignX); /* round down */ ROUND_DOWN_MOD( aptl[0].y, lByteAlignY); /* round down */ ROUND_UP_MOD( aptl[1].x, lByteAlignX); /* round up */ ROUND_UP_MOD( aptl[1].y, lByteAlignY); /* round up */ WinMapWindowPoints( HWND_DESKTOP, hwndClient, aptl, 2); /* scrn to client */ aptl[2] = aptl[0]; /* no shift */ GpiBitBlt( hpsClient /* memory to screen */ , hpsBitmapBuff , 3L , aptl , ROP_SRCCOPY , BBO_IGNORE ); Figure 17 VOID DrawPiece( hps, psl) HPS hps; PSEGLIST psl; { POINTL aptl[4]; MATRIXLF matlf; if( GpiQuerySegmentAttrs( hpsClient, psl->lSegId, ATTR_VISIBLE) == ATTR_ON) { GpiQuerySegmentTransformMatrix( hpsClient /* get segment , psl->lSegId transform */ , 9L , &matlf ); aptl[2].x = aptl[2].y = 0L; aptl[3] = psl->aptlBitBlt[3]; aptl[0].x = psl->rclBitBlt.xLeft + matlf.lM31; aptl[0].y = psl->rclBitBlt.yBottom + matlf.lM32; GpiConvert( hpsClient, CVTC_MODEL, CVTC_DEVICE, 1L, aptl); aptl[1].x = aptl[0].x + aptl[3].x; aptl[1].y = aptl[0].y + aptl[3].y; GpiBitBlt( hps /* punch a hole */ , psl->hpsHole , 4L , aptl , ROP_SRCAND , BBO_IGNORE ); GpiBitBlt( hps /* fill the hole */ , psl->hpsFill , 4L , aptl , ROP_SRCPAINT , BBO_IGNORE ); } } Figure 18 LONG alSegTag[HITS][DEPTH][2]; RECTL rcl; POINTL ptl; ptl.x = (LONG)(SHORT)SHORT1FROMMP( mp); ptl.y = (LONG)(SHORT)SHORT2FROMMP( mp); /* scan segment list in order of decreasing priority, looking for a hit */ for( pslPicked = pslTail; pslPicked != NULL; pslPicked = pslPicked->pslPrev) { rcl = pslPicked->rclCurrent; GpiConvert( hpsClient, CVTC_MODEL, CVTC_DEVICE, 2L, (PPOINTL)&rcl); rcl.xRight++; rcl.yTop++; if( WinPtInRect( habAsync, &rcl, &ptl)) /* is point in bounding box? */ if( 0L < GpiCorrelateSegment( hpsClient , pslPicked->lSegId , PICKSEL_VISIBLE , &ptl , HITS , DEPTH , (PLONG)alSegTag )) break; /* got a hit */ } if( pslPicked) lPickedSeg = pslPicked->lSegId; Figure 19 POINTL aptl[2]; LONG lColor; POINTL ptl; RECTL rcl; ptl.x = (LONG)(SHORT)SHORT1FROMMP( mp); ptl.y = (LONG)(SHORT)SHORT2FROMMP( mp); /* scan segment list in order of decreasing priority, looking for a hit */ aptl[0] = aptl[1] = ptl; aptl[1].x++; aptl[1].y++; GpiBitBlt( hpsBitmapSave, NULL, 2L, aptl, ROP_ONE, BBO_IGNORE); lColor = GpiQueryPel( hpsBitmapSave, &ptl); for( pslPicked = pslTail; pslPicked != NULL; pslPicked = pslPicked->pslPrev) { rcl = pslPicked->rclCurrent; GpiConvert( hpsClient, CVTC_MODEL, CVTC_DEVICE, 2L, (PPOINTL)&rcl); rcl.xRight++; rcl.yTop++; if( WinPtInRect( habAsync, &rcl, &ptl)) /* is point in bounding box? */ { DrawPiece( hpsBitmapSave, pslPicked, FALSE); if( GpiQueryPel( hpsBitmapSave, &ptl) != lColor) break; /* got a hit */ } } if( pslPicked) lPickedSeg = pslPicked->lSegId; Figure 20 BOOL DoDraw( hps, hrgn, fPaint) HPS hps; HRGN hrgn; BOOL fPaint; { HRGN hrgnOld; RECTL rcl, rclRegion, rclDst, rclClient; PSEGLIST psl; if( fPaint) { GpiSetColor( hps, CLR_BACKGROUND); GpiPaintRegion( hps, hrgn); /* erase region */ } GpiQueryRegionBox( hps, hrgn, &rclRegion); WinQueryWindowRect( hwndClient, &rclClient); if( !WinIntersectRect( habAsync, &rclRegion, &rclRegion, &rclClient)) return( FALSE); /* not in client window */ GpiSetClipRegion( hps, hrgn, &hrgnOld); /* make the clip region */ for( psl = pslHead; psl != NULL; psl = psl->pslNext) /* scan all segments */ { /* get the piece bounding box in device coordinates */ rcl = psl->rclCurrent; GpiConvert( hpsClient, CVTC_MODEL, CVTC_DEVICE, 2L, (PPOINTL)&rcl); rcl.xRight++; rcl.yTop++; /* if the piece might be visible, and drawing allowed, draw the piece */ if( WinIntersectRect( habAsync, &rclDst, &rcl, &rclRegion)) if( DosSemWait( hsemDrawOn, SEM_IMMEDIATE_RETURN)) DrawPiece( hps, psl); else break; } GpiSetClipRegion( hps, NULL, &hrgnOld); /* clear the clip region */ return( TRUE); } Figure 21 /* set the default viewing transform */ VOID SetDVTransform( fx11, fx12, fx21, fx22, l31, l32, lType) FIXED fx11, fx12, fx21, fx22; LONG l31, l32, lType; { MATRIXLF matlf; matlf.fxM11 = fx11; matlf.fxM12 = fx12; matlf.lM13 = 0L; /* must be 0 */ matlf.fxM21 = fx21; matlf.fxM22 = fx22; matlf.lM23 = 0L; /* must be 0 */ matlf.lM31 = l31; matlf.lM32 = l32; matlf.lM33 = 1L; /* must be 1 */ GpiSetDefaultViewMatrix( hpsClient, 9L, &matlf, lType); } /* Calculate and set the default viewing transform based on zoom and scroll */ VOID CalcTransform( hwnd) HWND hwnd; { RECTL rclClient; POINTL ptlCenter, ptlTrans, ptlScale, aptl[4], ptlOrg, aptlSides[12]; PSEGLIST psl; LONG l; MATRIXLF matlf; /* from bounding rect of picture get center of picture */ ptlCenter.x = (rclBounds.xLeft + rclBounds.xRight) / 2; ptlCenter.y = (rclBounds.yBottom + rclBounds.yTop ) / 2; /* translate center of picture to origin */ SetDVTransform( (FIXED)UNITY , (FIXED)0 , (FIXED)0 , (FIXED)UNITY , -ptlCenter.x , -ptlCenter.y , TRANSFORM_REPLACE); /* scale down to 1:1 of bitmap in file */ ptlScale.x = UNITY * bmpBitmapFile.cx / (ptlTopRight.x - ptlBotLeft.x); ptlScale.y = UNITY * bmpBitmapFile.cy / (ptlTopRight.y - ptlBotLeft.y); /* add in zoom scale */ ptlScale.x += ptlScale.x * lScale / (ZOOM_MAX + 1); ptlScale.y += ptlScale.y * lScale / (ZOOM_MAX + 1); SetDVTransform( (FIXED)ptlScale.x , (FIXED)0 , (FIXED)0 , (FIXED)ptlScale.y , 0L , 0L , TRANSFORM_ADD); /* translate center of picture to center of client window */ WinQueryWindowRect( hwnd, &rclClient); ptlTrans.x = (rclClient.xRight - rclClient.xLeft) / 2; ptlTrans.y = (rclClient.yTop - rclClient.yBottom) / 2; /* add in horizontal and vertical scrolling factors */ ptlTrans.x -= ptsScrollPos.x - ptsHalfScrollMax.x; ptlTrans.y += ptsScrollPos.y - ptsHalfScrollMax.y; SetDVTransform( (FIXED)UNITY , (FIXED)0 , (FIXED)0 , (FIXED)UNITY , ptlTrans.x , ptlTrans.y , TRANSFORM_ADD); /* discard unused precision in default viewing transform */ GpiQueryDefaultViewMatrix( hpsClient, 9L, &matlf); GpiSetDefaultViewMatrix( hpsClient, 9L, &matlf, TRANSFORM_REPLACE); ■ ■ ■ ────────────────────────────────────────────────────────────────────────────

Volume 4 - Number 6

──────────────────────────────────────────────────────────────────────────── Examining NewWave, Hewlett-Packard's Graphical Object-Oriented Environment Alan Cobb and Jonathan Weiner NewWave by Hewlett-Packard offers a wide range of advanced features for Microsoft Windows Version 2.11 graphical environment-based applications. It is in effect an extra layer on top of the MS-DOS operating system, enhancing and extending the services provided by MS-DOS1 and Windows2. There are five areas in which NewWave surpasses Windows: control, communication, integration, abstraction, and ease of use. Because NewWave controls programs and provides new methods of communication between them, users can integrate several programs to do one task easily. Furthermore, NewWave enables the users to deal with the computer at a higher level of abstraction. That is, users can use simpler techniques to do broader, more complicated tasks--similar to the way programmers can do much more with one line of code in a high-level language than with one line of Assembler. Finally, NewWave is easy to use. Its advanced help and Computer-Based Training (CBT) systems make it simple for users to learn how to avail themselves of its services. At the implementation level, NewWave is built upon multiple Windows programs and dynamic-link libraries (DLLs), as well as its data files. Windows developers can access these features by making calls to the NewWave functional interface and conforming to its interapplication communication protocols. Using NewWave Although under the surface NewWave is constructed from normal Windows programs, the user interacts with the program in quite a different way. Instead of beginning work in the MS-DOS Executive with its familiar directory listing, the first thing a NewWave user sees is the NewWave Office window, shown in Figure 1. Users can also access a conventional text listing of the icons, illustrated in Figure 2. The Office can be resized like any other Windows program, but it usually covers the entire screen. On its surface is a set of icons representing familiar office tools such as the Printer, the Waste Basket, and the File Drawer; data objects such as the file folders, simple text documents and images; and compound documents built from smaller objects of text, graphs, and images. While the screen will show only one copy of a tool at a time, it can show multiple instances of data objects. To work with a particular data object, just double click on its icon. NewWave has a record of which tool (EXE file) was used to create that object's data file. The appropriate program is automatically started and the data is read into it. Thus, the user is raised above the details of the file system. NewWave also has a one-step drag-and-drop technique used for operations like copying, deleting, and printing objects. For example, to delete a data object or folder of objects, the user just clicks on the object, drags the mouse to the Waste Basket icon, and drops it in. Features One of the principal benefits of NewWave is its superior ability to deal with compound documents. To get a better feel for NewWave's features, we will look at a simple demonstration system, consisting of a compound monthly report document that contains a nested bitmapped image and a nested spreadsheet. Figure 3 shows the structure of this document. Compound documents are data objects built from a tree of other nested data objects. The component objects can be pieces of text, graphs, spreadsheets, Tagged Image File Format (TIFF) images, or even captured voice recordings or animation sequences. In the example, the pieces are incorporated into a compound object and manipulated by a prototype NewWave-capable word processor called NewWave Write. There are several methods that can be used to combine the nested objects into a larger compound document. The simplest is to drag the icon of the nested object (the source object) and drop it into the larger destination document at the point where it is to appear. This method moves the document completely inside the destination, so the separate source icon will no longer be shown in the Office window. Figure 4 shows the sample object on the screen after it has been constructed. But what if you also want to show the source object, say the spreadsheet, in another compound document at the same time? In that case, you can simultaneously share the single source with multiple destination objects. To do this, highlight the source icon with a mouse click, then select Share from the Edit menu. A reference to the source object is now on the clipboard. Now you can go into one or more destination documents and use the clipboard's familiar Paste command to insert a nested view of the source document. After the pasting, the separate icon for the source will still appear in the NewWave Office window. The full power of NewWave is demonstrated when you need to modify the compound document or one of its components. For example, suppose you decide to add another column to the nested spreadsheet. To access the spreadsheet, simply double click on the area where it is nested. Since the NewWave database of links or views knows which tool and data files were used to create the nested spreadsheet, it can automatically start the tool application and load the spreadsheet for modification. So you can see what is happening, NewWave explodes the nested window and grays the area where it is normally nested. The exploded window will be the main window of the application that was used to create the source object. In our spreadsheet example, the application might be Microsoft Excel. Figure 5 shows the nested TIFF picture being modified. When a conventional program needs to support a new data type, new code to handle that type must be added to the application. Another feature of NewWave is that it eliminates this duplication of code by logically joining all application data files to the programs that created and edited them. When it is time to view or manipulate the source data, the destination object can simply ask the source object to do it. This is the principal sense in which NewWave is object-oriented. In fact, a NewWave object is defined as this paired combination of data and the application required to manipulate it. NewWave is also object-oriented in that objects pass command messages back and forth to each other. The standard term "method" is used to describe the code used by an object to process one of its messages. What if you start modifying the visually linked image separately while the larger compound document is closed? NewWave's database of interobject links--part of the Object Management Facility (OMF) discussed below--takes care of this also. When the larger report is later opened or printed, it will see a flag in the database telling it that the graph has changed. The report object can then ask the graph to rerender the projected view of itself. NewWave allows you to paste the report into an even larger compound document. Nested groups of compound objects can be moved, copied, printed, erased, or mailed all at once, simply by selecting the overall object with the mouse and dragging it to the destination or by copying and pasting it through the clipboard. Advanced Features Now we will extend the sample program to include advanced NewWave features. In its present form the sample monthly report only uses NewWave's visual links (also called visual views). That is, the source objects are only connected to the larger destination visually; there is no actual passing of data between the applications. To pass binary data, such as an array of integers from a spreadsheet, NewWave uses its second main type of link, the data passing link. Figure 6 shows how data passing links can be added to the sample program. We added two new hypothetical applications, a NewWave-capable terminal program and a NewWave-capable graphing program. A NewWave Agent task script, which is like an advanced batch file or macro for Windows programs, has also been added. The script is used to make all the applications work as a team to produce the monthly report. To produce the report, the controlling Agent script first sends the appropriate commands to the NewWave-capable terminal, causing it to dial all of the company's regional offices. The Agent script automatically collects the monthly sales data from each office. Details of the quantities of products sold are passed via a NewWave data passing link to the spreadsheet. As in the first part of the example, the spreadsheet uses a visual link to project the spreadsheet grid into the report, but now it also uses a data passing link to pass the summarized spreadsheet data to a NewWave-capable graphing program. The graphing program then uses a visual link to display itself inside the report. The abstraction of file folder icons to represent directories and the files they contain is another advanced feature of NewWave. To illustrate this feature, the sample also includes the monthly report in a folder of all monthly reports for the year. The folders can be nested inside each other as well as inside the File Drawer icon. Folders are opened with a simple double click. Figure 7 shows the open File Drawer; the Budgets folder within has also been opened. The connection between filed objects is accomplished with NewWave's third type of link, the simple link. Instead of passing views or data these links simply enclose one object inside a parent container object. Other objects can also be inserted into or removed from a container object, such as the Waste Basket, with the drag-and-drop method. OMF and API Systems Now that you are familiar with the features of NewWave, we will describe its major components (some of which were referred to earlier). NewWave consists of two main systems, the OMF and the API (see Figure 8). The API in turn is composed of three smaller systems: the Agent task script system, the Help system, and the CBT. The OMF is used to record and supervise the visual and data links between objects. The API Agent script system handles the recording, playback, and editing of the task scripts used to control NewWave applications. The API, Help, and CBT systems provide a high-quality, prewritten, standard foundation for adding help, demos, and training to NewWave applications. All the objects in a NewWave system are connected in a tree. Near the top of the tree is the NewWave Office. It is a parent to every object that appears in the Office window. For example, the File Drawer is a child of the NewWave Office, and it in turn has its own children in the form of folders. Figure 9 shows one of the diagnostic utilities that comes with the system; this utility allows you to traverse the object tree. A child object is enclosed in a container or acts as the source in a visual or data link. One child can have multiple parents (a graph object, for example, can have visual views projected into three parent reports at the same time), which makes the structure more than a simple tree. Processors Every NewWave application contains two systems, the Action processor and the Command processor, as shown in Figure 10. The Action processor translates user actions, such as mouse clicks and menu selections, into one of the commands that the NewWave application can perform. Often, several different actions will be translated into the same command. For example, using an accelerator key sequence (such as Alt-F-x) can execute the same command as selecting a menu item with the mouse (such as clicking on File, then Exit). The Command processor provides all the actual functionality of the application. It executes commands that are passed to it from several possible sources. For example, commands can come directly from the Action processor as a result of current user keystrokes and mouse movements or be played back from an Agent script. The Command processor must be able to handle the full range of verbs in the application's command language. Agent scripts are built from this set of commands. This is discussed in detail below. Splitting the application's control system into Action and Command processors makes it much easier for NewWave to implement the Agent macro facility. That is, when NewWave records an Agent script, it need not concern itself with the details of the user's actions. It only has to store the series of commands that result from the Action processor's translation. At a lower level, the Action and Command processors are supported by several other processors and components. Figure 11 shows the system in detail. This design enables the application to run in one of five modes: Playback, Record, Intercept (for Help), Monitor (for CBT), or Error mode. In Playback mode, an Agent task script sends commands to the application through the API. In Record mode, the user's actions are translated into commands and stored in an Agent task script. Intercept mode is entered when the user asks for context-sensitive help. The cursor changes to a question mark, and the user clicks on the item of interest. In Monitor mode, all commands are passed to the CBT system before they are executed. This allows the CBT system to discard inappropriate commands during a lesson and guide the user in the right direction. During Error mode, any error notifications are rerouted to the controlling Agent instead of to the normal destination, which is an error message box for the user. Although the system is complex, the hard part of initial design and debugging has been done for you. The NewWave Software Development Kit (SDK) gives detailed examples of how to build the pieces. The components (as opposed to processors) are supplied by Hewlett-Packard. The user need only change the variable names in these components to match his or her system. As shown in the figure, any modeless dialog boxes need to be supported with their own Action processor. The Translate to Internal and Translate to External processors convert commands between Internal and External format. Commands are stored in External binary form by Agent tasks. This is the form passed to the application for execution. The application translates this to its own private Internal format, which can take whatever form is necessary. Compared to Windows NewWave has several advantages over Windows. One is the superiority of the forms of links NewWave has to the methods of interprocess communication in Windows. In Windows there are no standard equivalents of the visual views and simple container links found in NewWave. Nor are there Windows equivalents to the share capability or ability to move, print, copy, and paste compound documents NewWave possesses. These features make it significantly easier to organize and maintain complex objects in NewWave than in Windows. The Windows Dynamic Data Exchange (DDE) protocol does, however, provide some of the same features as the NewWave data passing links. DDE moves data between two applications via messages and shared memory blocks. Both DDE and NewWave data passing links allow one application to update a linked program automatically without requiring the user to take any action. For example, one conventional Windows program can read stock price data from Dow Jones and pass the latest prices via DDE to a graphing program for real-time display. The primary advantage that NewWave data passing links have over DDE is that they provide several fairly complex services that DDE users would have to rewrite and debug from scratch. Not only would a DDE application need considerable added code, that code would have to be duplicated the same way in all the other programs with which the DDE application was going to communicate. NewWave moves much of that common code out of individual applications and into one centralized, standardized, operating-system-style service. Another way in which the NewWave link services go beyond DDE is that they are persistent. When you shut down two programs using DDE, the link will be broken. Under NewWave, a systemwide database in the OMF records a list of the links among all applications. As soon as a link is created, it is recorded in the database. When the NewWave programs are closed, the link persists and will resume operation when the programs are restarted. (DDE applications, on the other hand, would have to reestablish the link each time.) For example, suppose you have linked some numbers from a spreadsheet to a report you are producing with an editor. The editor doesn't have to be on constantly, waiting to get possible changes from the spreadsheet. If the spreadsheet does change, NewWave will automatically set a flag in its link database, indicating that the report needs to be updated when it is reopened. When the editor starts again, NewWave can automatically restart the spreadsheet in the background to rerender the linked data. If the editor were using conventional DDE, it would have to implement its own list of the server applications to which it was linked as well as the type of data passed over each of the links. NewWave removes this burden from individual applications by providing it as an environmental service. Another interprocess communication service unique to NewWave is the snapshot. A snapshot is a special type of object used to reduce the time and memory overhead required for communication views between two objects. Usually when a destination object requests the rendering of a fresh view or more data from a source object, the entire source object must be loaded and run. This could mean loading an entire spreadsheet application and a large worksheet just to access three numbers somewhere in the worksheet. NewWave enables you to provide a small snapshot object that can render only a particular linked view. If a snapshot is present when a destination object requests the rerendering of a view, NewWave will reroute the request, instead of the full application, to the destination object. A snapshot is implemented as a small DLL with an associated data file. It loads faster and requires less memory because it is a DLL and not a full process. A snapshot doesn't have to contain the user interface or any other code beyond what is required to render that one view. Conventional DDE, on the other hand, would require both complete applications to be present in memory in order to pass the new data. Another way in which NewWave extends Windows is in its task script language, called Agents. Agents are comparable to extended BAT files that control Windows programs. Windows Version 2.1 currently has no built-in control script facility of this kind. Although under Windows it is possible to write journaling programs that record and play back series of keystrokes and mouse movements, the NewWave Agents function at a cleaner and more fundamental level. Unlike journaled keystrokes and mouse movements (which are just recorded user actions rather than commands), Agents will work regardless of how many applications are present in the Office and where they are positioned. The task language has many standard statements, including opening, closing, minimizing, and maximizing applications, that correspond to commands shared by most NewWave programs. The most interesting feature of the Agent language, however, is its extensibility to support individual applications. Each NewWave program defines its own command language and implements a parser to translate it. A full NewWave application must define statements in its command language to support all its menu items and their parameters. The goal is for the Agent to be able to do anything the user can do directly. See the sidebar "NewWave Command and Function Summary," which has some commands from the Agent task language. An Agent can control individual applications by communicating with them directly in their own language of commands. As a result, NewWave applications no longer need the extra code necessary to support their own nonstandard internal macro languages. Users can control all their NewWave applications with a single task language. Agent scripts can be generated by capturing a series of user commands (see Figures 12 and 13) or by creating them directly with an ordinary text editor. The language supports control structures such as loops, branching, and procedures; it also supports integer, float, and string variables. For performance, the scripts are compiled into a binary form before being run. In the future, a built-in scheduling system will allow Agents to perform tasks at specified times, such as a single time or regularly every day or week (see Figure 14). An Agent could be told to wait for a trigger event before it starts running. For example, the receipt of a piece of E-mail could trigger the data being placed into a report. Figure 15 shows a simple Agent task script. Note that because this article was written before the release version of NewWave was ready, the PASTE and CLOSE commands had to be used in intermediate pcode form. NewWave also adds to Windows with its user support. NewWave includes prewritten CBT, Help, and Native Language Support (NLS) systems. NewWave is in a unique position to offer first-rate Help and CBT services because of the similarity of its Help, CBT, and Agent macro facilities. All three systems are concerned with monitoring and controlling the execution of individual applications. In fact, the CBT lessons are written in an extended form of the Agent macro language. As you have seen, all user actions must pass through a NewWave application's Action Processor before being translated into commands that are executed by its Command Processor. The CBT system can watch and modify this traffic of commands to control the user's interaction with the system. The CBT can, for example, intercept, recognize, and point out correct and incorrect user responses during a lesson. NewWave provides tools for creating CBT and Help documents. Its built-in CBT animation development system, shown in Figure 16, helps developers design documentation and Help files at the same time they are writing the program. Conventional CBT systems often attempt to simulate the behavior of specific parts of the application. Rather than adding new simulation code, NewWave CBT simply uses the application itself by sending its commands directly to the Command Processor. Converting Applications NewWave has built-in support to help convert an application to other languages and customs. First, a single generic version of the program is written. Later, nontechnical translation workers can use the tools NewWave provides to adapt the local character handling (see Figure 17) and customs support without changing any code. In order to see how NewWave capabilities can be added to an existing Windows application, we will look at portions of the code for a Shapes program that comes with the Windows SDK. The NewWave version is called HPShape. Shapes does only one thing: it draws one of four geometric figures selected from its menu--a triangle, an ellipse, a rectangle, or a star. HPShape has been made into a typical source object that projects its visual view into a destination object. The NewWave SDK includes a second program called HPLayout as a sample destination object. To nest a visual view of HPShape in HPLayout, mark off an area inside the HPLayout window with the mouse. The nested area is shown in reverse (white foreground on a black background). To project the HPShape view into this small area, drag HPShape's icon over it and drop it in. If you want to manipulate the nested HPShape object, double click on the nested area to bring up HPShape's main window. Figure 18 shows the compound document, SDK NW Layout Demo, with its two nested children open. The areas where they normally appear in the document are gray. The object called Nestable HP Text is another simple source object that comes with the NewWave SDK. It projects a visual view of text. Getting NewWave's additional functionality comes at the price of a fair amount of extra code. Whereas the simple Shapes WinApp weighs in with only 7Kb of C source code, the HPShape and HPLayout applications require 55Kb and 157Kb of C source code, respectively. Of course, that added code gives you a help system, a programming language, and data and visual linking capability. The amount of added NewWave code is also relatively fixed in size. For a normal sized program it will be a smaller percentage of the total code. Figures 19 and 20 are calltrees for the programs produced with Microsoft CALLTREE.EXE utility. Calltree listings show the hierarchy of function calls for a C program. The calls made from a function are indented beneath it. Figure 19 shows the structure of the pre-NewWave Shapes; Figure 20 shows the HPShape NewWave program. The full source code for three HPShape functions (ShapeWndProc, ActionProcessor, and CommandProcessor) is shown in Figures 21, 22, and 23. The data file referred to in the figures is used to record the specific shape that was being displayed in the HPShape window. This means that even after the NewWave application is closed, its state persists and when it is reopened this state is restored. As with all Windows and Presentation Manager programs, a NewWave program is essentially a large message processor. The program spends its life responding to the spectrum of messages that enter its main window procedure. It separates the messages into general categories and passes them to more specialized handlers. Figure 24 contains the messages recognized by the pre-NewWave Shapes; Figure 25 contains the considerably broader list to which HPShape must respond. Conventional Applications In order for applications to exploit NewWave fully, they must be specifically written to interact with its new interfaces. But while these fully NewWave-capable applications are being written it is important for NewWave to be able to interact with any conventional MS-DOS and Windows programs such as Lotus 1-2-3. NewWave provides several methods for encapsulating these existing applications to make them more functional in NewWave. At its lowest level, NewWave allows existing programs to be run from a menu. This requires no encapsulation at all. Windows applications as well as character mode or graphic MS-DOS programs that take over the whole screen can be run this way. The user can context switch between the program and the rest of NewWave and use the clipboard to cut and paste between it and other applications. Integrating a new program to operate at this level takes only a few minutes. Moving up the integration scale requires the encapsulation of the existing program into a shell created by an interactive installation tool provided by NewWave. The lowest level of encapsulation takes about an hour. It allows data files created by the program to be represented as an icon in the NewWave Office. These objects can be manipulated in most of the same ways that a full NewWave application can. They can be opened with a double click or moved, copied, filed, mailed, or discarded with the drag-and-drop technique. The disadvantages are that they allow neither data nor visual links to other objects nor can they use the full Agent task language, help, or training systems. The highest level of encapsulation requires considerable programming, but doing that programming is significantly easier than completely rewriting the application as a native NewWave application. The new code primarily adds support for data and visual links. It consists of a browser program that understands the format of the application's data and can talk to the NewWave OMF. For example, when a destination object sends a message asking an encapsulated source object to display a view of itself in a given rectangle, the browser reads the object's data file and displays the data to the screen. If it is necessary to edit the data, the browser can invoke the necessary application to do so. A full NewWave application goes further by providing complete support for the Agent task language, context-sensitive help, NewWave user interface, and Computer-Based Training. Machine Requirements The minimum hardware that HP recommends for NewWave users is a 286 PC/AT or 100 percent compatible, 3Mb of LIM 4.0 EMS memory, a 20Mb hard disk, and an EGA display (a VGA is significantly better). Developers should increase this to 4Mb of EMS memory. A 40Mb or larger hard disk is recommended. Developers will also need Microsoft Windows/286 Version 2.11, and Microsoft C 5.1 or higher. As with most Windows or Presentation Manager work, a fast 386 machine significantly increases productivity. NewWave enhancements to Windows programs come at the cost of increased code, although that cost can be lessened by choosing the level of encapsulation your program requires. Windows programmers who are interested in extending and enhancing their Windows programs must give serious consideration to programming for the NewWave environment. A NewWave Glossary: NewWave has its own definitions for a number of terms. Most of them are illustrated in Figures 3, 4 and 5. NewWave Terms: Objects All the icons in the NewWave Office window represent either tools or data objects. Office Tools or System Objects Tool objects are tools such as the Waste Basket, File Drawer, and Printer. They are associated with some type of fixed system service. They cannot be copied or deleted by the user. User Objects User objects are objects that can be freely copied, deleted, cut, pasted, or shared by the user. Data Objects Data objects are the combination of an application that creates and manipulates data and a specific data file that was created with it. For example, a word processor and a memo created with it would form one object. Under NewWave the user doesn't work with applications but rather with these bundled pairs of application and data. Compound Objects Compound objects are built from a combination of smaller objects. For example, a spreadsheet object could have several small text note objects attached to it to explain some of the calculations. The aggregate group can be copied, printed, and moved as a single entity via the clipboard. Container Objects Containers are objects like the File Drawer, File Folders, and Waste Basket, that are used to hold objects in a group. They are represented by a single icon in the Office window. Containers can be opened to show the objects they hold. Views or Links Objects can be connected to one another in several different ways called links or views. Visual Views or Links In a visual link, one object projects a view of itself into another. The projected object is shown nested inside the destination object. For example, a graph object could be connected with a visual view to a text report in which it appears. The nested object does all the drawing of its own view. Data Passing Links Data links or views actually pass pieces of binary data between objects. For example, a communications program could pass stock prices to a spreadsheet object for analysis. Simple Links Container objects are connected by simple links to the objects they enclose. There is no passing of data or visual views between them. Source and Destination Objects The source object is the one either sending binary data via a data link or else projecting a view of itself via a visual view into a destination object. Child and Parent Objects A child object is either a source object or an object held within a container. A parent object is either the destination of a visual or data link or else a container holding other objects. One object can be simultaneously both a parent to objects below it and a child to objects above it. Other Terms: API Although API (Application Program Interface) is normally a generic term that refers to any functional interface to a subsystem, NewWave uses it to refer specifically to the interface to its Agent, CBT, and Help systems. Methods Objects communicate among themselves and with NewWave by sending messages to each other. For example, the message DISPLAY VIEW is sent from a parent object to a child during the setup of a visual link. The code in the child that processes a particular message is referred to as a method. Agent An Agent is like a batch or macro file for controlling data objects, tools and other programs in the NewWave environment. The Agent's commands are written in an Agent task script language. Individual scripts are shown in the NewWave Office as icons. Share Although one child object can be moved or pasted completely into a single parent object, it can also be shared into the same object.A child can be shared into multiple objects at once. For example, one chart could appear in several reports at the same time. When the child chart is updated, it would be shown in its updated form in all the parent report objects. NewWave Command and Function Summary The Class Independent Commands are executed by the Agent itself at run time, independent of any application object that may be open. Most of them either manipulate task conversational windows or handle flow control of the Agent task. Command Name Description CLEARWINDOW Clear a user conversational window CLOSEWINDOW Close a conversational window DEFINEWINDOW Define a user conversational window DO Execute a procedure EDITBOX Create an edit box in a conversational window END Terminate execution of a task FOCUS Change focus to a specified object GOTO Transfer control to a labeled statement IF ELSE ENDIF Conditional execution INPUT Create a window to prompt the user for input JUSTIFY Justify text in a conversational window LABEL Define a label LOCATE Position the cursor in a conversational window MESSAGE Create a message window (OK, RETRY, and so on) ON ERROR DO Trap on any error condition ON ESCAPE DO Trap on the escape key ON TIMEOUT DO Trap on a timeout OPENWINDOW Open a conversational window OUTPUT Output text to a conversational window PAUSE Halts execution temporarily PROCEDURE ENDPROC Define a procedure PUSHBUTTON Draw a pushbutton in a conversational window RETURN Exit a procedure SCREEN Map logical screen and window coordinates SET ERROR Set error trapping on or off SET ESCAPE Set escape trapping SET RATE Sets rate at which Agent executes commands SET TIMEOUT Set timeout trapping TASK ENDTASK Defines the main body of a task script TITLEWINDOW Set caption bar text of conversational window WAIT Suspend execution until an event is trapped WHILE ENDWHILE Looping of control Class Independent Functions for Data Manipulation Function Name Value Returned EDITBOX String of text in an edit box TESTBUTTON Tells if a given button was pushed ABS Absolute value of a numeric argument ASC Integer for a given string CHR String for a given integer FIND Location of substring in a string INT A long when passed a real LEFT String of leftmost characters LEN Number of characters in a string MID Extracted substring MOD Remainder for two integers RIGHT String of rightmost characters STR ASCII string given a numeric argument SYS_ERROR Error number of last run-time error NUM Tells if parameter is numeric VAL Numeric value of input string Class Dependent Commands for the NewWave Office Window Individual applications have their own unique set of commands that they will accept. These are called Class Dependent Commands. Below is a subset of the Class Dependent Commands for the NewWave Office. Command Name Description ABOUT? Display "About ..." dialog box ACTIVATE Change the currently active window ADD_SELECTION Select the specified object ADJUST_WINDOW Move or size the current window ALIGN_BY_ROWS Align the icons in the Office window AUTO_ALIGNMENT Set auto_alignment (snap to grid) mode CHANGE_ATTRIBUTES Change attributes of a selected object CHANGE_TITLE Change an object's title CLOSE Close the currently active window CONTROL_PANEL Execute the Windows Control EXE program COPY Copy an object to the clipboard COPY_TO Copy an object to a closed container CREATE_A_NEW Create a new object CUT Delete selected objects to the clipboard EXPORT_TO_DISK_FILE Serialize an object's data to a disk file ICONIC_VIEW Display objects in HP Office as icons IMPORT_FROM_DISK_FILE Deserialize an object from a disk file LIST_VIEW Display a container's objects as a list LOCK_DISPLAY Display dialog box to record password MAKE_COPY Copy all selected objects in the window MANAGE_MASTERS Remove objects from the workspace MANAGE_TOOLS Change the selection of tools in the Office MAXIMIZE Increase the current window's size MOVE_TO Move selected objects to a container OPEN Open all selected objects OPEN_SELECTED_OBJECT Open an object PASTE Paste objects from the clipboard PERFORM Perform an Agent task PRINT Drop selected object on printer icon PRINT_LIST_OF_OBJECTS Make a hard copy of container contents RESTORE Return a window to its default size SAVE_AS_MASTER Save a copy of the object as a template SELECT Select one object by class and title SELECT_ALL_OBJECTS Select all objects in the current window DESELECT_ALL Deselect all the objects in the window DESELECT Deselect one object by class and title SELECT_OPENED Select an open object by class and title SEND_TO_MAILROOM Insert selected objects into the mail room SET_PASSWORD? Display the "Password" dialog box SET_USER_TIME_ZONE Modify user name and time zone data SHARE Share selected objects to the clipboard SHOW_DOS_PATH Display path of an MS-DOS application SHOW_LINKS Show the links to an object SHOW_OWN_LINKS Show the links to an object's parents OPEN_PARENT Open a linked parent STRAIGHTEN_UP Snap objects to the nearest grid point THROW_AWAY Move selected objects to the Waste Basket TRANSFER_MAIL Initiate a send or receive mail transfer Class Dependent Commands for the HPSHAPE Sample Application This is the unique set of commands for the HPSHAPE sample program discussed in the text. The commands correspond to the items on HPSHAPE's menus. Notice that there is some overlap with the Office window's command set. Command Name Description CLEAR Clear the object's window CLOSE Close the currently active window MAXIMIZE Increase the current window's size RESTORE Return a window to its default size ELLIPSE Display an ellipse RECTANGLE Display a rectangle STAR Display a star TRIANGLE Display a triangle Figure 16 '****************************************************************** '* Simple NewWave task script example. Copies a bitmapped image * '* object called "Image: Our New CFO" to the clipboard, then * '* pastes this into a compound document called "Demo Report." * '* It repeats this append operation in a loop until the user * '* decides to exit. Author: Alan Cobb * '****************************************************************** TASK bContinue# = 1 WHILE bContinue# = 1 FOCUS OFFICE "NewWave Office" ' Set keyboard focus. SELECT NEWWAVE_WRITE "Demo Report" OPEN ' Open report. SELECT NWImage "Image: Our New CFO" COPY ' Copy to clipboard. FOCUS NEWWAVE_WRITE "Demo Report" DO PASTE_INTO_REPORT ' Paste image. DO CLOSE_REPORT ' Save result. MESSAGE bContinue# "Repeat process?" yesno ' Loop again? ENDWHILE MESSAGE bTemp# "Exiting." ok ' Pause before exit. END ENDTASK '****************************************************************** '* Equivalent of PASTE command in pcode form. '****************************************************************** PROCEDURE PASTE_INTO_REPORT PCOMMAND 4 PCODE "0400CD00" RETURN ENDPROC '****************************************************************** '* Equivalent of CLOSE command in pcode form. '****************************************************************** PROCEDURE CLOSE_REPORT PCOMMAND 8 PCODE "0800020001000100" RETURN ENDPROC Figure 19: WinMain < Initialization and main event loop. | ShapesInit < Initialize main window. ShapesWndProc < Handle all messages sent to Shapes. ShapesCommand < Process menu selections. ShapesPaint < Paint the requested shape. ShapesSetupDC DrawRect DrawEllipse DrawTriangle DrawStar Figure 20: WinMain < Initialization and main event loop. | ShapeInit < Initializes HPSHAPE'S window | ShapeCreateWindow < Creates a NewWave main window. ShapeWndProc < Handle all messages sent to HPSHAPE. TranslateToInternalProcessor < Take the external command from the buffer. < Change external command into the internal < command format. TranslateToExternalProcessor < Convert the internal command format < into the external command format. ActionProcessor < Handle all of the user actions. Prepare | < them for Command Processor InterrogateFromAPI < Handle context-sensitive help requests. MessageFromOMF < Process messages sent from the OMF. ReadDataFile < Get the data file for HPSHAPE. HasMethod < Reply to the HAS_METHOD message to < indicate whether a method is supported by | < HPSHAPE. InitView < Establish a view definition for < the data to be displayed in the < requesting object. GetSize < State the size HPSHAPE needs to be in | < order to display itself as a view. DisplayView < Paint the view in the DC specified | < by the destination object. CopySelf < Allow the object to be copied when | < contained in another object. SaveDataFile < Save the data file that is the last < shape selected. SaveDataFile... SaveWindowPosition < Saves the last window coordinates in a | | < property. ShapePaint < Paint the requested shape. CommandProcessor < Handle any commands passed from the | < ActionProcessor or API (for example, Agent). SaveWindowPosition... Figure 21: /************************************************************/ ShapeWndProc Main procedure to handle all messages sent to HPSHAPE COPYRIGHT HEWLETT-PACKARD COMPANY 1987, 1988 /************************************************************/ long FAR PASCAL ShapeWndProc( hWnd, message, wParam, lParam ) HWND hWnd; unsigned message; WORD wParam; LONG lParam; { APIRTNTYPE applRtn; /* Used as a return value */ APICMDSTRUCT extCmd; /* The external command structure for API */ INTCMDSTRUCT intCmd; /* The internal command structure for API */ /* Function called for windows that are created with */ /* NW_CreateWindow to specially handle some messages */ if( NW_MessageFilter(hWnd, message, wParam, lParam, (LONG FAR *) &applRtn)) { return (applRtn); } /* Is API to intercept messages or has an API menu item been */ /* selected such as the help or the task? */ if( APIInterceptOn(gAPIModeFlags) || APIHaveMenu (message, wParam)) { /* Is this a message for the API - may set certain flags? */ APIUserActionInterface(ghAPI, hWnd, (LPAPIUNSIGNED)&message, wParam, lParam, API_NO_MODE); } applRtn = (APIERRTYPE)0L; /* Still a command to be handled? Or did APIUserAct take it? */ if( APIHaveMessage (message) ) { intCmd wCmd = API_NO_CMD; /* Are you currently playing back a message or recorded task? */ if (APIPlaybackMsg(message)) /* Translate to the internal format of the application */ TranslateToInternalProcessor(message, wParam, lParam, &intCmd); else /* You are in record mode or a user interactive command */ ActionProcessor(hWnd, message, wParam, lParam, &intCmd, &applRtn); /* Is there a command created in the action processor that needs to be executed? */ if( APIHaveCommand(intCmd.wCmd) ) { gapplErr = API_NO_ERR; /* Are you in a CBT? */ if( APIMonitorOn(gAPIModeFlags) ) { /* Set to external language and pass the external form of the command to the Agent */ TranslateToExternalProcessor(&intCmd,&extCmd); APICommandInterface(ghAPI, (LPAPICMDSTRUCT)&extCmd, API_NO_MODE); /* The internal command must be canceled if the */ APICommandInterface or APIClgCommandInterface has nullified the command */ if (extCmd.wCmd == API_NO_CMD) intCmd.wCmd = API_NO_CMD; } /* Has the command been formed and executed? */ if( APIHaveCommand(intCmd.wCmd) ) /* Perform the command */ CommandProcessor(hWnd, message, wParam, lParam, &intCmd, &applRtn); /* Is a command being played back or being recorded? */ if( APIPlaybackOn(gAPIModeFlags) || APIRecordOn (gAPIModeFlags)) { if( APIRecordOn(gAPIModeFlags) ) { /* Translate to ext lang and tell Agent that command is complete and ready for next command. */ TranslateToExternalProcessor(&intCmd,&extCmd); APIRecordInterface(ghAPI,(LPAPICMDSTRUCT)&extCmd, API_NO_MODE); } /* Return control to the user */ APIReturnInterface( ghAPI, gapplErr, API_NO_MODE ); } } /* EndIf of APIHaveCommand */ } /* EndIf of APIHaveMessage */ return(applRtn); } /* End of ShapeWndProc */ Figure 22: /********************************************************************/ ActionProcessor Handle all of the user actions - set up intCmd for Cmd Processor COPYRIGHT HEWLETT-PACKARD COMPANY 1987, 1988 /********************************************************************/ void PASCAL ActionProcessor (hWnd, message, wParam, lParam, intCmd, pRtn) HWND hWnd; unsigned message; WORD wParam; LONG lParam; PINTCMDSTRUCT intCmd; LONG *pRtn; /* The return value of the procedure */ { PAINTSTRUCT ps ; /* The paint structure */ /* This routine actually builds a command */ switch( message ) { /* Was a paint message sent to update the screen? */ case WM_PAINT: BeginPaint (hWnd, (LPPAINTSTRUCT)&ps); ShapePaint (hWnd, (LPPAINTSTRUCT)&ps); EndPaint (hWnd, (LPPAINTSTRUCT)&ps); break; /* Was there a menu selection? */ case WM_COMMAND: /* Was the close option chosen? */ if (wParam == IDDCLOSE) intCmd->wCmd = API_CLOSE_WINDOW_CDCMD; else { /* Something was selected from the shape menu */ intCmd->wCmd = NEW_SHAPE; intCmd->internal.ICmd = wParam; } break; /* The window being closed */ case WM_CLOSE: intCmd->wCmd = API_CLOSE_WINDOW_CDCMD; break; /* Selection from system menu or minimize/maximize */ case WM_SYSCOMMAND: switch (wParam) { case SC_MINIMIZE: intCmd->wCmd = API_MINIMIZE_WINDOW_CDCMD; break; case SC_MAXIMIZE: intCmd->wCmd = API_MAXIMIZE_WINDOW_CDCMD; break; /* Request for saving previous coordinates */ case SC_RESTORE: intCmd->wCmd = API_RESTORE_WINDOW_CDCMD; break; default: /* Let windows handle it */ *pRtn = DefWindowProc(hWnd, message, wParam, lParam); break; } /* End of case WM_SYSCOMMAND */ break; case API_INTERROGATE_MSG: /* The message has come via the API */ *pRtn = InterrogateFromAPI(wParam, lParam); break; case API_SET_MODE_FLAGS_MSG: if (wParam == API_SET_MODE_ON_FLAG) gAPIModeFlags = gAPIModeFlags | lParam; else gAPIModeFlags = gAPIModeFlags & lParam; break; case WM_OMF: /* The message has come via the OMF */ *pRtn = MessageFromOMF(hWnd, wParam, lParam); break; default: *pRtn = DefWindowProc(hWnd, message, wParam, lParam); break; } } /* End of ActionProcessor */ Figure 23: /*********************************************************************/ CommandProcessor Handle any of the commands passed from the ActionProcessor or API COPYRIGHT HEWLETT-PACKARD COMPANY 1987, 1988 /*********************************************************************/ void PASCAL CommandProcessor (hWnd, message, wParam, lParam, intCmd, pRtn) HWND hWnd; unsigned message; WORD wParam; LONG lParam; PINTCMDSTRUCT intCmd; LONG *pRtn; /* Application return error code */ { HMENU hMenu; RECT rcRect; switch( intCmd->wCmd ) { case API_MINIMIZE_WINDOW_CDCMD: if (IsIconic(hWnd)) NW_Restore(hWnd); else { if (!IsZoomed(hWnd)) GetWindowRect(hWnd, (LPRECT)&gWinPosn.rcRect); NW_Minimize(hWnd); } break; case API_MAXIMIZE_WINDOW_CDCMD: if (IsZoomed(hWnd)) NW_Restore(hWnd); else { if (!IsIconic(hWnd)) GetWindowRect(hWnd, (LPRECT)&gWinPosn.rcRect); NW_Maximize(hWnd); } break; case API_RESTORE_WINDOW_CDCMD: NW_Restore(hWnd); break; case API_CLOSE_WINDOW_CDCMD: SaveWindowPosition(hWnd); GetWindowRect(hWnd, (LPRECT) &rcRect); ShowWindow(hWnd, SW_HIDE); UpdateWindow(hWnd); APINotReady(ghAPI, API_NO_MODE); if (!OMF_Closing(ghOMF, (LPRECT) &rcRect)) NoteError(); break; case NEW_SHAPE: /* Get the handle to the menu of the current window */ hMenu = GetMenu(hWnd); /* Uncheck the old menu item */ CheckMenuItem(hMenu, gnShape, MF_UNCHECKED); /* Check the new menu item */ if ((gnShape = intCmd->internal.ICmd) != SHAPE_NONE) CheckMenuItem(hMenu, gnShape, MF_CHECKED); /* Send a paint message */ InvalidateRect(hWnd, (LPRECT)NULL, TRUE); UpdateWindow(hWnd); /* Force repaint for every shape, not just last shape, when playing back several NEW_SHAPE commands */ /* Are there views of the shape program in any other */ /* object? Set the new_data flag for all views of */ /* shapes */ if (OMF_SetNewData(ghOMF, 0)) { /* Notify the destinations */ if (!OMF_AnnounceNewData(ghOMF)) NoteError(); } break; default: NoteError(); break; } } /* End of CommandProcessor */ Figure 24: Conventional Windows applications deal with two primary types of messages. Messages resulting from user actions These result from mouse clicks, mouse movements, menu selections, or keystrokes. They are acted on immediately. WM_COMMAND Results from menu selection WM_SYSCOMMAND User selected something from system menu or minimize/maximize MS Windows housekeeping messages WM_CREATE Sent when CreateWindow is called WM_PAINT Sent to update the screen WM_ERASEBKGND Sent when window background needs clearing WM_DESTROY Terminate the message loop Figure 26: Messages resulting from user actions These messages result from mouse clicks, mouse movements, menu selections, or keystrokes. They are not acted on directly, but rather are translated by the Action Processor into commands for the Command Processor in the application's own task language. (See message type 5.) WM_COMMAND Results from menu selections. WM_SYSCOMMAND User selected something from system menu or minimize/ maximize. OMF Messages These messages come from the OMF as WM_OMF messages. They are used to communicate either with the OMF itself or with other objects. They each correspond to an OMF method that this application implements. A method is the code that is executed in response to a given message: for example, the code needed to create, open, or terminate this object. CREATE_OMF Respond by reading data file to remember the "persisting" state of the object when it was last closed. Was it a triangle, and so on. WARM_START Indicates that the receiving object was shut down in a consistent state the last time it was run. The object can treat as valid any context or state data that was saved at shutdown. OPEN Respond by reading properties to find the last size and position of window. Move the window there. HAS_METHOD Respond to say if a particular OMF "method" is supported by HPShape. INIT_VIEW Respond by setting up a view specification for the data to be displayed in a requesting object. GET_SIZE Respond by saying what size HPShape needs to allow it to display itself as a view in a destination object. DISPLAY_VIEW Paints the visual view from HPShape in the DC specified by a destination object. COPY_SELF Allows this object to be copied when it iscontained in another object. TERMINATE Respond by saving current shape in data file. DIE_PLEASE Tells this object to terminate itself. WINDOW_TO_TOP Tells this object to bring its window to the front. NewWave API messages User request Agent request Interrogate messages API_INTERROGATE_MSG A request for information from the application. API_SET_MODE_FLAGS_MSG Used to change mode the application is in, such as Playback, Record, and Monitor. MS Windows Housekeeping messages WM_PAINT Sent to update the screen. WM_CLOSE User closing window. Internal Messages for the Command Processor These messages originate inside the application. They may result from a translated user action or from an Agent script being played back. Each corresponds to a command in the application's task language. API_MINIMIZE_WINDOW_CDCMD Requests application to iconize itself API_MAXIMIZE_WINDOW_CDCMD Requests application to grow to take up the whole screen. API_RESTORE_WINDOW_CDCMD Requests application to return its window to the previous size. API_CLOSE_WINDOW_CDCMD Requests application to close its main window. NEW_SHAPE Rerender the current shape. Emulating the UNIX RS-232 General Serial I/O Interface Under DOS Michael J. Chase One popular means of transferring information between DOS1 or UNIX applications and terminals or control devices is the use of RS-232 serial communication. This article focuses on writing applications that rely on RS-232 serial communication under both DOS and UNIX. We will explore the generalized serial I/O interface provided under UNIX and a device driver that emulates it under DOS. UNIX programmers have a general interface for asynchronous serial devices that is independent of hardware; it has many useful features and, once understood, is very easy to work with. Programmers working in the DOS environment, however, usually can't use the COM1 or COM2 serial device interfaces because they are not interrupt driven and do not support buffering or XON/XOFF handshaking. Third-party communications libraries are often called upon to help, but most require the C programmer to learn at least 20 function calls to a proprietary interface--20 more than most people would prefer to have to learn. Moreover, third-party communication code usually will not port to UNIX-based systems or to another vendor's DOS communication libraries. Documentation for the generalized UNIX serial device seems incredibly terse and hard to read if you are not familiar with the many details surrounding asynchronous communications. After some explanation, however, the serial device interface becomes easy and convenient to use. Usable features (buffering, XON/XOFF handshaking, watchdog timers, parity control, exception handling, line disciplines, and so on) of this general interface are presented so that portable device control software can be written for general and embedded applications under both UNIX and DOS. Trade-offs in communications software design (buffer sizes, communication attributes, error recovery, and so on) are also discussed. Communication Device Drivers A major goal of all device drivers is to provide a logical software interface for applications that isolates them from physical hardware. The physical hardware can differ; however, the logical software interface to the hardware (device) remains the same. This is true for block-oriented devices (disk interfaces) as well as character-oriented devices (serial port interfaces). Both UNIX and DOS provide block and character device drivers that allow C programs to read and write bytes by using standard library calls through the file system. Familiar I/O function calls such as open, read, write, and close (level 1 functions) or stream-buffered functions such as fopen, fread, fscanf, fwrite, fprintf, and fclose (level 2 functions available in the standard library) can be used to communicate with a file or a serial device. These functions afford programmers writing serial I/O software in UNIX and DOS portability across many hardware platforms. There is one problem though. The default asynchronous device driver shipped with DOS is not very powerful because it does not allow you to change speed, parity, buffer control, handshaking, and so on, independent of the hardware. To control a serial device from a C program, calls to the BIOS must be made--a serious impediment to portability. Fortunately there are three viable methods you can use to write serial I/O software for the DOS environment: you can build your own communications software, use linkable communications libraries provided by a third party, or use a device driver capable of emulating the popular UNIX serial I/O interface under DOS. Building Software Building a communications functions library from scratch has certain advantages. It gives you control over exactly what you need, it helps you learn about communication port hardware, and it gives you ownership of source code. By and large, though, you will be reinventing the wheel. You will need a hardware debugger and/or a logic analyzer to catch the most subtle bugs, especially those found in interrupt logic. You will also need to invest a great deal of time in supporting, updating, and maintaining the software--in other words, you will become your own technical support staff. Third-Party Libraries Third-party add-on communication libraries may help you write serial I/O programs by providing a C language interface for controlling device attributes. Your C code, however, will be married to these typically nongeneral interfaces under DOS. Therefore, you must weigh both the advantages and disadvantages of this scheme. An advantage of third-party libraries is their highly granular control over speed, parity, stop bits, XON/XOFF, buffer management, and so on. Another advantage is not having to install a device driver, since device drivers have to be loaded when a system is booted and then remain part of the operating system. I/O functions loaded with your programs, however, are not resident in the operating system, do not consume any space when your program exits, and do not incur operating system overhead to pass information through the file system. One disadvantage is that the interface to a third-party library is not through the file system; another is that calls to fopen, fread, fscanf, fwrite, fprintf, and fclose will not work. You must devote the time to learn a new set of interface functions supported by a given vendor. Some interfaces are unnecessarily complex; one commercial vendor, for example, boasts more than 125 serial communication function calls. Another disadvantage is that simple command line (or batch file/shell script) redirection is not supported. For example, under DOS the simple command line C> dir > \device\tty01 will not work. Further, the logical interface is vendor specific; C code that is portable between DOS and UNIX is nearly impossible to write. Finally, some third-party libraries are designed as terminate-and-stay resident (TSR) and consume memory. If you only develop for DOS and portability is not an issue, or if you only need a serial device interface for DOS that emulates the widely accepted UNIX serial interface, the advantages probably outweigh the disadvantages. In that case, and with the bridging of the UNIX and DOS worlds, this option deserves consideration. Emulating UNIX Serial I/O A UNIX-compatible serial device driver under DOS provides a number of useful advantages. First, it gives you sufficient control over speed, parity, stop bits, XON/XOFF, buffer management, and so on. Second, C code that controls serial I/O is portable between DOS and UNIX. UNIX programmers need not learn a new serial I/O interface for DOS and vice versa. Third, the interface is through familiar file system calls (such as fopen and fprintf) so command line (and batch/shell) redirection is possible. In fact, the interface is so general that it allows any language-supporting file I/O (C, Assembler, Pascal, Clipper, and so on) to use the serial device. Fourth, the controlling interface is via one standard function call, namely IOCTL. It is the same under both UNIX and DOS. Fifth, standard DOS-critical error handlers can be used to trap run-time exceptions. Emulation, however, presents its own set of problems. Since device drivers are only loaded once, at boot time, and DOS supports no explicit resource management, an existing application (a TSR, for example) could corrupt the device driver by competing for the same communication port hardware. (Add-on communication function libraries can also suffer the same dilemma.) The device driver always consumes memory, even though it may not be used. The sample DOS serial device driver accompanying this article emulates the UNIX driver by consuming almost 8Kb, excluding buffers. (Code for the device driver may be downloaded from any MSJ bulletin board--Ed.) Furthermore, applications depending on the driver cannot load it before executing, although they can test for its absence and recover. Microsoft documents no formal mechanism (that is, a DOS function call) for dynamic device driver loading, except for its mouse driver. Finally, since DOS is not reentrant, and must be entered to gain access to the device driver, buffers cannot be resized at run time. They must be sized at boot time and remain fixed. Of the three methods you can use to write serial I/O software in the DOS enviroment, the best one is emulation of the UNIX serial I/O device driver. It is the most general solution for a given need; which means it best meets the goals of providing transportable code, a standard interface, and minimization of the learning curve. The issues you will need to address when developing serial I/O devices are discussed next. Blocking and Nonblocking I/O Normally when a function call is made requesting information from a block device (a disk), the function call will return only when the request is satisfied or when an error occurs. For example, the following call to read will wait until 10 characters are transferred or an error occurs: if(read(fd, buffer,10)!=10) { /* process the error */ } If the read is from the disk, the request is usually not satisfied until the disk access is complete. Under the DOS operating system, the application program waits for DOS to service the request completely. Under a preemptive multitasking system such as UNIX, the program (or process) is put to sleep (that is, the program blocks) until the request is satisfied; then it is awakened ( or unblocked) and is eligible to run. Note that while the process is asleep, the operating system can serve other processes. DOS, however, is a single-tasking system, so a program must patiently block until DOS returns control--nothing else can run. If you have a serial communications link with the same read calling for 10 characters, and no other characters are available, your process will block under both UNIX and DOS. If nothing shows up, the DOS program may block forever. The UNIX program also blocks, but other programs can run. Both DOS and UNIX, however, can be convinced not to block if nothing is available. Several issues related to blocking and nonblocking I/O must be addressed: The minimum amount of time and/or number of characters for which a read request waits to be assembled before being partly or fully satisfied; the number of m characters (if available) with n requested (m< n), that should be picked up; whether the request is free-form and just asks for a completed line (that is, whether all characters are up to a ' \n' or some other line delimiter); whether or not line editing is available so that mistakes can be corrected; and how the EOF condition will be detected and handled. These issues are explored in later examples; as it turns out, they are all options that can be configured through a general interface. Asynchronous Driver There is one general interface for controlling serial I/O parameters under UNIX--the IOCTL system call. Its interface specification is shown in Figure 1. If you have worked with DOS, you know that there is a similar, but not equivalent, DOS function call (44H) named IOCTL. Unfortunately, there is no documented evidence that lets you use IOCTL to control COM1 or COM2 attributes such as bps, parity, and hardware handshaking. (Note that the DOS IOCTL call allows several operations to be performed other than those discussed here, especially on block devices.) BIOS calls must be used. Microsoft borrowed the UNIX device driver philosophy for DOS, but did not generalize the DOS asynchronous serial I/O device interface as they did the disk drive interface. As a consequence, you must use one piece of serial hardware, the 8250 UART, mapped at a fixed address through the BIOS. To get around that, DOS device drivers that use the general UNIX serial I/O interface are available; they replace the COM1 and COM2 drivers provided with DOS. The UNIX terminal interface serves as an excellent model for DOS, as have many other UNIX features (such as the hierarchical file system, the I/O subsystem, redirection, pipes, and environment variables). Before using the generalized UNIX serial I/O interface, you must know the following: the bit rate, character data length, parity, and stop bit(s) requirements for communications; the scheme to be used to manage the flow of information in both the transmit and receive directions (XON/XOFF, RTS/CTS, DTR/DSR); how the device handles exceptions such as loss of modem carrier, a break signal, or ^C; the kind of input or output post-processing, if any, that needs to be done (for example, CR->NL, NL->NL/CR, convert cases, expand tabs); whether DOS programs should use binary or text mode when communicating with a device driver; and how parity, framing, and overrun errors should be detected and handled. Getting Started Just as you must open a disk file (with open or fopen) to modify it, you also must open a serial device, so that it can be written to, read from, or advised of new operation attributes. You can use either method shown in Figure 2; both level 1 and level 2 file I/O functions are provided in the standard C library. Note that in the call to fopen, "rwb" advises the I/O system not to insert CR/LF translations within the data and to pass the ^Z (1aH) character as binary data. The advantages of level 1 are that there is no buffering overhead in the standard library and that information specific to the operating system such as locking and networking, can be communicated. Level 1 provides more control for character-at-a-time reading and writing. A disadvantage of level 1 is that only read and write are available for I/O. The process can require a costly context switch to the operating system kernel for each read or write function call. Level 2 allows the use of fprintf, fscanf, fgets, fputs, and so on, for I/O. These are more portable between different operating system environments. Buffers can be flushed (reset) in the standard library. Context switching to the operating system for I/O is only necessary when output buffers are flushed or when input buffers are replenished, not for each call to fread or fwrite. A problem with level 2 is that there may be too much buffering overhead. The device, operating system, and standard library all maintain buffers. Furthermore, stream buffer overhead for character-at-a-time I/O is more beneficial for block devices (disks) than character devices (serial I/O ports). See Figure 3 for more information about I/O paths. Despite the disadvantages of using level 2 file I/O, it is the best choice for the application discussed here because of the convenience provided by the printf and scanf functions. Also, the standard library can be convinced that the stream buffer it maintains for a file stream is of length 1 with a call to setbuf. Buffering overhead is reduced to approximately that of level 1 I/O, and fflush need not be called after each fwrite or fprintf transaction. Once a device has been opened, it can immediately be read from or written to. For now, assume that either the device has been initialized or its default attributes will suffice. You can therefore communicate with the serial device, as in Figure 4. Changing Parameters Next, consider the mechanism for changing serial device attributes. As stated earlier, the IOCTL function is used to get or set device attributes passed in a C structure. To use the IOCTL function call, the device must be opened with the open function, and a valid file descriptor (a DOS file handle) must be obtained. If the standard library function fopen is used, the file descriptor can be derived from the file pointer with the fileno macro found in stdio.h. One C structure, termio, is used to set or get attributes for all asynchronous devices under UNIX; it is found in the include file termio.h. Members of the termio structure contain information on input modes, output modes, control modes, line disciplines, and an array of eight special control characters. The termio structure is shown in Figure 5. Each flag in the termio structure is actually a collection of several flags that produce a bit pattern fitting into an unsigned short. The flags have the effects listed below. c_iflag Instructs the device how to react to received input. Break, parity generation, parity checking, bit stripping, carriage return to new line mapping, uppercase to lowercase mapping, and XON/XOFF software handshaking can be set. c_oflag Instructs the device how to process output. Mapping of case, new line translation, fill characters, and delays for CR, HT, NL, and BS can be set. c_cflag Allows the setting of speed (bps), character size, stop bits, parity options, and control of data terminal ready (DTR), request to send (RTS), and data carrier detect (DCD) RS-232 control signals. c_lflag Enables exception handling, input-queue processing, echoing, line editing, buffer management, and mapping of special characters. c_line Selects a line discipline; usually set to 0. The line discipline selects a mode of mapping (filtering) characters from a raw input queue to a canonical input queue (this will be explained later). This filter mechanism allows line editing, conversion of lowercase to uppercase, expansion of tabs, and so on. Most UNIX systems support only one well-established line discipline, but other disciplines have been implemented (for example, for handling synchronous data links supporting SDLC types of communications). c_cc[NCC] An array of eight characters that can be assigned to represent the interrupt and quit signals (such as ^C, DEL, and BREAK), the erase character, EOF, and EOL. Some of the c_cc[] positions have dual meanings that depend on other flags, specifically the c_cc[4] (EOF, MIN) and c_cc[5] (EOL, TIME) positions. All of the flags of the termio structure are set by using the bitwise AND(&) and OR(|) operators with the constants provided in the termio.h include file. Figure 6 describes many of the possible flags. They are fully documented in the manual pages under termio(M) (Xenix) and termio(7) UNIX System V and by vendors of equivalent drivers for DOS. The fragment of code shown in Figure 7 demonstrates how to open and obtain the current attributes for the device /dev/tty01. Flags are assembled to change the speed to 9600 bps, to change the number of data bits to 7, and to enable reading. Once the flags are configured, a call to IOCTL is made to set the attributes. Set all values of the termio structure to valid values so the driver will not configure randomly. Also check return codes from library calls (discussed later). Data Acquisition Computer-to-terminal communications is a simple, general, and easily tested form of data acquisition. The program in Figure 8 will send a request message to an attached I/O device and expect a 10-byte response back within 2 seconds. If the expected response is received in time, data received back from the device is written to a log file: otherwise error recovery is started. Most, but not all, data acquisition devices request responses in a similar way. If an unsolicited response arrives, it will be buffered for the next read and is not lost. Selected attributes for the device, which are given below, are stored in the termio structure. c_iflag IGNBRK advises the driver to ignore the break sequence completely if the break signal is received. The driver could otherwise cause an interrupt signal to be sent to the application. IGNPAR states that any received characters containing detected parity errors are ignored. IXON enables software flow control on the output stream; IXOFF enables the same on the input stream. c_oflag No flags are selected and all features represented by this flag are disabled. c_cflag B4800 selects the bit rate of both the transmit and receive channels. CS8 advises that 8 data bits be sent and received. CREAD enables the read system call to transfer characters from the device driver's buffer. HUPCL drops the DTR signal on the last close of the device (the device can be opened more than once). CLOCAL advises the device not to examine the DCD signal as a qualifier when the driver opens the device. c_lflag No flags are selected, and all features represented by this flag are disabled. c_line The default line discipline is 0. c_cc[0] (VINTR) Not used since the ICANON | ISIG flags are not set. c_cc[1] (VQUIT) Not used since the ICANON | ISIG flags are not set. c_cc[2] (VERASE) Not used since the ICANON | ISIG flags are not set. c_cc[3] (VKILL) Not used since the ICANON | ISIG flags are not set. c_cc[4] (VEOF/MIN) Since the ICANON flag is not set, c_cc[4] contains the minimum number of characters that must be collected by the driver before a read call is satisfied. Otherwise it would represent the EOF character. c_cc[4] can be used in tandem with c_cc[5]. c_cc[5] (VEOL/TIME) Since the ICANON flag is not set, the c_cc[5] attribute contains the number of 0.10 second increments the driver should wait for before returning from a read. Otherwise it would represent the EOL character. c_cc[5] is a watchdog that will wait until the requested (or minimum) number of characters is available or the timer has expired, whichever comes first. This feature allows application programmers to specify the amount of time to wait for an attached serial device to send something (for example, after a query has been sent to it). c_cc[4] and c_cc[5] let you specify the minimum number of characters that must be collected and/or the maximum amount of time to wait for the characters; no timing loops are required in the application. c_cc[6] Reserved. c_cc[7] (SWTCH) Not used. Note that the stream buffers for the tty_stdin and tty_stdout have been set to length 1. This example can be adapted to work with almost any I/O device that is request/response driven. As characters arrive, they are buffered until the next read call is satisfied. tx_block and rx_block are simple character arrays; they could be structures containing more detailed information about what is sent and received. Also note that the path name of the device is /dev/tty01. This is a standard pathname to a UNIX device driver residing in the /dev directory. Curiously enough, /dev/tty01 is also a valid path under DOS to the device tty01. The Microsoft C run-time library translates the forward slash ( / ) to the back slash ( \ ) in file or device pathnames on calls to file system routines. However, \dev\tty01 must be used at the DOS command line (batch file) level. Input and Output Queues Once the serial device attributes have been selected and set and the device has been opened, I/O may begin. Three queues are maintained by the UNIX serial device driver--the output queue, which can be written to, and the raw and canonical queues, which can be read. (Do not confuse these queues with stdin, stdout, and stderr.) Characters that are written to the device are copied to the output queue for transmission. Depending on flag settings, delays and/or translations may be performed as characters are transmitted. Figure 9 shows the relationship of the raw and canonical input queues. An application can choose to read characters from either. The raw input queue is simple: anything received (excluding parity errors and break sequences) is buffered and passed directly to the application. When reading from the raw queue, it is possible to configure the driver to implement a watchdog timer or specify a minimum number of characters that must be available before a read completes successfully. The size of the raw queue is limited by a shared pool of buffers; these buffers, called clists under UNIX, are dynamically allocated and released. There is no pool of clists under DOS; buffers are usually fixed, because the device driver is accessed through the DOS file system. Remember that DOS is not reentrant, so it is nearly impossible to perform dynamic memory allocation of clists inside any DOS device driver. The canonical input queue obtains its characters from the raw queue via a line discipline. The line discipline is analogous to a filter; as characters are obtained from the raw queue, they may be translated to other sequences. For example, CR can translate to NL (ICRNL flag). The canonical input queue is enabled with the ICANON flag, which also advises the device driver to look for the special characters in the c_cc[] array and take appropriate action in accordance with the IBRK and ISIG flags. Interactive Terminals Interactive programming requires a special style of coding. People are much less predictable than machines, so special care must be taken when prompting and handling responses (valid or invalid). This example assumes that there is a dumb terminal or PC connected to the serial port, either directly or through a modem. The program in Figure 10 is trivial if input is from and output is to the DOS console device. During an interactive session at a remote terminal on a serial link, however, special precautions must be taken. When the terminal attached to the serial link is prompted for an integer, there are three possible responses: correct, incorrect, or no response. The enclosed switch statement handles these three cases. This example sets up a terminal with many of the features that a shell might use for command line interpretation, notably line editing, character echoing, and abort sequences. Selected attributes for the device, given below, are stored in the termio structure. Note that the buffers for the streams tty_stdin and tty_stdout have been set to length 1. c_ifla IGNBRK advises the driver to ignore the break sequence completely if the break sequence is received; otherwise, the driver could cause an interrupt signal to be sent to the application. IGNPAR states that any received characters containing detected parity errors are ignored. IXON enables software flow control on the output stream. IXOFF enables software flow control on the input stream. CLOCAL disables modem control; DCD is ignored when opening the device. ICRNL specifies that the CR character is converted to the LF (' \n ') character on input. c_oflag OPOST enables the post-processing of output. ONLCR converts the ' \n ' (LF) character to the CR/LF characters on output. OCRNL converts the CR character to the LF (' \n ') character on output. TAB3 expands the horizontal tab character to spaces up to the next tab stop. Tab stops usually occur every eight characters. c_cflag B4800 selects the bit rate of both the transmit and receive channels. CS8 advises that eight data bits are sent and received. CREAD enables the read system call to transfer characters from the device driver's buffer. HUPCL drops the DTR signal when the device is closed for the last time. CLOCAL advises the device not to examine the DCD signal as a qualifier in opening the device. c_lflag ICANON enables the processing of character filtering from the raw input queue to the canonical input queue. Line editing and character echoing is now performed by the device driver. The watchdog for a timer and/or a minimum number of characters seen in the last example is no longer active. c_cc[4] and c_cc[5] now contain characters representing EOF and EOL, respectively. ISIG tells the driver to check each input character against the special characters designated in the c_cc[] array for INTR, QUIT, and SWTCH. If any of these characters are found in input, the appropriate action is taken (that is, signals are sent). ECHO echoes all characters received to the output queue. ECHOE echoes the designed erase character as BS-SP-BS, thus erasing the character in error. c_line Selects a line discipline; usually set to 0. c_cc[0] (VINTR) If matched on input, the interrupt signal is sent to the controlling process. Under DOS, the current process is killed. Note that the ICANON | ISIG flags are set in c_cflag. c_cc[1] (VQUIT) If matched on input, the quit signal is sent to the controlling process. c_cc[2] (VERASE) If matched on input, the last valid character entered is erased. c_cc[3] (VKILL) If matched on input, whatever has been assembled in the current line is erased. c_cc[4] (VEOF/MIN) This is the designated EOF character, typically ^Z (DOS) or ^D (UNIX), although any character will work. c_cc[5] (VEOL/TIME) represents the EOL character; typically ' \n '; any character will work, however. This character is the anchor used by the line discipline routines to mark the end of a line. Typically lines are assembled and edited by the user. When the EOL character (stored in c_cc[5]) is received, the line is available for reading by the application. c_cc[6] Reserved. c_cc[7] (SWTCH) Not used. Serial Link The examples of data acquisition and request/response communications discussed in Figures 8 and 10 demonstrated important and useful features when using the UNIX (or compatible) serial I/O device driver. All I/O was performed with familiar calls to the C standard library. Many useful features of the UNIX serial I/O device driver can save the C programmer many headaches--features such as watchdog timeouts, buffering in both the TX and RX directions, CR and LF translation on both input and output streams, line editing, tab expansion, and XON/XOFF software handshaking. One common interface, IOCTL, is used to adjust device parameters using a standard system call. Serial I/O software is portable between UNIX and DOS systems. There are other modes of serial link communications and other programming contexts, but they are usually variations of one of the examples presented. Advanced techniques of serial communication programming might address any of the following topics. Advanced Modem Control If the CLOCAL flag is cleared, the device will wait for the DCD line to become active for an open call to complete. This allows the application to block for a modem connection before the device driver opens. UNIX programmers can set the O_NDELAY flag when performing an open operation. The open function will then use errno to return immediately, indicating success (DCD present) or failure (DCD absent) in opening the device. Reacting to Interrupt Signals Under UNIX, the reception of DEL or ^\ (changeable defaults in the c_cc[ ] array) sends the signals interrupt or quit, respectively. Reception of a BREAK sequence will also send the interrupt signal. If the flags ICANON | ISIG are set, a signal is sent to the controlling process. Under DOS, some device drivers provide an option to kill the currently executing process by issuing DOS interrupt 4BH. Sending the BREAK Character Calls can be made to IOCTL to send the BREAK character. See the IOCTL command TCSBRK. Forcing the Driver XON/XOFF State Calls can be made to IOCTL to suspend output and restart suspended output. See the IOCTL command TCXON. Handling Parity Errors If parity checking is enabled by setting the PARENB and PARMRK flags and clearing the IGNPAR flag, a character (X) received with a parity error is passed as a special sequence (' \x7F ', ' \0 ', X) to identify the character in error. The character ' \x7F ' is then read as ' \x7F ',' \x7F '. Recovering from DOS Critical Errors DOS will translate critical errors to interrupt vector 24H. If the application traps this interrupt, all critical errors encountered by a device driver should trap the user's exception handler with sufficient information (device name and type of critical error) to recover. Command Line Interface As mentioned earlier, an advantage to performing serial I/O with a device driver is having access through the file system. Command line redirection (and batch/shell files) can communicate with the device. For example, under DOS the command C>dir > \dev\tty01 sends a directory listing to a serial device. Note that no mention has been made of how to change device attributes from the command line. This is accomplished with the UNIX command stty. The stty command is also capable of obtaining the current attributes of a device and displaying them. The same command is available with DOS serial device drivers; however, DOS requires the device path to be \dev\tty01. Refer to Figure 11 for an example of stty under both DOS and UNIX. In the examples in Figure 11, the stty program reopens its standard input, the serial device /dev/tty01, and sets its attributes to 9600 bps, even parity enabled, 7 data bits per character, XON/XOFF software handshaking enabled, and modem control enabled. This configuration allows communication with a popular laser printer over the serial link. In the two commands shown in Figure 12, file_1 is sent through a PostScript preparation filter and finally to a printer attached to /dev/tty01. System Interface There are some intrinsic features of the operating system that concern both UNIX and DOS programmers that you should be aware of when opening a serial device for I/O. The first concern is how the blocking (or process suspension) is handled. Under DOS, there is no facility to suspend a process on the assumption that it will resume running when some event has occurred (or has not occurred). Under UNIX, a terminal driver can cause the suspension of a process if there are no characters to read or a line has not yet been completely assembled via a selected canonical line discipline. Once characters are ready or a line has been assembled, a suspended (or blocked) process is awakened to run. Since DOS has no facility that suspends a process to wait for I/O, any request to a device driver should return immediately (after checking for set watchdog timers), whether characters (or lines) are ready or not. If nothing is available, EOF can be returned. The second concern is that there are differences in the treatment of EOF indications. DOS supports two access modes for reading and writing character devices--ASCII text mode and binary mode. If the character 1aH (^Z) is read, the input from a file or device opened in text mode is terminated (shut down) and an EOF indication is returned. Even if there are characters pending, any and all read requests from this point forward are ignored and the EOF indication is always returned. If the file were opened in binary mode, the EOF character (^Z) would be passed to the application, and further requests for characters would be honored. Text mode also insists on translating LF to CR/LF on output and CR/LF to LF on input, so I/O in text mode could add characters or delete them. Binary mode reads and writes exactly what is requested. These features are intrinsic to DOS. DOS device drivers should return EOF if there is nothing to read (after checking for set watchdog timers). A device driver could block the process attempting the read; the application might then hang forever waiting for a character. If EOF is returned, the application can do something else and attempt another read later. Anything that arrives between reads will be buffered. When the EOF character, designated by c_cc[4], is received by a UNIX character device driver, all characters waiting to be read are immediately passed to the application without waiting for a new line, and the EOF is discarded. Thus if no characters are waiting in the input queue--that is, if EOF occurred on the beginning of a line--no characters will be passed back to the application; this is a standard EOF indication under UNIX. The third concern is the difference in the treatment of signals. The signal mechanism available under the UNIX operating system is not available under the DOS operating system, although most C run-time libraries, most notably Microsoft C 5.10, have mechanisms that perform similar functions. The signal( ) function call allows the UNIX and DOS programmer to catch special messages sent to processes when exceptions such as floating point overflow, ^C, and modem disconnect occur. Async device drivers under UNIX are also able to send signals to related processes. These signals are typically INTR, QUIT, HANGUP, and BREAK. DOS does not provide the signal mechanism that UNIX does, although DOS drivers can be designed to have similar functions. Conclusion This article explored how to write portable serial I/O software, useful features of a general serial I/O device interface operating under both UNIX and DOS. UNIX and its many hybrids all share the same serial I/O interface. With AT&T, Digital Equipment, Hewlett-Packard, IBM, Microsoft, and other companies, embracing UNIX as a standard product offering, it simply cannot be ignored by the professional programmer. Figure 1 #include <termio.h> int ioctl( fd, command, argument); int fd; int command; union { int i_arg; struct termio *s_arg; } argument; ioctl() Returns 0 if successful in performing the requested command; returns -1 otherwise and errno is set to reflect the reason for the error. EBADF, ENOTTY, EINTR, EFAULT, EINVAL, EIO, ENXIO and ENOLINK are possible values for errno; see the include file <errno.h>. fd A valid file descriptor (DOS file handle) obtained from a successful call to open() or fopen(). It must be possible to write to the file to set device attributes and it must be possible to read from the file to get device attributes. command There are seven commands that tell the device driver how to respond. Note that the value and type of the last parameter, argument, will depend on the command selected. TCGETS Get current attributes from the device (TCGETA on some UNIX systems). argument.s_arg points to the structure that will receive the existing attributes. TCSETS Immediately set the passed attributes in the device (TCSETA on some UNIX systems). argument.s_arg points to the structure that contains the attributes to be set. TCSETAW Set passed attributes after the output buffer has drained. argument.s_arg points to the structure that contains the attributes. TCSETAF Wait for the output to drain, flush the input queue, then set the new attributes. argument.s_arg points to the structure that contains the attributes. TCSBRK Send a break sequence for 250 milliseconds. argument.i_arg must be 0. TCXONC Start (XON) or stop (XOFF) the transmission of output from the device. If argument.i_arg is 0, output is suspended; if 1, suspended output is restarted. TCFLSH Flushes the input and/or output queues. If argument.i_arg is 0, flush the input queue; if 1, flush the output queue; if 2, flush both the input and output queues. argument The union argument contains one of two data types: struct termio * s_arg or int i_arg The type depends on the command being issued. s_arg is a pointer to a C structure that will be used for setting or getting device attributes. i_arg is an integer. Serial Communications Terms Asynchronous Communications Character-at-a-time transmission. Characters are randomly sent one at a time, far apart or close together. They are separated by start and stop bits. (See Framing.) Baud Symbols per second. Each symbol usually contains analog information for 2^s:n: bits (n = 0, 1, 2, 3, ... ). For example, a symbol that has four different voltage levels (n = 2) contains information for 2 bits; thus a baud rate of 600 will yield an effective bit rate of 1200 bps. Blocked Process If a program (process) is waiting for an I/O completion, it may be put to sleep; that is, marked as no longer eligible to run. The process is unblocked (awakened) when the I/O request is complete; it is then eligible to run. BPS Number of binary transitions per second. (See Baud.) Break BREAK asserts the SPACE condition on the serial line for approximately 0.25 to 0.30 seconds. It may cause a framing or overrun error to occur. It is typically used to reset the serial link to some known state. DCD Data Carrier Detect. An electrical signal stating the presence or absence of a modem carrier. The DCD is used to indicate when two or more modems have established a connection. DTE/DCTE Data terminal equipment. Usually a terminal device capable of generating or displaying information. Data circuit terminating equipment. Usually a device capable of transmitting or receiving data over a chosen medium. Examples are a modem and a line driver. DTR/DSR Data Terminal Ready/Data Set Ready. A DTE advises that it is ready to converse by raising DTR. The DCTE advises that it is ready to converse by raising DSR. DTR and/or DSR are usually active for the duration of a conversation. Flow Control A method or protocol for governing the starting and stopping of transmission. It is used so that resources can be managed to accommodate incoming data; typically it advises a sending entity that a receiving buffer is nearly full (stop transmitting) or nearly empty (start transmitting). Framing For asynchronous character transmission, framing identifies the meaning of bits that constitute a character. The number of start bits (1; SR), information bits (5, 6, 7, or 8; D0..D7), optional parity bit (0, 1; P), and stop bit(s) (1.0, 1.5, 2.0; SP) are identified. Example: | SR | D0 | D1 | D2 | D3 | D4 | D5 | D6 | D7 | P | SP | Hardware Handshaking Flow control that is managed at the hardware level. The UART waits for permission to send characters, which is granted through the RS-232 RTS/CTS signals. Level 1 Direct I/O The C programmer has the choice of two levels of I/O: level 1 or level 2. Level 1 I/O sends and receives characters directly from the operating system (the operating system may buffer characters). Level 2 Stream I/O The C programmer has the choice of two levels of I/O, level 1 or level 2. Level 2 I/O sends and receives characters to and from stream buffers (typically 512 bytes long) maintained by the C standard library. Level 2 is written as a function of level 1. When write buffers become full, they are flushed out to the operating system; when read buffers become empty, more characters are requested from the operating system. Line Discipline Conversion rules chosen for the c_lflag in the termio structure. A mapping or filter strategy for transferring characters from the raw input queue to the canonical input queue. Mark An electrical signal that denotes a logical binary 1. Under RS-232, typically -3 to -25 VDC. Overrun When too many data bits are received between the start and stop bit, an overrun condition has occurred. Parity A bit designated for error-detection purposes. There are two basic types of parity--even and odd. If the parity bit is designated as odd (or even), its job is to take a value that makes the total number of 1s in an asynchronous character transmission odd ( or even); for example, 00010101P; P = 0 odd parity (P = 1 even parity). Parity errors detect some, but not all, types of transmission errors. Ring Buffering A circular buffer or queue of finite size that has no physical beginning or end. The logical beginning and logical end are typically tracked with pointers. The next character put in the queue is at the logical end of the buffer; the next character taken out of the queue is at the logical beginning. If the buffer is full and a character is put in the queue, the oldest character in the queue may be overwritten. A ring buffer of size N will always contain at most the last N - 1 characters in the queue. RS-232 Recommended Standard 232. RS-232 is administered by the Electronic Industries Association (EIA). Electrical characteristics for transmission of information and signaling information is specified. RS-232 is also known as CCITT V.24. Physical characteristics of a connector are also specified; for example, the common DB-25. RTS/CTS Request to Send/Clear To Send. Hardware signals used in flow control. A DTE can request permission to send characters by raising RTS and be granted permission with the activation of CTS by the DCTE. SDLC Synchronous Data Link Communications. (See Synchronous Communications.) Software Handshaking Flow control that is managed at the software level. Software typically sends an XOFF (ASCII DC3; 13H) character to request the suspension of character transmission (receive buffers are nearly full) and an XON (ASCII DC1; 11H) character to resume a suspended transmission (receive buffers are nearly empty). Space An electrical signal that denotes a logical binary 0. Under RS-232, typically +3 to +25 VDC. Synchronous Communications Block-at-a-time transmission. Groups of characters are sent in blocks of bits. There are no start or stop bits to separate characters; there are, however, bit patterns (flags) that separate blocks of bits. UART Universal Asynchronous Receiver Transmitter. A hardware device whose primary function is to perform serial-to-parallel conversion of electrical signals. XON/XOFF Characters that have been chosen for software flow control. An entity receiving an XON is advised to resume (begin) transmission. An entity receiving an XOFF is advised to suspend (stop) transmission. (See Software Handshaking.) Figure 2 #include <stdio.h> #define SERIAL_DEVICE "/dev/tty01" static int fd_tty; static FILE *fp_tty; /***************************************************************/ /* L E V E L 1 O P E N M E T H O D */ if( ( fd_tty = open( SERIAL_DEVICE, 2 ) ) != -1 ) { /* fd_tty now a valid UNIX file descriptor */ /* fd_tty now a valid DOS file handle */ } else { /* The device can't be opened. */ } /***************************************************************/ /* L E V E L 2 F O P E N M E T H O D */ if( ( fp_tty = fopen( SERIAL_DEVICE, "rwb" ) ) != NULL ) { /* fp_tty now a valid stream pointer */ } else { /* The device can't be opened. */ } Figure 3 I/O Paths: The path a character will take when an application writes it to an output stream (or reads from an input stream). The application opens the device with a call to fopen(). The application then sets up a buffer, count, and so on, and calls fwrite(). A call to write() bypasses (2), the C standard library stream buffers. fwrite() transfers bytes from the user buffer to a stream buffer maintained by the standard library. Stream buffers are usually 512 bytes in length (see the definition for BUFSIZ in the include file <stdio.h>). Writing or reading bytes to or from a stream buffer saves the overhead of making a context switch to the operating system. When the stream buffer becomes full, its contents are flushed out to the operating system with a call to write. write() transfers bytes from the stream buffer to internal operating system buffers (known as clists under UNIX) if the destination is a character device. Under DOS, fixed buffers are most likely maintained by the device driver itself. If the destination is a block device, write() transfers bytes from the stream buffer to an operating system buffer (known as the buffer cache under UNIX). When the device is capable of transmitting, a byte is transferred from the appropriate clist to a transmit register. Finally, the byte held in the transmit register is shifted out to the serial line designated for transmission. The following description also applies to the fread() call, with the exception that the sequence is reversed. Binary information is shifted in from the serial line designated for reception and is held in a receive register. When the device has received a byte, it is transferred from the receive register to the appropriate clist. read() transfers bytes from the appropriate clist to a stream buffer maintained by the C standard library. Note that there are two types of input queues (clists)--the raw and canonical input queues. The raw input queue contains exactly what was received. The canonical input queue contains characters that have been transferred from the raw input queue and possibly filtered per a selected line discipline. fread() transfers bytes from a stream buffer maintained by the standard library to the buffer provided by the user. When the stream buffer becomes empty, its contents are replenished by a call to the operating system function read(). fread() returns to the application program. Figure 4 auto int in_int; auto FILE *fp_tty; /* Trivial request and response from the terminal */ /* Assumes fp_tty is opened in "rwb" mode */ do { fprintf( fp_tty, "\nHello Terminal. \nEnter an integer: " ); } while( fscanf( fp_tty, "%d", &in_int ) != 1 ); Figure 5 #define NCC 8 struct termio { unsigned short c_iflag; /* Input modes. */ unsigned short c_oflag; /* Output modes. */ unsigned short c_cflag; /* Canonical modes. */ unsigned short c_lflag; /* Local modes. */ char c_line; /* Line discipline. */ unsigned char c_cc[NCC]; /* Special chars. */ }; Figure 6 The c_iflag field describes the basic terminal input control. IGNBRK Ignore break condition BRKINT Signal interrupt on break IGNPAR Ignore characters with parity errors PARMRK Mark parity errors INPCK Enable input parity check ISTRIP Strip character INLCR Map NL to CR on input IGNCR Ignore CR ICRNL Map CR to NL on input IUCLC Map uppercase to lowercase on input IXON Enable START/STOP output control IXANY Enable any character to restart output IXOFF Enable START/STOP input control The c_oflag field specifies the system's treatment of output. OPOST Post-process output OLCUC Map lowercase to uppercase on output ONLCR Map NL to CR-NL on output OCRNL Map CR to NL on output ONOCR No CR output at column 0 ONLRET NL performs CR function OFILL Use fill characters for delay OFDEL Fill character is DEL, else NUL NLDLY Select New-Line delays: NL0 New-Line character type 0 (no delay) NL1 New-Line character type 1 (0.10 second delay) CRDLY Select Carriage-Return delays: CR0 Carriage-Return delay type 0 (no delay) CR1 Carriage-Return delay type 1 (column position dependent) CR2 Carriage-Return delay type 2 (0.10 second delay) CR3 Carriage-Return delay type 3 (0.15 second delay) TABDLY Select Horizontal-Tab delays: TAB0 Horizontal-Tab delay type 0 (no delay) TAB1 Horizontal-Tab delay type 1 (column position dependent) TAB2 Horizontal-Tab delay type 2 (0.10 second delay) TAB3 Horizontal-Tab delay type 3 (expand tabs to spaces) BSDLY Select backspace delays: BS0 Backspace delay type 0 (no delay) BS1 Backspace delay type 1 (0.05 second delay) VTDLY Select Vertical-Tab delays: VT0 Vertical-Tab delay type 0 (no delay) VT1 Vertical-Tab delay type 1 (2.0 second delay) FFDLY Select Form-Feed delays: FF0 Form-Feed delay type 0 (no delay) FF1 Form-Feed delay type 1 (2.0 second delay) The c_cflag field describes the hardware control of the terminal. CBAUD Baud (Bit) rate: B0 Hang up B50 50 bps (baud) B75 75 bps (baud) B110 110 bps (baud) B134 134.5 bps (baud) B150 150 bps (baud) B200 200 bps (baud) B300 300 bps (baud) B600 600 bps (baud) B1200 1200 bps (baud) B1800 1800 bps (baud) B2400 2400 bps (baud) B4800 4800 bps (baud) B9600 9600 bps (baud) B19200 19200 bps (baud) B38400 38400 bps (baud) CSIZE Select character size for both transmission and reception: CS5 5 bits CS6 6 bits CS7 7 bits CS8 8 bits CSTOPB Send two stop bits, else one CREAD Enable receiver PARENB Parity enable PARODD Odd parity, else even HUPCL Hang up on last close CLOCAL Local line, else dial-up (modem) LOBLK Block layered output The c_lflag field of the argument structure is used by the line discipline to control terminal functions. The basic line discipline (0) provides the following: ISIG Enable signals ICANON Canonical input (erase and kill processing) XCASE Select canonical uppercase/lowercase presentations ECHO Enable echo ECHOE Echo erase character as BS-SP-BS ECHOK Echo NL after kill character ECHONL Echo NL NOFLSH Disable flush after interrupt or quit The c_cc[NCC] array contains characters that are mapped to special actions. As characters are received by the device, they can be checked against the characters stored in the c_cc[] array. This checking is enabled by setting combinations of the ICANON, ISIG, and IGNBRK flags. If a match is found, a special action can be performed by the device driver. Characters representing backspace, EOF, EOL, MIN, TIME, and assorted signals are stored here. c_cc[0] (VINTR) If matched, an interrupt signal is sent to the controlling process. c_cc[1] (VQUIT) If matched, a quit signal is sent to the controlling process. c_cc[2] (VERASE) If matched, the last enqueued character received is deleted. c_cc[3] (VKILL) If matched, the current line buffer is flushed and the line is killed. c_cc[4] (VEOF/MIN) If matched, the device driver assumes that the end of input (EOF) has been reached. Any characters pending in the input line buffer are immediately passed to the application. This also serves as the minimum number of characters to collect in raw input mode when ICANON flag is not set. c_cc[5] (VEOL/TIME) If matched, the device driver assumes that an entire line has been assembled; the line is now available for reading by the application. This also serves as a timeout parameter if the ICANON flag is not set. c_cc[6] (Reserved) Not used. c_cc[7] (SWTCH) If matched, the current foreground process is moved to the background. This is the job control switch character (UNIX only). Figure 7 #include <stdio.h> #include <termio.h> static FILE *tty_fp; static struct termio tty_params; /* Open the device */ tty_fp = fopen("/dev/tty01", "rwb"); /* Obtain its current attributes; stored at &tty_params */ ioctl(fileno(tty_fp), TCGETS, &tty_params); /* Change desired attributes; stored at &tty_params */ tty_params.c_cflag = B9600 | CS7 | CREAD; ■ ■ ■ /* Set new attributes */ ioctl(fileno(tty_fp), TCSETS, &tty_params); Figure 8 #include <stdio.h> #include <termio.h> /* Installed with .h files */ #define TTY_STDIN_FILE_NAME "/dev/tty01" #define TTY_STDOUT_FILE_NAME "/dev/tty01" #define LOG_FILE_NAME "logfile" static char tx_block[10] = "SEND.DATA"; static char rx_block[10]; extern int errno; main() { static struct termio termio_var; auto FILE *tty_stdin, *tty_stdout, *log_file; auto int error_flag, i; if((tty_stdin = fopen(TTY_STDIN_FILE_NAME, "rb")) == (FILE *)NULL) exit(1); if((tty_stdout = fopen(TTY_STDOUT_FILE_NAME, "wb")) == (FILE *)NULL) exit(2); if((log_file = fopen(LOG_FILE_NAME, "wb")) == (FILE *)NULL) exit(3); /* Length of tty_stdin & tty_stdout stream buffers to 1 */ setbuf(tty_stdin, (char *)NULL); setbuf(tty_stdout, (char *) NULL); if(ioctl(fileno(tty_stdin), TCGETS, &termio_var)) exit(4); /* Set up the terminal attributes */ termio_var.c_iflag = IGNBRK | IGNPAR | IXON | IXOFF; termio_var.c_oflag = 0; termio_var.c_cflag = B4800 | CS8 | CREAD | HUPCL | CLOCAL; termio_var.c_lflag = 0; termio_var.c_line = 0; termio_var.c_cc[VEOF] = sizeof( rx_block ); /* MIN */ termio_var.c_cc[VEOL] = 20; /* TIME */ if(ioctl(fileno(tty_stdout), TCSETS, &termio_var)) exit(5); else { for(i = 0; i < 100; i++) { if(fwrite(tx_block, sizeof(tx_block), 1, tty_stdout) != 1) fprintf(stderr, "Can't write to I/O device on tty_stdout\n"); if(fread(rx_block, sizeof(rx_block), 1, tty_stdin) == 1) { fprintf(stdout, "Valid response from device received; Logging..."); if(fwrite(rx_block,sizeof(rx_block), 1, log_file) == 1) fprintf(stdout, "Passed.\n"); else fprintf(stdout, "Failed.\n"); } else fprintf(stderr, "No response from device; errno: %d\n", errno ); } fclose(tty_stdin); fclose(tty_stdout); fclose(log_file); } return(0); } Figure 9 This figure shows the input and output queues maintained by a typical UNIX character device driver. As characters are written by the process, they are placed on the output queue and are eventually sent by the serial link hardware. As characters are received by the device driver, they are placed on the raw input queue and may be read directly, without modification, by the process. The process can also elect to read from the canonical input queue (the ICANON flag must be set). As characters are received and placed in the raw queue, they are moved to the canonical input queue via the selected line discipline. The line discipline acts as a filter; removing, replacing, or adding characters on the input stream. A common replacement is translating CR with CR-NL. If the canonical input queue is enabled, line editing is possible. Figure 10 #include <stdio.h> #include <termio.h> /* Installed with .h files */ #define TTY_STDIN_FILE_NAME "/dev/tty01" #define TTY_STDOUT_FILE_NAME "/dev/tty01" extern int errno; main() { static struct termio termio_var; auto FILE *tty_stdin, *tty_stdout; auto int error_flag, i_var, rc; if((tty_stdin = fopen(TTY_STDIN_FILE_NAME, "rb")) == (FILE *)NULL) exit(1); if((tty_stdout = fopen(TTY_STDOUT_FILE_NAME, "wb")) == (FILE *) NULL) exit(2); setbuf(tty_stdin, (char *) NULL); setbuf(tty_stdout, (char *) NULL); if(ioctl(fileno(tty_stdin), TCGETS, &termio_var)) exit(3); termio_var.c_iflag = IGNBRK | IGNPAR | IXON | IXOFF | CLOCAL | ICRNL; termio_var.c_oflag = OPOST | ONLCR | OCRNL | TAB3; termio_var.c_cflag = B4800 | CS8 | CREAD | HUPCL | CLOCAL; termio_var.c_lflag = ICANON | ISIG | ECHO | ECHOE; termio_var.c_cc[VINTR] = '\177'; /* DEL char */ termio_var.c_cc[VQUIT] = '\003'; /* ^^C char */ termio_var.c_cc[VERASE] = '\b'; /* backspace */ termio_var.c_cc[VKILL] = '@'; /* @ kills line */ termio_var.c_cc[VEOF] = '\004'; /* Designated EOF */ termio_var.c_cc[VEOL] = '\n'; /* Designated EOL */ if(ioctl(fileno(tty_stdout), TCSETS, &termio_var)) exit(5); else { fprintf(tty_stdout, "\nEnter an Integer > "); while(1) { switch(rc = fscanf( tty_stdin, "%d", &i_var)) { case EOF: break; case 0: fprintf(stderr, "\nInvalid response\n\n"); fflush(tty_stdin); fprintf(tty_stdout, "\nInvalid response\n\n"); fprintf(tty_stdout, "\nEnter an Integer > "); break; case 1: fprintf(stdout, "From tty: %d \n\n", i_var); fprintf(tty_stdout, "\nEnter an Integer > "); break; default: fprintf(stderr, "fscanf() : %d ?\n", rc); break; } } fclose(tty_stdin); fclose(tty_stdout); } return(0); } Figure 11 Under UNIX $stty 9600 parenb -parodd cs7 ixon ixoff -clocal hupcl < /dev/tty01 Under DOS C>stty 9600 parenb -parodd cs7 ixon ixoff -clocal hupcl < \dev\tty01 Figure 12 Under UNIX $cat file_1 | PS_filter > /dev/tty01 Under DOS C>type file_1 | PS_filter > \dev\tty01 Simplifying Pointer Syntax for Clearer, More Accurate Programming Greg Comeau Pointers containing the address of functions, variables, or objects clarify and simplify your code. They are tricky, though, and confusing if carelessly written. The derivations of pointer syntax that are presented in this article should make pointer usage perfectly clear to you. While these derivations are simple, some of them may be new to you or may produce results you would not have predicted. For example, the output of the program in Figure 1 may not be obvious, although it is a common and basic C construct. An analysis of Figures 2a and 2b will give us a common frame of reference for this article. In these figures, the value of c is 'a', and the value of p when p = &c executes is 1000. The value 1000 is the address of c (randomly chosen for this discussion), and the & signifies that we want to take the address of the accompanying object operand c. Therefore p = &c implies that the data contained in p is actually the address of another variable, which also contains its own data. This seems logical, since c and p each have their own storage space and can hold independent data within that storage space. It is easy to forget this when your declarations and programs become more complex. Pointer Uses Basically, you can do three things with pointers: assign another value to the pointer, assign the pointer's value to another pointer explicitly by means of an assignment or implicitly as an argument to a function, or dereference the pointer. Assigning another value to the pointer is as simple as coding the assignment (see Figure 3a). The memory map of the program as it executes will help you understand exactly how this works (see Figure 3b). Notice how the reassignment to p simply causes p to change values; this is the normal functioning of a variable and applies even though we are dealing with a pointer. The reassignment of a pointer, however, can have devastating effects on some programs. This is especially true with heap space when free subroutine calls are not issued between the assignments. Using the pointer in an expression such as an rvalue shows once again that a pointer is just another variable. In Figures 4a and 4b, the statement p = &c assigns the address of c to p. Then the statement p2 = p assigns to p2 the address of c by using the value contained in p. This does not imply that p2 points at p. Instead the value of p gets copied into p2. This is no different from int i; int j; i = 5; j = i; where we know that j will also contain its own copy of the value 5. To prove this, run the code from Figure 4a to see what the output will be. Note that it will not print out numbers, like 1000, used in the memory maps. Instead the %p format specifier of printf will encode the output in segment:offset format. The last use of pointers involves the value that the pointer contains through the unary * operator. The unary * operator is one way to accomplish indirection and dereferencing of pointers. For the moment, let's take a look at the last line of Figure 5a. As you can see from the memory map, the result of this statement is to change the value of c from 'a' to 'A.' Since p2 becomes another name for c, we can code *p2 = 'A'; instead of c = 'A'; The indirection ability of pointers is very powerful. It allows us to access named symbolic variables and unnamed variables (identifiers) with the same ease--without requiring that you keep a lot of information about which objects you are pointing to, where they are located, and so on. This indirection can result in faster and smaller code (depending upon the compiler and hardware in question). Unary * Operator Near the end of Figure 5a, c is a char object whose value is 'a' and p2 is a pointer to a char object whose value is the address of c. *p2 represents what p2 points to, not the contents of p2 as it may appear to read. For example, using i and j from the previous example, we know that j = i says j is equal to the contents of i. There is no reason to expect pointer notation to function differently. For example, Figure 5a contains the statement p2 = p; which requests that the contents of p be placed into p2. To explain further why *p2 does not mean "the contents of p2," it is worth noting that when we say j = i, it actually means j = *&i. Read *&i as "a pointer to the contents of the address of i." A memory map of Figure 5a is shown in Figure 5b. Besides mapping the contents of what p2 points to, *p2 can represent the contents of the address of p2. Since in this case the content of p2 is 1000, this expression implicitly becomes * char *1000--meaning the contents of memory location 1000, taken to be a char, which is 'A'. Derivations Now that we've gotten some of the background out of the way, we can analyze some concrete pointer examples. PTRINC.C (see Figure 6) shows a small group of simple pointer operations. These pointer operations illustrate that there is more going on behind the scenes than is apparent at first glance. The following discussion is based on a debugging technique described in the sidebar "In-Line C Program Debugging." For further information, refer to "Pointers 101: Understanding and Using Pointers in the C Language," MSJ (Vol. 4, No. 4). The debug.h file explained in the sidebar is included in the first line of PTRINC. Examining lines 9 and 10 you will find the declarations char *p and char *origp, which are two pointers to char objects. On line 12, p is assigned to the address of a string literal--a group of characters next to each other in a source program, prefixed and suffixed by the double-quote character. Internally, this kind of construct is a static array of characters containing each character that is enclosed in the quotes, including a terminating null byte which is added by the compiler. This format is known as ASCIIZ. A string literal is a good example of an unnamed area of storage. To use a string literal, we must either set a pointer to its address as PTRINC has done or use it as an argument to a function where its address becomes apparent. Most of us have used this context in our first C program by specifying a string literal as an argument to printf. For a complete explanation of string literals, especially those involving multibyte characters, refer to the ANSI C draft (the latest version, published December 7, 1988, is available from the Computer and Business Equipment Manufacturers Association in Washington, D.C.) or a relatively new C text such as The C Programming Language, Second Edition, Kernighan and Ritchie (Prentice-Hall Inc., 1988). Line 13 of PTRINC prints the characters 'a' through k in our debug format. Line 14 assigns origp to array--the same place that p is pointing to. For a visual representation of the statements in lines 12 to 32, see Figure 7. At line 17, the value printed for the contents of p is 1234, which is the same as &array[0]. Note that line 17 does not print the address of p. If we wanted that, we would have coded printp(&p) to produce an output of &p = 1200. The address of p is a given, in accordance with the top of Figure 7. An arbitrary location for it has been chosen since it will not affect this discussion (actual execution of the code will give another result). Therefore, like any occurrence of text which only mentions p, printp(p) produces the contents of p; origp = p functions similarly. The value printed by the execution of line 17 should point at the beginning of the array since we have not incremented or reassigned a value to p. Line 18 increments p. As with all pointers, the increment value is contingent upon the derived or base type of the pointer. In PTRINC, p is a pointer to char, therefore the increment will be 1, the sizeof(char). If PTRINC had been written so that p was a pointer to an integer (an int *), p++ would have a scaling factor of sizeof(int), which typically evaluates to 2 or 4 bytes depending upon the compiler. In C, scaling factors allow pointers to be manipulated without forcing the programmer to worry about adjusting the given pointer by hand. The pointers, however, must be of the proper type. This can cause a problem because the C language does not let you create pointers to an object with variable size. The objects may be created in the heap via the various alloc routines. This concept will be covered in a future article. Note that such objects do not need to be a linked list and that there are useful cases in which a method for obtaining the object's length after the allocation is unnecessary (dynamic copies of ASCIIZ strings, for example). The output of line 19 reflects the increment of p by 1. Line 18 allowed p to point at the b within the array, as expected. Before continuing our examination of PTRINC, we need to discuss side effects, sequence points, expression statements, and the comma operator--features of the C language that relate directly to line 20 of PTRINC. Side Effects A side effect is a change in an object's value due to the evaluation of an expression. Side effects happen as a result of function calls, assignment expressions, auto-increment expressions, and auto-decrement expressions. Unless an expression is void, it produces a value; in the process, it also produces a side effect. Side effects involving functions can modify the value of a variable in a way that might not be clear in a large or complex program. For example, in int i; void f(void) { i++; } main() { f(); } it may appear that the call to f is innocent, but actually it modifies the value of i. No real harm is done here, although the increment of i may not be apparent in a larger program or in a program with many statements occurring in f and main. If, however, we modify this code slightly by introducing another function named g int g(void) { i++; f(); return (5); } and add j = g() + i; to main, the resulting value of j may not be clear. Either way we are dealing with a side effect. Code such as this assignment statement i = 5; has the side effect of replacing the value of i with 5. As long as an expression modifies an object, a side effect occurs. Therefore a side effect can have ill effects. The classic example of a side effect with an expression statement is: a[i] = i++; In this situation, the compiler has the choice of computing the value of the subscript before or after it computes the value on the right-hand side of the equal sign. We would expect this unpredictability to be incorrect, but the compiler nevertheless makes this decision on its own. Three more examples are represented by the code: i = 5; func(i++, i++); In the first scenario, the compiler chooses to generate code to perform the post increment after both i's are evaluated. Both arguments will be 5; the value of i after the expression can be either 6 or 7. The i's are post-incremented in the second scenario in the order shown; the first argument will be 5 and the second one will be 6. The value of i after the expression will be 7. The i's are post-incremented in the last scenario in reverse of the order shown; the compiler is not constrained to evaluate the arguments of a function in left-to-right order. The second argument will be 5 and the first one will be 6. The value of i after the expression will be 7. Generally speaking, the third scenario will usually be true, since most compilers put the arguments to functions on the stack in reverse order. This, however, does not necessarily imply that the arguments are evaluated as they are put on the stack. All side effects of function arguments are evaluated before calling the function, which makes them consistent and does not conflict with any of the scenarios given above. As tricky as these examples are, we can accept them after giving them some thought. In fact, these examples appear much more sensible than the statement: j = i++ + i++; I will leave this one as an exercise. The possibilities it presents will be similar to those discussed above. Expression Statements Most C programmers do not realize that C is more expression-based than any of the other popular general-purpose languages. For instance, even though we typically refer to a statement such as i = 5; as an assignment statement, it is actually an assignment expression. A statement in C can be one of the standard flow control statements such as for, goto, and break. In the Microsoft C Version 5.1 Optimizing Compiler manual, the syntax summary on page 229 in Appendix B of the User's Guide Language Reference manual informs us that a statement may be of the form: expression; The assignment listed above is actually an expression statement containing an assignment. We've already examined several situations in PTRINC that prove that this example is an assignment expression statement. For instance, line 18 of PTRINC contains p++. Even though this resembles p = p + 1, the syntax is clearly a construct that allows it to stand alone. Even p = p + 1 results in a value--this statement not only assigns p + 1 to p, it also returns p. The return value of the expression is often ignored; this is how statements such as j = i = 5 can occur without wreaking havoc. Sequence Points A sequence point invokes the concept of a hypothetical C machine. In such a machine, the compiler's code generator must completely follow your C code. A more precise definition is shown in Figure 8. Every statement in this C machine must be executed in the order in which it has been coded: statement order followed by flow of control order. This means that the hypothetical C machine allows few if any optimizations to take place. This C machine exists only in theory; in practice no compiler will follow it. The comma operator and the volatile keyword (which many compilers still do not support) serve the same function as the hypothetical C machine. The comma operator is explained in the next section. The volatile keyword was explained in "A Guide to Understanding Even the Most Complex C Declarations," MSJ (Vol. 3, No. 5). Their purpose is to ensure that agreement points exist at appropriate places. Comma Operator As with all sequence point operators, the comma operator ensures that its operand expressions are evaluated in left-to-right order. The result of the comma operator is always the value of the last expression in the expression list. Generally, the comma operator cannot force the evaluation of a given statement sequence, such as a for or block compound statement. The comma operation is only valid within expressions--it does not control statements, though it may be used within the controlling expression of a conditional statement. The comma operator is different from a semicolon; it occurs in expressions, while the semicolon terminates statements. Also, note that the comma operator has nothing to do with the comma that is used to separate function arguments. In the latter case, the comma serves strictly as punctuation. PTRINC.C We've just discussed many features of C; I'll now explain how they pertain to line 20 of PTRINC. Line 20 of PTRINC contains the statement *p++. To determine what this means, look at the operator precedence table (see Figure 7 for the output of line 20). A precedence table can be found on page 137 of the Microsoft C 5.1 Optimizing Compiler Language Reference. The precedence table reveals whether line 20 increments the contents of p, increments p, changes the address of p, increments the contents of the object p points to, or performs some combination of these. Some of these cases are obvious. The first two cases are the same for the reasons stated earlier. Case c cannot be true, since we do not have the ability to change the address of a variable, especially when that variable is an identifier. We are left with the following possibilities: line 20 increments the contents of p, increments the contents of the object p points to, or performs some combination of the above. We know that the associativity of the ++ operator from the operator precedence table chart means "do not perform the increment until after the (simple) expression has been evaluated" because it is a post-increment operator. Since the ++ immediately follows p, it's p that's being incremented, not the contents of the object p points to. The simple expression referred to in the preceding paragraph is *p. However, like the case presented in the previous sections, we don't assign or use the value of *p. It is discarded--*p goes nowhere and then p is incremented afterwards. This is equivalent to having coded the following: *p, p++; This syntax is a valid C expression and a valid C statement. It demonstrates that we wouldn't normally want to write code this way. For all practical purposes, line 20 increments only p; the implication is that it wasn't supposed to be written that way. We should be cautious of such situations, because it's too easy to code a construct such as *p++ without stopping for a second to see what is actually happening, especially since the compiler is not going to object. Sometimes, however, it makes sense to write code that uses the value of this kind of expression. For example, the assignment to c in the following code is legitimate and powerful: char *p; char c; p = ...; c = *p++; ...; Statements like the assignment to c present a style issue because such coding practices can create hard-to-read code. Instead you could write the assignment on two lines: c = *p; p++; Such constructs, however, are idioms of C, so whether or not you should use them is simply a matter of style. Next Case In line 22, (*p)++ might increment the contents of p or increment the contents of the object to which p points. Since the ++ is outside the parentheses, it is doubtful that they modify p. Of course, (*p)++ is a legal expression that modifies the contents of p and ++ occurs outside the parentheses in that case. Also, if we envision a case such as i++; being at some point equivalent to (i) = (i) + 1; line 22 could be interpreted as: (*p) = (*p) + 1; Therefore line 22 increments the contents of the object to which p points. Case c wasn't worth considering since there is only one ++ operator in the statement. Figure 7 shows that p has not changed when we print it on line 23. What has changed is that we've added 1 to c; under the ASCII character set this changes the c to a d. Line 22 could also have been written as either of the following: ++(*p) ++*p since a standalone statement such as i++ could be rewritten as ++i. If the result of these statements were to be used, however, this would not be true since it makes a difference where ++ is placed, according to the precedence table. For example, the expression ++(*p) is evaluated and then incremented, but the expression ++*p is incremented first and then evaluated. It may not be clear whether the ++ on line 24 applies to the * or the p. It can't apply to the * without the introduction of some other operand, like the ++*p above, which is mentioned as a syntax level reference since the two statements have different meanings. As with all these examples, the operator precedence table provides the key. Since the ++ binds with the p and is a pre-increment, the compiler will not let the dereference occur without first performing the increment. Therefore, line 24 could have been coded p++, *p; or: ++p, *p; Much of what is true about line 20 is also true of this statement. The effect of 24 is that p is incremented. Line 25 confirms this. On line 26, we simply check to make sure we know where we are. Turmoil strikes again on line 27 and this time we have two sets of problems. First we are going to use the value of the expression instead of ignoring it as we did in earlier statements. We are left with: *(p++) Clearly, p will be incremented, but what's not clear is when this will happen. At first glance, we would say that it's the same as p++, *p which is the same thing line 24 mapped into because expressions in parentheses must be performed first. We'd be wrong. The statement actually maps to *p, p++ which is the same as line 20. The explanation of this is that p++ constitutes a complete subexpression involving a long value no different from what while ((c = getchar()) != EOF) does in its assignments to c. In this case, the subexpression has a side effect involving a post operator. We know that the side effect will occur sometime during that statement and before the next statement in this situation. The operator precedence table clarifies the situation by showing that the post ++ operator and the unary * operator are always right-associative. This means that if we look at the reverse situation of *p++, it must be interpreted as *(p++) rather than (*p)++. Therefore, the parentheses on line 27 only order the p, not the p++, since the increment is performed afterwards. Line 27 has a second problem. We can see that when line 27 is output and fed from the C preprocessor into the C parser it looks something like this: fprintf((&_iob[2]), "*(p++)=%d/%c\n", *(p++), *(p++)); (This code should all be on one line; it is broken here due to space considerations--Ed.) Notice that we have ended up with two *(p++) expressions--a statement containing a macro with a side effect. This is a very subtle problem. The decimal value of the character printed by the first *(p++) in this example may actually be different from the character value we expected because we still do not know when the p++'s will occur. On my UNIX machine, the p's were incremented in the reverse of the order shown, probably because of the way the arguments on the stack were put. The value for the d is correct, but the decimal value 101 represents an e (see Figure 7). Line 28 verifies that there were two increments on my machine. There might not have been since we cannot predict whether both increments start using the same value of p or whether one is based on the other. Lines 29, 31, and 32 are self-explanatory except for the third character of the array, which is now a d rather than a c. This change occurred in line 22. Summary This article has introduced and explained many pointer derivations and shown some common and often unexpected side effects of using pointers in your code. A solid understanding of how pointers work, and a willingness to examine even simple pointer constructions for hidden errors, will help you to write concise, fast code. Figure 1 #include <stdio.h> main() { char *p = "abcdefg"; printf("%c\n", *(p++)); } Figure 3a main() { char c = 'a'; char d = 'z'; /* .... */ char *p; /* .... */ p = &c; p = &d; /* .... */ } Figure 4 main() { char c = 'a'; char d = 'z'; /* .... */ char *p; char *p2; /* .... */ p = &c; p2 = p; p = &d; /* .... */ printf("%p\n", p); printf("%p\n", p2); } Figure 5A: Lmain() { char c = 'a'; char d = 'z'; /* .... */ char *p; char *p2; /* .... */ p = &c; p2 = p; p = &d; /* .... */ printf("%c\n", *p); *p2 = 'A'; } Figure 6 1 #include "debug.h" 2 3 /* 4 * ptrinc.c 5 */ 6 7 main() 8 { 9 char *p; 10 char *origp; 11 12 p = "abcdefghijk"; 13 prints(p); 14 origp = p; 15 prints(origp); 16 17 printp(p); 18 p++; 19 printp(p); 20 *p++; 21 printp(p); 22 (*p)++; 23 printp(p); 24 *++p; 25 printp(p); 26 printc(*p); 27 printc(*(p++)); 28 printc(*p); 29 printp(p); 30 31 prints(p); 32 prints(origp); 33 } Figure 7: Assume that p and origp are pointers each 4 bytes in length located at addresses 1200 and 1210, respectively. Line No. Address ----> | 1 1 1 1 1 1 1 1 1 1 1 1 | 2 2 2 2 2 2 2 2 2 2 2 2 V 3 3 3 3 3 3 4 4 4 4 4 4 4 5 6 7 8 9 0 1 2 3 4 5 output 12 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' '\0' --> elements of array | p points at the beginning of array 13 p="abcdefghijk" 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 4 5 6 7 8 9 0 1 2 3 4 5 14 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' '\0' p points at the beginning of array origp points at the beginning of array 15 origp="abcdefghijk" 17 p=1234 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 4 5 6 7 8 9 0 1 2 3 4 5 18 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' '\0' A A | | origp p 19 p=1235 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 4 5 6 7 8 9 0 1 2 3 4 5 20 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' '\0' A A | | origp p 21 p=1236 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 4 5 6 7 8 9 0 1 2 3 4 5 22 'a' 'b' 'd' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' '\0' A A | | origp p 23 p=1236 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 4 5 6 7 8 9 0 1 2 3 4 5 24 'a' 'b' 'd' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' '\0' A A | | origp p 25 p=1237 26 *p=100/d 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 4 5 6 7 8 9 0 1 2 3 4 5 27 'a' 'b' 'd' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' '\0' A A | | origp p or 'a' 'b' 'd' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' '\0' A A | | origp p 27 *(p++)=101/d 28 *p=102/f 29 p=1239 31 p="fghijk" 32 origp="abddefghijk" Figure 8 Sequence Point The point at which all side effects of previous evaluations shall be complete and no side effects of subsequent evaluations shall have taken place. Agreement Point The sequence point for some object or class of objects at which the value of the object(s) in the real implementation must agree with the value prescribed by the abstract machine. The sequence points of a C program are: ■ at the call to a function after the arguments to the function have been evaluated ■ at the end of the first operand of the following operators: logical and: && logical or: || conditional: ? comma: , ■ at the end of a full expression such as: an initializer the expression in an expression statement the controlling expression of a selection statement (if or switch) the controlling expression of an iteration statement (while, do, or for) the expression in a return statement. In-Line C Program Debugging DEBUG.H is a header file that performs in-line program debugging. An example of DEBUG.H (see Figure A) is shown in TESTDEBUG.C (see Figure B). TESTDEBUG.C includes DEBUG.H in a source file and uses it and three macro variables to control the output. One macro controls the occurrence of any debug output, another controls the printing of subroutine entry and exit points, and the third controls the printing of subroutine names with the respective debugging information. The output shown for TESTDEBUG.C demonstrates the advantage of using DEBUG.H, which is that the argument to the print* and DEBUG* macros allows the code to be less tedious and more easily read (assuming the debug information is to be left in). In this program, a statement such as printd(somevariable) produces somevariable = the value of somevariable as its output. This avoids having to code fprintf(stderr, "i=%d\n", i); or a similar statement whenever you want to see what value a variable holds. For instance, line 16 in TESTDEBUG.C produces i = 5. The expanded form can become a nuisance, especially with more complicated variables such as a member of an element of an array of structures. As in DEBUG.H, there are five categories of output statements controlled by the last character of the printx statement. (You can add more.) Use s to print the value of a string, d to print an int or short, ld to print a long, p to print a pointer, and c to print a character. Also note that DEBUG.H contains a group of macros, the printx macros, which you can use to print unconditionally. This is in contrast to the DEBUGx macros which have an effect on your code only if you define DEBUGL1 before you include DEBUG.H. Therefore, TESTDEBUG.C will always print the value of i, but whether it prints any of the other output, including the subroutine trace, depends upon DEBUGL1 being an active macro. Space does not permit us to elaborate upon the internal workings of DEBUG.H. Remember that for the purposes of this article, print?(var); allows you to produce var = value. TESTDEBUG.C should be sufficient to demonstrate the interface to DEBUG.H. Figure A: /* debug.h */ #ifndef DEBUG_H #define DEBUG_H /* Depending upon which compiler you have, STRINGIZING should be set to determine if #v syntax is allowed. For some compilers, this setting may not be obvious since __STDC__ may not be supported or may be set to an inappropriate value. */ #if __STDC__ == 1 #define STRINGIZING 0 #else #define STRINGIZING 1 /* 1 means compiler can use #v syntax. You may need to swap this line with the other one */ #endif #include <stdio.h> #ifndef SUBTRACE #define SUBSTART(x) #define SUBEND(x) #else #if STRINGIZING == 1 #define SUBSTART(func) fprintf(stderr, "%s()ing\n", #func) #define SUBEND(func) fprintf(stderr, "%s()ed\n", #func) #else #define SUBSTART(func) fprintf(stderr, "%s()ing\n", "func") #define SUBEND(func) fprintf(stderr, "%s()ed\n", "func") #endif #endif #if STRINGIZING == 1 #define prints(str) fprintf(stderr, #str "='%s'\n", str) #define printd(dec) fprintf(stderr, #dec "=%d\n", dec) #define printld(ldec) fprintf(stderr, #ldec "=%ld\n", (long)ldec) #define printp(p) fprintf(stderr, #p "=%p\n", p) #define printc(ch) fprintf(stderr, #ch "=%d/%c\n", ch, ch) /* printc without a side effect is properly written as: #define printc(ch) {char c = (ch);fprintf(stderr, #ch "=%d/%c\n", c, c); } */ #else #define prints(str) fprintf(stderr, "str='%s'\n", str) #define printd(dec) fprintf(stderr, "dec=%d\n", dec) #define printld(ldec) fprintf(stderr, "ldec=%ld\n", (long)ldec) #define printp(p) fprintf(stderr, "p=%p\n", p) #define printc(ch) fprintf(stderr, "ch=%d/%c\n", ch, ch) /* printc without a side effect is properly written as: #define printc(ch) {char c = (ch);fprintf(stderr, "ch=%d/%c\n", c, c); }*/ #endif #define PRINTSUBfprintf(stderr, "%s:", __FILE__) #ifndef DEBUGL1 #define DEBUG1(a1) #define DEBUG2(a1, a2) #define DEBUG3(a1, a2, a3) #define DEBUG4(a1, a2, a3, a4) #define DEBUG5(a1, a2, a3, a4, a5) #define DEBUG6(a1, a2, a3, a4, a5, a6) #define DEBUGS(str) #define DEBUGD(d) #define DEBUGLD(ld) #define DEBUGC(c) #define DEBUGP(p) #define DEBUGCALL(func) #else #ifdef DEBUGF #define DEBUG1(a1) PRINTSUB, fprintf(stderr,"%s",a1) #define DEBUG2(a1,a2) PRINTSUB, fprintf(stderr,a1,a2) #define DEBUG3(a1,a2,a3) PRINTSUB, fprintf(stderr,a1,a2,a3) #define DEBUG4(a1,a2,a3,a4) PRINTSUB, fprintf(stderr,a1,a2,a3,a4) #define DEBUG5(a1,a2,a3,a4,a5) PRINTSUB, fprintf(stderr,a1,a2,a3,a4,a5) #define DEBUG6(a1,a2,a3,a4,a5,a6) PRINTSUB, fprintf(stderr,a1,a2,a3,a4,a5,a6) #define DEBUGS(str)PRINTSUB, prints(str) #define DEBUGD(dec)PRINTSUB, printd(dec) #define DEBUGLD(ld)PRINTSUB, printld(ldec) #define DEBUGC(c)PRINTSUB, printc(c) #define DEBUGP(p)PRINTSUB, printp(p) #define DEBUGCALL(func) PRINTSUB, func #else #define DEBUG1(a1) fprintf(stderr, "%s", a1) #define DEBUG2(a1, a2) fprintf(stderr, a1, a2) #define DEBUG3(a1, a2, a3) fprintf(stderr, a1, a2, a3) #define DEBUG4(a1, a2, a3, a4) fprintf(stderr, a1, a2, a3, a4) #define DEBUG5(a1, a2, a3, a4, a5) fprintf(stderr, a1, a2, a3, a4, a5) #define DEBUG6(a1, a2, a3, a4, a5, a6) \ fprintf(stderr, a1, a2, a3, a4, a5, a6) #define DEBUGS(str)prints(str) #define DEBUGD(dec)printd(dec) #define DEBUGLD(ld)printld(ldec) #define DEBUGC(c)printc(c); #define DEBUGP(p)printp(p) #define DEBUGCALL(func) func #endif #endif #endif Figure B: #define DEBUGL1 #define DEBUGF #define SUBTRACE #include "debug.h" /* TESTDEBUG.C */ main() { int i = 5; int *pi = &i; char *s = "string"; SUBSTART(main); printd(i); DEBUGD(i); DEBUGS(s); DEBUGP(&i); DEBUGP(pi); SUBEND(main); } When executed, this program prints: main()ing i = 5 testdebug.c:i = 5 testdebug.c:s = 'string' testdebug.c:&i = 0005:0FB8 testdebug.c:pi = 0005:0FB8 main()ed Integrating Subsystems and Interprocess Communication in an OS/2 Application Richard Hale Shaw Programming in the OS/2 systems is always a challenging and interesting experience. One reason is that the OS/2 environment provides a platform for integrated application development that is richer than its rivals and outperforms them. Another is that the OS/2 application programming interface (API) is a robust set of functions that makes even the most complicated applications much easier to design, develop, test, and complete. The intent of this series of OS/2 articles (MSJ Vol. 4, Nos. 1 through 6) has been twofold. First, its purpose was to introduce you to OS/2 programming, showing you how to write multithreaded applications, use the Vio, Kbd, and Mou subsystems, and allow multiple OS/2 applications to communicate via interprocess communication (IPC). This final article presents an application that integrates all of those aims. Second, the series intended to give you the OS/2 kernel programming knowledge needed to program for OS/2 Presentation Manager (hereafter PM). Good PM programming requires a thorough understanding of three elements: the OS/2 multithreaded application programming environment and application programming interface (API); PM windowing facilities (and their associated API functions); and PM event-driven, message-based architecture (a superset of the architecture of the Microsoft Windows environment). The first five articles in this series have provided an overview of the OS/2 API. This final article discusses how to use OS/2 IPC to implement an event-driven, message-based queue that you can use to construct applications whose architecture is similar to the architecture of PM. Directory Information The programs in this article prepare you for PM programming. They assume that you are familiar with how to work with multiple threads, the various subsystems, and IPC. The programs integrate many (if not all) of the concepts presented in the previous articles in the series. They are also practical; that is, they are either useful in their own right or can be easily modified to fill a particular need. The directory information (DI) application presented in this article consists of three programs built on a client-server architecture. One, a directory server, gives disk and directory information to any client program that requests it, provided the client knows how to initiate the request and use the results. Each of the client applications uses a set of function calls to generate and process a request for directory information. The function calls, combined with the server, are a high-level interface to the OS/2 DosFindFirst function; this interface is simple and easy to use, it hides the complexities of finding files and managing memory, and it avoids having to make multiple calls to the DosFindNext function. The server program can also be modified to run on a network, where it could be enabled to display information about directories that a program running on a node might not be able to access. And, although I didn't take this step, the client functions can be placed in an OS/2 dynamic-link library (DLL), where they can be used by more than one program at a time even though they are only loaded once by OS/21. Two kinds of client programs are presented here. One is a simple command-line directory utility that I ported to OS/2 and modified to use the directory information functions. The other is a directory program for end users that employs multiple threads to manage input and output and lets the user work with the keyboard and mouse. Client-Server Architecture The DI functions implement a classic client-server relationship. If, for example, the client asks "C:\OS2\*.* ", the server will respond with a list of all the files that meet that specification. The client can request the expansion of multiple filespecs at a time. A filespec may consist of a full or partial path and a standard MS-DOS operating system file specification, including wildcard characters. If a path is not included, the client's current directory is assumed; each filespec can have a different path. Each filespec can also have a unique matching attribute value. The server will return the results in a form that makes it easy for other DI functions to sort them, retrieve them, and make use of the information. The DI client-server architecture is implemented using OS/2 interprocess communication. When you start the DI server, it opens a queue that is known to the DI functions bound into a client program. A client DI function can pass requests for directory information to the server via this queue, and the server will create a thread to service the request. The server can manage up to 20 threads at a time, making it easy for it to handle multiple demands for directory information. Each server thread receives a request packet that is sent via the queue from the client. (The queue actually carries a pointer to a shared memory block that contains the request.) This request packet includes all the information necessary for the server to process the request, as well as a pointer to a work area segment (also in shared memory) where the server thread must place the results. When the server thread has finished its task, it clears a semaphore in the request header, which signals the client that the thread's task is complete. Then the client can use other DI functions to retrieve the results of the request. With DI functions, a client program can prepare a request by making one or more calls to the DiMakeRequest function. When the request has been prepared, the client can send it to the server using the DiSendRequest function, which will return when the request is complete. (You could put the call to this function in a separate thread, so that the application can continue while the server fulfills the request.) When the thread calling DiSendRequest returns, the client can retrieve the request information by calling several different functions that are described later in the article. Finally, the request information should be released with DiDestroyRequest. An example of the code required to write such a client program is shown in the file DISIMPLE.C (see Figure 1). DI Data Structures DI functions use the two data structures shown in Figures 2 and 3. (Full source code for the figures in this article can be downloaded from any MSJ bulletin board--Ed.) Figure 2, REQUESTHEADER, is the primary structure and contains the information the server will need to process and complete the request. This information is stored in a segment that is known as the request header segment. This segment is allocated by the first call to DiMakeRequest (which also allocates the work segment). The information in the REQUESTHEADER structure includes the following: ■ the client's selector to the work segment ■ the server's selector to the request header segment and a server selector and pointer to the work segment ■ the handle of the server's queue and the server's process ID (PID) ■ the total size of the header and work segments ■ the number of filespecs that make up the request ■ the number of results found by the server ■ a RAM semaphore that blocks the client thread calling DiSendRequest until the semaphore is cleared by the server ■ other pointers used by either the client or server The header also includes an array of one or more structures of type DIRINFORESULT, one for every filespec contained in the request (see Figure 3). One of these structures is automatically included in a header segment when the segment is allocated by DiMakeRequest. An application can subsequently call DiMakeRequest for each filespec to be included in the request. When a new filespec is included, DiMakeRequest resizes the header segment (using DosReallocSeg) to include space for an additional DIRINFORESULT structure. Each DIRINFORESULT structure contains the following information about a particular filespec: ■ the filespec for which to search ■ the path to the filespec ■ the attributes to use when searching for files ■ a pointer to the first file that the server finds matching the filespec ■ the number of files found by the server for the filespec ■ a pointer used by the functions that retrieve the results from the work segment Creating a DI Request An application program should call DiMakeRequest to create a directory information request. The DiMakeRequest function is part of DI.C (see Figure 4). DiMakeRequest requires the address of a variable that will be a handle to the request, the complete path and filespec that the server will expand, and the attributes that the server should use when expanding the filespec. The handle is of type PVOID and should be initialized to NULL before the first call to DiMakeRequest, so that the function will create a new request. Otherwise, the function will assume that the handle refers to an existing request. You can add filespecs to the request by repeated calls to DiMakeRequest with the same handle before you call DiSendRequest. When you are ready to send a request to the server, your application should call DiSendRequest. Remember that this call will block until the server has finished processing the request, so you should place this call in a separate thread if you need to avoid blocking the main thread of a client application. You should either call DiDestroyRequest before reusing the handle or use a different handle for subsequent requests. DiMakeRequest assumes that the server is running and has created its queue; otherwise, the function will wait until the server's queue has been created. Once the server has created its queue, DiMakeRequest will open it using DosOpenQueue, which returns a queue handle and the PID of the server. DiMakeRequest will use DosGiveSeg and the server's PID to enable the work and header segments to be shared with the server. DiMakeRequest will add each filespec to the request, expand it to include a full path, and create a new DIRINFORESULT structure for each filespec. The details of this process and how the DI functions work with the DosFindFirst OS/2 function are discussed in the sidebar "DosFindFirst and DI Memory Management." Once an application has finished adding filespecs to a request, it can then use DiSendRequest to send the request to the server (see Figure 2). The DiSendRequest function does some housekeeping, writes the request to the queue, then waits until the server has completed its task and clears the header semaphore. DISERVER Program The DISERVER program handles requests for directory information from client processes that pass these requests to the server by using its queue. DISERVER is also a thread server--it creates a new thread to process every request it receives. DISERVER.C, which contains the entire server program, consists of two functions: main, which waits for requests and then starts a new thread to service them, and server_thread, which contains the code that services the request. The main thread of DISERVER is fairly simple. It creates its queue and blocks on a call to DosReadQueue to wait for requests from client processes. Once it receives a request, it finds an available thread and creates a specific thread to service the request. It uses an array of SERVERTHREAD structures that contains a semaphore, a thread ID, a request pointer, a thread number, and a stack for each thread. To find an available server thread, the main thread simply looks for an unused structure (identified by a thread ID set to zero) in this array. Then it sets the thread's semaphore and creates the new thread. The thread will immediately block on the semaphore, allowing the main thread to set the thread's ID, request pointer, and number. Then the main thread clears the thread's semaphore and returns to the DosReadQueue call to wait for the next request. There is no need for DISERVER to provide any visual output in routine use, so you can run it in a PM window, a full-screen session, or in a background screen group. I occasionally found it useful (and fascinating) to watch its progress, however, so I left a number of messages embedded in the code. These will print if you start DISERVER with the /v (verbose) parameter. Server Threads Each thread started by the server will block on its semaphore until the semaphore is cleared by the server's main thread. Then the server thread will enter a loop, calling DosFindFirst for each filespec in the request. The server thread does not have to call DosFindNext, since the thread provides a large buffer in which DosFindFirst can place its results. DosFindFirst creates a FILEFINDBUF structure for each filename found; these structures are placed in a buffer area that is specified when the function is called. If DosFindFirst indicates that no files are found or that the path doesn't exist, the returned error value is placed in the DIRINFORESULT structure for that filespec. If only one file is found and the file's attribute byte has its directory bit set, the thread will try the call to DosFindFirst again, but this time with " \*.* " appended to the filename, allowing it to expand to a single matching directory. Otherwise, DosFindFirst will place the resulting filenames in the work segment area. A pointer to this area and a variable indicating the remaining space are adjusted with each pass through the loop, so that subsequent calls to DosFindFirst will not overwrite information from previous calls. If a call to DosFindFirst is successful, the server thread will set the fields of that filespec's DIRINFORESULT structure for the first file found and the number of files found. The server thread will also adjust the total number of files found in the request header. Then the thread enters a loop to adjust the buffer pointer past the last file found (if verbose mode is on, it will also print each filename as it passes through the loop), sleep for one second, and return to the top of the loop in order to process the next filespec. Once all filespecs have been processed, the thread will use pointer arithmetic to calculate the total space used in the work segment. Then it frees the work segment, clears the header semaphore to notify the client that it is finished with the request, and frees the header segment. The server thread then sets its own thread ID to zero, thus notifying the main thread that it is terminating (and allowing the main thread to reassign the server thread's data area to a new thread). Finally, it will call DosExit to terminate itself. Return to the Client When the server clears the request header semaphore, the client thread that called DiSendRequest and blocked on the semaphore can proceed. The thread using DiSendRequest allocates a new work segment, copies the data and adjusts the pointers, and frees the old work segment. Then it returns to the calling thread. At this point, the client can retrieve the request results. Even though the client has only made a call to DiMakeRequest for each filespec and a single call to DiSendRequest, the results are in; the server has actually done all the real work. The client can use several functions to access the results of a request. In DISIMPLE.C, a call is made to DiGetResultHdl to get a handle to the results of filespec zero, since this was the first (and only) filespec in the request. An application can call DiGetNumResults, however, to find out how many filespecs were placed in a request (and, incidentally, how many filenames were found) and then call DiGetResultHdl for each of the filespecs. Once it has obtained a handle to a result, an application can either call DiGetFirstResult and DiGetNextResult to retrieve the name of each file found, or DiGetFirstResultPtr and DiGetNextResultPtr to retrieve a pointer to the FILEFINDBUF structure returned by OS/2 for that filename. Although the FILEFINDBUF structures are not in an array, a client can call DiBuildResultTbl to create an array of pointers to the results of a request. The use of DiBuildResultTbl will be demonstrated in one of the client programs discussed below. An application should call DiDestroyRequest when it has finished accessing the results of a DiSendResult call. DiDestroyRequest will call DosFreeSeg to free the work and header segments and permit OS/2 to discard the segments. DiDestroyRequest will reset the request handle to NULL, too. The next part of this article is a discussion of two client applications--the LS utility and the DIPOP program. LS Utility The LS utility has an unusual history. I first wrote LS as an add-on utility for a UNIX-like DOS2 shell. When I began programming for OS/2, one of the first assignments I gave myself was to port my favorite tools from DOS to OS/2. I had always found the UNIX LS program more useful and informative than the DOS DIR command, so my DOS-based imitation of the LS program was one of the tools I ported to OS/2. Once the DI routines were complete, I found it a trivial task to modify LS to use them--and found myself cutting away a great deal of the existing code, now that the complexities of DosFindFirst and DosFindNext were absent. LS not only illustrates how easy it is to incorporate and use the DI routines in an application, it also demonstrates the portability of C; after all, the program imitates a UNIX utility, was written for DOS, and was ported to OS/2. LS, like its UNIX counterpart, takes a series of command-line filespecs, expands them, and prints the results of the expansion. One or more options can be specified that control the output of the program (sorting, printing in columns, paging, filtering out certain types of files, and so on). LS is only slightly more complex than the DISIMPLE program. It uses the read_options function to read any command-line arguments, sets the options, and removes the options from the command line. Then it enters a loop and calls DiMakeRequest for each of the command-line filespecs. Finally, it calls DiSendRequest to issue the request to the server. Upon returning from DiSendRequest, LS calculates the remaining space on the drive on which the first filespec is found. To do this, it calls DiGetResultHdl to get a handle to the first filespec and DiGetResultDir to access the full path to that filespec, which is another convenient use of the DI functions. In order to access, sort, and print the results, LS calls DiBuildResultTbl to build a table of the results. This function will create an array of pointers to each of the FILEFINDBUF structures placed in the work segment by the server. DiBuildResultTbl returns a pointer to the results table that the calling thread can use to access these structures. LS passes the table to the C library qsort routine, which sorts the pointers in preparation for printing the file information. (The details of the sort comparisons are contained in the function qscmp, which can sort the pointers by the date-time stamp of the file or the filename.) LS calls the print_entries function in order to access the table and print the filenames. Print_entries can display the output in a long listing (the default), with name, date, time, size, and attributes, or as filenames only, in a single column or multicolumn listing. It can also wait for a keystroke at the end of each screenful of information and will automatically detect the screen size of a window. When it has finished displaying the results, LS calls DiDestroyResultTbl in order to free the table and DiDestroyRequest to free the header and work segments. DIPOP Program The DIPOP client program is more complex. It uses the DI functions to access the server, expand filespecs, and retrieve directory information; it has a user interface, and it uses multiple threads to manage input and output. Like LS, DIPOP is a character-mode directory information program. But DIPOP is not a command-line utility; it has a real user interface and you can leave it running in a PM window and use it as needed. DIPOP handles only one filespec at a time, but you can use either the keyboard or the mouse to select the drive and directory and to create the filespec used to search for files. You can also select a combination of attributes to be used. To facilitate mouse input in a character-mode application, I've adapted the screen buttons that were used in an earlier article in this series ("Exploring the Key Functions of the OS/2 Keyboard and Mouse Subsystems," MSJ Vol. 4, No. 4). Finally, DIPOP implements a message queue scheme that is reminiscent of the one found in Presentation Manager (see the sidebar "Create Message Queues Without Using Presentation Manager"). DIPOP's architecture is inherently multithreaded. Separate threads monitor mouse and keyboard input and another thread displays the results of each filespec request in a portion of the screen. The main thread starts each of the other threads, reacts to information from the keyboard and mouse threads, creates and sends requests to the DI server, and notifies the print thread when the server has returned results. Two key components are essential to the architecture of DIPOP. The first is the message queues, which the mouse and keyboard thread use to notify the main thread of input events and which the main thread uses to notify the print thread that output is available. The other is the use of screen buttons, which are kept in a table (an array of structures) in DIPOP.C. Several routines, found in BUTTON.C , are available to initialize, display, paint and repaint the buttons, and to notify a thread when a mouse click occurs inside them. Each button is assumed to be three screen rows high, use a double border on the top and bottom and a single border on the sides, and be painted as white foreground on red background when highlighted (this is controlled by a macro at the top of the source file). The structure for each button includes the text to be displayed in the button, the button's row and column coordinates, and a button attribute for repainting the button. A variable controls whether the button is a press button (turns briefly on and then off when clicked), a toggle button (turns off when clicked and on when clicked again), or an input button (does not change state when clicked). Finally, each button contains values that are returned when the left or right mouse button is clicked on it and an accelerator key is pressed (this allows the button to be accessed by using the keyboard). Using the message queues and mouse buttons minimizes the overhead of the main thread and efficiently delegates the management of input to the keyboard and mouse threads. The buttons also allow the main thread to create an input screen easily, and simplify maintenance and modifications by the programmer. For example, I added a directory attribute button to the interface long after the rest of the code had been written, spending only a few moments and adding only a couple of lines of source code. How DIPOP Works The use of the screen buttons and the message queue simplifies the operation of DIPOP immensely. The main thread begins by calling the message queue function, MsgQCreate, to create its message queue. Next, the program clears a semaphore that it will use to control the scrolling by the print thread (signaled by pressing the Pause button). Then the main thread creates each of the three threads and sleeps briefly, to allow them to get started. The main thread then calls a set of customized functions that initialize the buttons and the screen and display the various buttons. Finally, the main thread enters a loop that contains a large switch statement. It will block on a call to MsgQGet until it receives an event message and will use the switch statement as a table of messages and actions. Unidentified messages are passed, by default, to the buffermgr function, which will attempt to use the message to manipulate the filespec entry field. Thus, once the main thread of DIPOP receives an event message from its message queue, it can process the message swiftly and effectively and take any necessary action. All mouse and keyboard input in DIPOP is detected by the appropriate thread. The mouse thread uses the ButtonPressed function to screen out unwanted mouse events and to include events for which a screen button was pressed. The keyboard thread uses the function AcceleratorPressed to distinguish button accelerators from other keyboard events. Both of these threads call the message queue function, MsgQSend, to notify the main thread of these events. Since all message queue messages are a single unsigned word, the application is designed to use the high-order bits in the message to pack additional meaning into the message. This is how ordinary ASCII values, scan codes, and mouse events are encapsulated into messages (see the SCANCODE macro and EVENTCODE macro at the beginning of the program). Most event messages pertain to building a request for directory information, but there are three exception messages. When the main thread receives an ENTER message, it calls SendRequest to create the request and send it to the DI server. After the server has serviced the request and before the function returns, it places the request on the print thread's message queue, notifying the print thread that there is work to be done. If the main thread receives a PAUSE_EVENT message, it will toggle the state of the semaphore that controls the processing by the print thread. This lets the main thread control the print thread instantly, allowing the user to control the scrolling of the display temporarily. If the main thread receives an ESCAPE_EVENT message, it will close its message queue, destroy any existing DI request, clear the screen, and exit. User's View of DIPOP Figure 5 shows what the user sees when using DIPOP. The screen is divided into two sections: half of the screen area is in a window that extends from the upper-left corner of the screen, and the other half is filled with buttons and runs along the right and bottom sides of the screen. To build a directory request, users just type a filespec (at the cursor position in the entry field) and select the drive and directory in which the server will search for the filespec. To select the drive, the mouse is used to click the drive button. Clicking the left mouse button will decrement the drive letter choice; clicking the right button will increment it. As users change drives, the current directory for each appears along the upper border of the directory button. The path that appears is always the one that is used in a DI request. The directory button can be used to see into a list of subdirectories in the current directory. Users can click on this button to view the entries in the list. Clicking the right mouse button on the [..] button changes to a directory; clicking the left mouse button on the [..] button changes to the parent directory. When changing directories, the path on the upper border of the drive button will reflect the change. The [\] button can be used to jump to the root directory of any drive. In addition to typing the characters that make up the filespec, users can use the mouse to click the " * ", " \ ", " . ", and " ? " buttons. When one of these buttons is clicked, the program inserts the character represented by the button at the current cursor position in the entry field. Users can control the scope of the search by clicking the attribute buttons that appear on the right side of the screen. They can access the attribute buttons via an accelerator key, which is Alt and the first letter of the button text; for example, Alt-H toggles the Hidden button. When users are ready to initiate a request for directory information, they press the Enter key or click the Enter button. To pause the scrolling of the results, they click the Pause button (clicking the Pause button again resumes scrolling). Pressing Alt-C or clicking the Clear button clears the entry field and resets the attribute buttons. Users can press Esc or click the Esc button to terminate the program. Improvements After designing the DI programs presented in this article, I thought of several improvements that could be made to them. The server could be modified to use named pipes, which would allow it to be used transparently over a network. Moving the results from a memory block that belongs to the server into a memory block that belongs to the client would require some work--but it could be done. As I mentioned, the DI functions could be implemented as dynamic-link libraries. One improvement to DIPOP involves a bug in OS/2 that occurs when it is run in a full-screen window. If DIPOP is run in a full-screen window, the mouse pointer will leave traces of itself on the screen whenever a screen button is clicked. The code that repaints a button (in the ButtonPress routine) simply paints the button, sleeps momentarily, and repaints the button. Unfortunately, there is no coordination between VIO and the mouse driver, so VIO doesn't take into account that the mouse driver might need to know that the mouse pointer should be redisplayed in a new color--thus, traces of the mouse pointer are left on the screen. The only way around this problem is to store the current location of the mouse pointer before doing any screen update that might overwrite it, save what is stored under the mouse pointer, perform the screen update, and restore what was under the mouse pointer. It's unfortunate that the user has to do this, since this is just what VIO and the mouse driver are supposed to do. With the advent of OS/2 Version 1.2, there will be opportunities to make some minor modifications to the programs so that they will work transparently with the new High Performance File System (HPFS). These modifications should allow long, free-form filenames, few or no restrictions on path punctuation, new extended attributes, and new access control lists. The changes should also allow the use and display of the additional date-time stamps provided by HPFS. The error checking and exception handling in the DI programs can be improved. The server could be modified to clear all pending semaphores before it exits and to allocate the thread stacks dynamically instead of keeping them in a table. Other improvements to DIPOP might include better error and exception handling, syntax checking, and a wait indicator. Finally, the most important improvement would be a graphical user interface with real buttons, list boxes, and scroll bars. The next step for the program is obvious: a port to Presentation Manager. Figure 1 #define INCL_DOS #include<os2.h> #include<stdio.h> #include"di.h" void main(int argc, char **argv); void main(int argc, char **argv) { PVOID requesthdl = NULL, resulthdl; USHORT i, numresults; char filename[40]; if(argc != 2) { printf("Usage: disimple filespec\n"); DosExit(EXIT_PROCESS,0); } DiMakeRequest(&requesthdl,argv[1],0); DiSendRequest(requesthdl); DiGetResultHdl(requesthdl,0,&numresults,&resulthdl); for( i = 0; i < numresults; i++) { if(i) DiGetNextResult(resulthdl,filename); else DiGetFirstResult(resulthdl,filename); printf("\t%s\n",filename); } DiDestroyRequest(&requesthdl); } Figure 2 typedef struct _requestheader { ULONG RAMsem; /* RAM semaphore for client */ SEL rselector; /* Client selector to results */ HQUEUE qhandle; /* Handle to server's queue */ PID qowner; /* PID of server (queue owner) */ VOID FAR *resultptr; /* Server pointer to work area */ SEL serverhsel; /* Server selector to header */ SEL serverwsel; /* Server selector to results */ PCH currentdir; /* Client's dir in work area */ PCH requestspec; /* Next part of work area */ USHORT size; /* Current size header segment */ USHORT resultsize; /* New size of result segment */ USHORT totalresults; /* Total results found */ USHORT numRequests; /* Number of requests being made */ DIRINFORESULT resultArray[1]; /* Request structures */ } REQUESTHEADER; typedef REQUESTHEADER FAR *PREQUESTHEADER; Figure 3 typedef struct _dirinforesult { USHORT attributes; /* Attributes this request */ PCH filespec; /* File spec to use */ PCH currentdir; /* Current dir this request */ PFILEFINDBUF firstfile; /* First result */ PFILEFINDBUF nextfile; /* Next result */ USHORT numfound; /* Number of files found */ USHORT errorval; /* Error value returned */ struct _dirinforesult *next; /* Related structure if found */ } DIRINFORESULT; typedef DIRINFORESULT FAR *PDIRINFORESULT; Figure 4 #define INCL_DOS #define INCL_ERRORS #include<os2.h> #include<mt\stdio.h> #include<mt\string.h> #include<mt\stdlib.h> #include<mt\ctype.h> #include"errexit.h" #define DICODE #include"di.h" #define lastchar(str) (str[strlen(str)-1]) void diInit(PID *qowner, HQUEUE *qhandle); void adddriveletter(PCH *filespecs,USHORT driveno); void makefpath(char *org,char *result,char *currentpath, USHORT currentdrive); void getdriveinfo(USHORT *currentdrive,char *currentpath, USHORT *psize); USHORT diallocseg(USHORT size, SEL *oursel,PID other,SEL *othersel); void convertptr(VOID **ptr,SEL newsel); /* DiMakeRequest This function creates or adds to an existing directory information request. A new request is created if the pointer (hptr) is set to NULL. Note that this is the pointer whose address is passed to the function. The filespec is a full path and file specification with optional wildcards. The attribute parameter will control the files that are found. */ void DiMakeRequest(PREQUESTHEADER *hptr, PCH filespec, USHORT att) { PREQUESTHEADER header; PDIRINFORESULT resultstru; SEL hselector,serversel; void far *results; USHORT retval,size,psize = 0,setdir = FALSE; HQUEUE qhandle; PID qowner; PCH requestspec; char resultbuf[_MAX_DIR],*resultfspec; char currentpath[_MAX_PATH]; USHORT currentdrive; if(!(header = *hptr)) diInit(&qowner, &qhandle); /* Now gather the information to prepare the header: ■ Get the current disk drive ■ Get the length of the current path ■ Get the size of the request arguments (the requestspec) ■ Allocate the header segment ■ Make it available to the server process */ getdriveinfo(&currentdrive,currentpath,&psize); if(!header) /* If no header is allocated */ { /* Allocate header segment */ if(retval = diallocseg(size = sizeof(REQUESTHEADER),&hselector, qowner,&serversel)) error_exit(retval,"diallocseg"); header = MAKEP(hselector,0); /* Make header pointer */ *hptr = header; header->serverhsel = serversel; /* Allocate work segment */ if(retval = diallocseg(MAXSEGSIZE,&header->rselector,qowner, &serversel)) error_exit(retval,"diallocseg"); results = MAKEP(header->rselector,0) /* Make pointer */ header->RAMsem = 0L; /* Initialize semaphore */ header->resultptr = MAKEP(serversel,0); /* Set pointer to work area */ header->serverwsel = serversel; /* Selector to work area */ header->numRequests = 0; /* Set number of requests */ requestspec = (PCH)results; /* Set to work area */ header->currentdir = requestspec; /* Set pointer to it */ *requestspec++ = (char)(currentdrive + 'A' - 1); /* Add drive letter */ strcpy(requestspec,":\\"); /* And ":\" */ requestspec += 2; /* Move pointer past them */ DosQCurDir(currentdrive,requestspec,&psize); /* Add current directory */ requestspec += (strlen(requestspec)+1); /* Move pointer past dir */ header->qowner = qowner; header->qhandle = qhandle; } else { requestspec = header->requestspec; qowner = header->qowner; qhandle = header->qhandle; size = header->size + sizeof(DIRINFORESULT); hselector = SELECTOROF(header); if(retval = DosReallocSeg(size,hselector)) /* Resize segment */ error_exit(retval,"DosReallocSeg"); } /* Resultstru always points to the next available structure */ resultstru = &header->resultArray[header->numRequests]; strupr(filespec); /* Set arg to upper case */ memset(resultstru,0,sizeof(DIRINFORESULT)); /* Clear structure */ resultstru->attributes = att; /* Set attributes */ /* Get full path filespec */ makefpath(filespec,resultbuf,currentpath,currentdrive); resultfspec = strrchr(resultbuf,'\\'); /* Find last backslash */ if(strcmp(resultbuf,header->currentdir)) /* If not in currentdir */ { strcpy(requestspec,resultbuf); /* Copy path */ resultstru->currentdir = requestspec; /* Set pointer */ requestspec += (strlen(requestspec)+1); /* Set pointer to next spot */ } else /* Use default directory */ resultstru->currentdir = header->currentdir; /* Set pointer */ strcpy((char *)requestspec,resultfspec); /* Copy the filespec */ resultstru->filespec = requestspec; /* Set a pointer to it */ requestspec += (strlen(requestspec)+1); /* Set to next position */ header->size = size; header->numRequests++; header->requestspec = requestspec; } void DiSendRequest(PREQUESTHEADER hdr) { PCH *resptr,*newptr; USHORT offset,retval,i; SEL newsel,serverwsel = SELECTOROF(hdr->resultptr); PBYTE sheader = MAKEP(hdr->serverhsel,0); /* Make pointer */ /* Adjust resultptr to point to available space */ resptr = MAKEP(hdr->rselector,0); /* Create ptr to result */ offset = (hdr->requestspec - (PCH)resptr); /* Get offset to use */ hdr->resultptr = MAKEP(serverwsel,offset); /* Reset pointer */ /* Write request to the queue */ if(retval = DosWriteQueue(hdr->qhandle,0,hdr->size,(PBYTE)sheader,0)) error_exit(retval,"DosWriteQueue"); /* Wait for server to finish */ DosSemSetWait(&hdr->RAMsem,SEM_INDEFINITE_WAIT); /* Get new segment */ if(retval = DosAllocSeg(hdr->resultsize,&newsel,SEG_NONSHARED)) error_exit(retval,"DosAllocSeg"); newptr = MAKEP(newsel,0); /* Make pointer to segment */ memmove(newptr,resptr,hdr->resultsize); /* Copy from old segment */ convertptr(&hdr->currentdir,newsel); /* Convert pointers */ convertptr(&hdr->requestspec,newsel); for( i = 0; i < hdr->numRequests; i++) { convertptr(&hdr->resultArray[i].filespec,newsel); if(SELECTOROF(hdr->resultArray[i].currentdir) == hdr->rselector) convertptr(&hdr->resultArray[i].currentdir,newsel); convertptr(&hdr->resultArray[i].firstfile,newsel); convertptr(&hdr->resultArray[i].nextfile,newsel); } DosFreeSeg(hdr->rselector); /* Free old segment */ hdr->rselector = newsel; /* Set for new selector */ } void DiGetNumResults(PREQUESTHEADER header,USHORT *numresults, USHORT *numrequests) { *numresults = header->totalresults; *numrequests = header->numRequests; } void DiDestroyRequest(PREQUESTHEADER *header) { USHORT retval; PREQUESTHEADER hdr = *header; if(retval = DosFreeSeg(hdr->rselector)) /* Free work segment */ error_exit(retval,"DosFreeSeg"); if(retval = DosFreeSeg(SELECTOROF(hdr))) /* Free header segment */ error_exit(retval,"DosFreeSeg"); *header = NULL; /* Set pointer to NULL */ } char *DiGetResultFspec(PDIRINFORESULT result) { static char *p = "*.*"; if(result->errorval == DIREXPANDED) return p; return result->filespec; } char *DiGetResultDir(PDIRINFORESULT result) { static char dirbuf[80]; if(result->errorval == DIREXPANDED) { strcpy(dirbuf,result->currentdir); strcat(dirbuf,"\\"); strcat(dirbuf,result->filespec); return dirbuf; } return result->currentdir; } void DiGetResultHdl(PREQUESTHEADER header,USHORT requestnum,USHORT *num, PDIRINFORESULT *resulthdl) { *resulthdl = &header->resultArray[requestnum]; *num = (*resulthdl)->numfound; } void DiGetFirstResult(PDIRINFORESULT result, char *buffer) { result->nextfile = result->firstfile; DiGetNextResult(result,buffer); } void DiGetNextResult(PDIRINFORESULT result, char *buffer) { if(!result->nextfile) { *buffer = '\0'; return; } strcpy(buffer,result->nextfile->achName); result->nextfile = (PFILEFINDBUF)(&(result->nextfile->cchName)+ result->nextfile->cchName+2); } void DiGetFirstResultPtr(PDIRINFORESULT result, PFILEFINDBUF *ptr) { result->nextfile = result->firstfile; DiGetNextResultPtr(result,ptr); } void DiGetNextResultPtr(PDIRINFORESULT result, PFILEFINDBUF *ptr) { if(!result->nextfile) { *ptr = NULL; return; } *ptr = result->nextfile; result->nextfile = (PFILEFINDBUF)(&(result->nextfile->cchName)+ result->nextfile->cchName+2); } void DiBuildResultTbl(PREQUESTHEADER header, PFILEFINDBUF **table) { SEL tablesel; USHORT retval, i; PFILEFINDBUF f,*temp; if(retval = DosAllocSeg((header->totalresults*sizeof(PFILEFINDBUF)), &tablesel, SEG_NONSHARED)) error_exit(retval,"DosAllocSeg"); temp = MAKEP(tablesel,0); f = header->resultArray[0].firstfile; for( i = 0; i < header->totalresults; i++) { temp[i] = f; f = (PFILEFINDBUF)(&(f->cchName)+f->cchName+2); } *table = temp; } void DiDestroyResultTbl(PFILEFINDBUF **table) { USHORT retval; if(retval = DosFreeSeg(SELECTOROF(*table))) error_exit(retval,"DosFreeSeg"); *table = NULL; } void diInit(PID *qowner, HQUEUE *qhandle) { USHORT retval; /* Try to open the queue to the directory server */ if(!(retval = DosOpenQueue(qowner,qhandle,DIRINFOQNAME))) return; if(retval != ERROR_QUE_NAME_NOT_EXIST) error_exit(retval,"DosOpenQueue"); else error_exit(retval, "DosOpenQueue - Server probably hasn't opened queue"); } void convertptr(VOID **ptr,SEL newsel) { USHORT offset = OFFSETOF(*ptr); *ptr = MAKEP(newsel,offset); } /* Assumes that globals, currentpath and currentdrive are properly initialized */ void makefpath(char *org,char *result,char *currentpath, USHORT currentdrive) { char drive[_MAX_DRIVE], dir[_MAX_DIR], fname[_MAX_FNAME], ext[_MAX_EXT]; char currdir[_MAX_DIR], *backup, *outdir; USHORT driveno,cdirsize = _MAX_DIR-1, retval; strupr(org); /* Make the path uppercase */ _splitpath(org,drive,dir,fname,ext); /* Get path components */ /* If we have a full path from the user at this point, we don't have to do anything more--whatever they ask for, they get. If we don't have a full path, we will need to get the full path to the current directory of that drive, and then reconcile the working directory or parent directory to end up with the "real" path to the file. However, if the path is on the same drive, we can use currentdir and save the time of a DosQCurDir call. */ if(!(*drive)) /* If no drive letter */ { driveno = currentdrive; *drive = (char)(currentdrive+'A'-1); strcpy(&drive[1],":"); } else driveno = (*drive-'A'+1); if(*dir != '\\') /* If not a full path */ { if(driveno != currentdrive) { *currdir = '\\'; if(retval = DosQCurDir(driveno,&currdir[1],&cdirsize)) error_exit(retval, "DosQCurDir - probably invalid drive or directory"); } else strcpy(currdir,&currentpath[2]); if(lastchar(currdir) != '\\') strcat(currdir,"\\"); strcat(currdir,dir); strcpy(dir,currdir); } /* dir now has full path to the filespec, reconcile and restore */ while(backup = strstr(dir,"\\..\\")) /* Remove any "\..\" */ { for(outdir = backup-1; (*outdir != '\\') && (outdir > dir); outdir--); /* Now outdir is '\' dest */ backup += 3; /* now backup is source '\'*/ strcpy(outdir,backup); } while(backup = strstr(dir,"\\.\\")) /* Remove any "\.\" */ { outdir = backup; backup += 2; strcpy(outdir,backup); } _makepath(result,drive,dir,fname,ext); /* Put it all back together */ } void getdriveinfo(USHORT *currentdrive,char *currentpath,USHORT *psize) { ULONG drivemap; USHORT retval; DosQCurDisk(currentdrive,&drivemap); /* Get current drive number */ *currentpath = (char)(*currentdrive+'A'-1); strcpy(&currentpath[1],":\\"); *psize = _MAX_PATH; if(retval = DosQCurDir(*currentdrive,&currentpath[3],psize)) error_exit(retval,"DosQCurDir"); DosQCurDisk(currentdrive,&drivemap); /* Get current drive number */ *psize = 0; DosQCurDir(*currentdrive,NULL,psize); /* Get size of current path */ } USHORT diallocseg(USHORT size, SEL *oursel,PID other,SEL *othersel) { USHORT retval = 0; /* And shareable by server */ if(!(retval = DosAllocSeg(size,oursel,SEG_GIVEABLE))) retval = DosGiveSeg(*oursel,other,othersel); return retval; } Create Message Queues Without Using Presentation Manager Early in the design of the DIPOP program, it became apparent that using semaphores to pass information and synchronize input from the mouse and keyboard threads would be awkward and cumbersome. If a semaphore were used, the mouse or keyboard thread might be blocked for too long while waiting for the main thread to acknowledge and act on the event. Plus, users could alternate between clicking mouse buttons and pressing keys; using semaphores to determine the order of events would be messy. It is also possible that multiple events could occur and accumulate quickly--which would be intolerable if the threads were not able to process input efficiently. A facility was needed that would report keyboard and mouse events in the order in which they occurred. Such a facility would allow the event information to accumulate in that order until the main thread acted on them. In addition, the mechanism would be efficient, simple, and, with luck, elegant. The solution was the handful of functions in MSGQ.C (see Figure A). These functions apply a software interface layer to OS/2 queues and create and manipulate a series of message queues. OS/2 queues are typically used to transfer a pointer to shared memory (this is how the DI client-server functions use them), but they can also transfer a user-defined code in the form of a 2-byte, unsigned word. You can find more information on OS/2 queues in the article on IPC in this series, "A Complete Guide to OS/2 Interprocess Communications and Device Monitors," MSJ Vol. 4, No. 5. Since the contents of this user-defined code are determined by the programmer, you can use it to create your own messages, which can be customized from application to application or used over again. In addition, queues can be an efficient way to pass the event code. Thus, the functions in MSGQ.C allow one thread, the message queue owner, to use MsgQCreate to create a message queue. This function returns a handle that the queue owner passes to MsgQGet, where the thread will block until a message is received and returned. Other threads can send messages to the queue owner thread by opening the message queue with MsgQOpen, which also returns a handle. These threads can pass the handle to MsgQSend, along with a message that is placed in the queue. The message queue functions hide the details of manipulating and using OS/2 queues; a thread only needs the name of the queue to create or open it and it can use the returned handle when referencing the queue. Message Queues and DIPOP In DIPOP, message queues are essential to coordinating the activities of the different threads and passing information from one thread to another. Two message queues are created: one is owned by the main thread and is used by the mouse and keyboard threads; the other is the print thread, which uses a message queue to allow access to the results of DI requests from the main thread. After the main thread has created the other threads and completed its initialization, it calls MsgQGet and blocks until it receives an input event message from the keyboard or mouse thread. These threads block on keyboard input (by using KbdCharIn) or mouse input (using MouReadEventQueue). The keyboard thread can quickly identify a keystroke as an ASCII key, scan code, or accelerator key, place the key in the main thread's message queue, and wait for the next keystroke. The mouse thread waits for mouse events, screens out unwanted events (such as mouse movements when no buttons are pressed), identifies important mouse clicks, and places the corresponding messages in the message queue. Then it resumes waiting for the next mouse event. The main thread, meanwhile, remains blocked much of the time, waiting for event messages to appear in its queue and reacting to them when they do appear. The result is that the main thread's code is a large switch statement that resembles the kind of message-processing code found in Windows3 and in PM programs. This design worked so well that I decided to use it again when I wrote and tested the print thread code. It was easy to have the print thread create a message queue, block on the call to MsgQGet until the main thread posted a message, and use the message to display the results of a DI request. The messages generated by the keyboard and mouse threads are largely keyboard codes with the high-order bits set to indicate whether they are from the text buttons used in DIPOP. Since a message passed to the print thread is the selector of the request header segment, the main thread uses the SELECTOROF macro to extract the selector from a request header pointer or handle, and that selector is the message that is placed in the queue. The print thread uses the MAKEP macro to convert this selector back into a pointer or handle to the request. Finally, a welcome side effect of using the message queues in DIPOP was the compactness of the code for the mouse and keyboard threads. With so little to do, these functions stayed small, tight, and efficient. Advantages Message queues let you create a message-based, event-driven architecture that simplifies the workings of your program and the organization of your application's code. They let you focus the program's code on reacting to events; for example, user input. This makes the job of writing the code much simpler. (Just make sure you have a way to handle any foreseeable input.) Message queues also make it easier to write more efficient code, since a thread will wait for something to do and then promptly do it. In DIPOP, message queues are the means of coordinating input and output among multiple threads. You won't have to worry about flags or semaphores or the order of events--that's taken care of for you. Message queues allow each thread to monitor a source of input and independently report events to another thread, and for the events to be stored and retrieved in a simple, orderly fashion, without additional programming overhead. Message queues also allow more flexibility in designing a program and simplify stepwise refinements to the code. If, for example, a new capability is needed, you simply define the input (such as a keystroke or mouse click), define the steps for processing that input (such as adding a case to the switch statement), and you're done. A multithreaded program architecture allows for a proper division of the labor; that is, each thread is delegated the responsibility of handling a specific task. Message queues help ensure the smooth operation and flow of information from one thread to another, since they allow each thread to report important events and return to what the thread does best--waiting for the next event to occur. DosFindFirst and DI Memory Management The most important benefit of the DI functions and server is that they give applications a simpler, more effective, more efficient interface to the OS/2 file -searching functions. DosFindFirst and DosFindNext fit several application contexts flexibly, but their main purpose is to make it easy to port applications to OS/2 from DOS. Before exploring how to use them more effectively under OS/2, here's a brief look at their DOS predecessors. Finding Files under DOS When a DOS application needs to expand a wildcard filespec, it uses two functions of Int 21h. Typically, the application gains access to the DOS disk transfer area (DTA) and passes the filespec to function 4Eh of Int 21h. This function places the name, size, attributes, and date-time stamp of the first file found into the DTA. Then the application enters a loop and generates the interrupt again, this time with function 4Fh, and retrieves the information on each file until no more files are found. This process is illustrated in two real-mode sample programs--DIRREAL1.C and DIRREAL2.C. The former illustrates how the entire process is accomplished in C. The latter shows how high-level additions to Microsoft's real-mode C run-time library simplified the process somewhat. Under OS/2, you can create a protected-mode equivalent, like DIRPROT.C. That program demonstrates how the DosFindFirst and DosFindNext functions simplify a port of a DOS program to OS/2 by keeping the logic of the application intact. To port the code, just replace the Int 21h calls with DosFindFirst and DosFindNext, adjust the code to use the OS/2 FILEFINDBUF data structure, and you're done. Although this approach to searching for files must be followed under DOS, it isn't particularly efficient for OS/2. First, the calling application has to loop through multiple calls to the DosFindNext function and there is no way to determine how many of these calls must be made ahead of time. The loop could take a long time to finish if the number of files is large. Second, the application can process information on only one file at a time, or it must manage memory in an attempt to store all the file information found. There are two accepted approaches to storing all of the information. One is to allocate space for each piece of file information dynamically. The other is to make an assumption about the number of files to be found, allocate the space to hold them, and add to that space when necessary. Either approach can slow down an application if a large number of resulting files is found. DosFindFirst and DosFindNext In some OS/2 applications, using a loop with DosFindFirst and DosFindNext is reasonably efficient, especially when you're searching for a single file or when time and memory management are not critical to the calling thread. To use DosFindFirst, an application must supply a pointer to a filespec, a directory handle, an attribute word in which at least one bit should match a bit in the attributes of the resulting files, a buffer in which to place the results, the length of the buffer, and a variable in which the application specifies the number of files to find. (OS/2 will replace the variable with the number of files found.) DosFindNext requires the same parameters, except for the filespec and attribute word. DI Approach Although the DOS-like approach to finding files makes it easy to port applications from DOS to OS/2, DosFindFirst can be used more efficiently. Indeed, DosFindFirst is quite capable of returning all the files found in a single call, eliminating the need to call DosFindNext at all. The trick is to provide a memory buffer big enough to hold all the resulting FILEFINDBUF structures. The DI functions and the DI server make providing that memory buffer easy. They allocate a full 64Kb segment for use in the DosFindFirst call and place all of the results found in this segment. Then they essentially resize the segment, so that when DiSendRequest returns to the calling thread, the application owns only the memory that is necessary to hold the results. The DI functions assume that all results found for a single request, including those returned by the expansion of multiple filespecs, will fit into a single 64Kb segment. What this means, for example, is that if an application made a request with 10 filespecs and each expanded to 100 files, in which every filename occupied a full 13 characters, the result would occupy 36,000 bytes. (A full 13-character filename with the terminating NULL makes a FILEFINDBUF structure that is 36 bytes long; thus the result is 10 x 100 x 36, or 36,000 bytes.). A more likely situation would be 10 filespecs expanded to 200 files each in which the average length of the filenames was eight characters--in this case, the results would need a total of 62,000 bytes (10 x 200 x 31). This would leave 3535 bytes to spare. Although for most applications 64Kb is a safe assumption, an adjustment should be made under OS/2 Version 1.2 to use multiple work segments and allow for a longer average filename. Managing Memory When an application calls DiMakeRequest to create a new request (with a handle initialized to NULL), the function allocates the header segment and a 65,535-byte work segment and makes them shareable with the server, which will use the work segment for calls to DosFindFirst. Later, when the server makes this call, it will ask DosFindFirst to return all files found in one call, eliminating the need to call DosFindNext. When a filespec is added to a request via DiMakeRequest, the full path and filespec are stored in the work segment. The function also calls DosReallocSeg to enlarge the header and append a new DIRINFORESULT structure to it (pointers in this structure are set to the path and filespec). Thus, the DI functions can access these structures as a contiguous, variable-length array from an offset in the header, eliminating the complicated work with pointers that would be necessary if the structures and filespecs were intermixed. Since the actual number of structures is determined by the numRequests field of the header structure, the header can be extended indefinitely. In addition, this method allows filespecs and paths of varying length to be stored and accessed with little or no effort. As a footnote, I should mention that if a filespec's path is the same as the application's (stored in the work segment on the first call to DiMakeRequest), the path is not stored; its DIRINFORESULT pointer is instead set to the application's path. The additions to the beginning of the work segment do not have too great an impact on the remaining space. A large number of filespecs (perhaps 20) with long paths (say an average of 50 characters each) would occupy about 1000 bytes--leaving 64,535 bytes for the resulting filenames after expansion. Thus, the workspace available to the server when it first calls DosFindFirst is 65,535 minus the space occupied by the filespecs and paths. Just before DiSendRequest places a request in the server's queue, it adjusts the server's workspace pointer to an offset beyond the paths and filespecs in the work segment. As I indicated earlier, the server asks DosFindFirst to return, in one call, all files found that match the filespec. After each call, the server adjusts the workspace pointer to point beyond the most recent FILEFINDBUF results. Thus, each call to DosFindFirst in a request uses a smaller buffer space that is found at offsets further and further into the work segment. When a server thread is finished with a request, it calculates the space occupied by the results in the work segment and stores that result in the request header for use by DiSendRequest. Then it clears the request header semaphore, frees the header and work segments, and terminates itself. At this point, the client owns both segments; OS/2 will not discard shared segments until both processes have freed them. It would be ideal if the client could adjust the work segment down to the size occupied by the results of the request--there's no need for a 64Kb segment if only 4Kb, 16Kb, or even 20Kb are being used. Unfortunately, you cannot use DosReallocSeg to shrink a shared segment. Therefore, as soon as the server thread handling the request has freed the work segment and is about to free the header segment and terminate itself, it prepares a new work segment, which is the same size as the occupied portion of the old one. Then it copies the contents of the old segment to the new one and adjusts the appropriate pointers in the header to use the selector of the new work segment. Finally, DiSendRequest frees the work segment (which OS/2 will now discard) and returns. Note that the client can now call DiDestroyRequest in order to free the header and the new work segment. The DI functions and server make as much memory available as possible in the calls to DosFindFirst, then shrink the memory to the required size when the results are in. The overhead of memory management is relatively small, and the DI functions conveniently handle multiple filespecs in a request, while hiding the workings of the code from the client application. Further, the time spent managing memory is made up by eliminating the calls to DosFindNext and packaging the results in a form that makes it easy for applications to employ functions like DiBuildResultsTbl to access them. You could use the DI functions in an application any time the application needs to use wildcards to search for more than one file, when time is critical, and when you want to preserve the simplicity of an application. Exploring Dynamic-Link Libraries with a Simple Screen Capture Utility Kevin P. Welch A new programming concept that the Microsoft Windows environment has introduced to personal computers is dynamic linking and dynamic-link libraries (DLLs). In Windows1, dynamic linking refers to the manner in which a function call in one module is dynamically related to object code in another. DLLs contain a collection of functions that are linked dynamically. Although DLLs are not directly executable and don't receive messages, they are the building block on which the entire Windows interface is based. However, despite this relationship, DLLs remain somewhat mysterious to many Windows programmers. The PRTSC utility is a relatively simple application that uses a DLL to capture screen images and place them on the Windows clipboard. When screen capture is activated, the Alt-PrtSc key combination copies a bitmap representation of the client area of the active window, the window frame, or the entire display to the clipboard. Besides serving as a useful documentation utility for your programs, PRTSC also demonstrates how to employ DLLs, keyboard hook functions, and the Windows clipboard. In fact, this utility is very similar to one in ClickArt Scrapbook+, published by T/Maker Company; both are powerful and full-featured extensions of the Windows clipboard. System Hooks The section of the Windows Version 2.0 Programmer's Reference manual on the SetWindowsHook function is very intriguing. This function was not thoroughly documented in previous versions of Windows, and developers were advised to leave it alone until it became a formal part of the documented application program interface (API). System hooks are particularly interesting because they enable you to intercept various messages before they are processed by Windows or dispatched to an application. With this capability you can check for interesting keystrokes (as PRTSC does), monitor application or system messages, and even record all system events for subsequent playback. System hooks are necessarily a shared resource; when you install a hook you can affect all applications. Also, because of expanded memory considerations under LIM 4.0, most hook functions must be in DLLs to ensure that they are never paged out and made inaccessible. Associated with each system hook is a filter function, an application-supplied routine that is installed into a chain of functions and called whenever events of a specified type occur. Filter functions normally have the format shown in the code in Figure 1. The nCode parameter typically specifies whether the filter function should process the information or call the DefHookProc function and pass it back to the system. The wParam parameter provides additional information on the event, usually defining the context in which the event occurred. Finally, the lParam parameter is used to transfer additional information that further clarifies the event in question. This could be a far pointer to a message data structure or simply a set of binary flags that describe the event. Using this filtering scheme, you can define system hooks that intercept and/or process the kinds of events shown in Figure 2. Once defined, system hooks are inserted into the chain with the SetWindowsHook function. When you call this function you provide a code that specifies the type of hook you are installing, followed by a procedure-instance address of the appropriately defined filter function. Note that this function must be exported in the library's module definition file. Although libraries can use a function address directly since they can only have one data segment, applications must use MakeProcInstance to retrieve a procedure instance. lpfnOldHook = SetWindowsHook (nHookType, (FARPROC)HookFn ); The value returned by the SetWindowsHook function is the procedure-instance address of the filter function previously installed in the chain, if any such filter exists. This value should be saved as needed when passing messages down the chain with the DefHookProc function. Of these system hooks, all but the application message hook must be defined within the context of a DLL. The application message hook intercepts only task-specific messages and is not subject to the same limitations as the other system hooks. While they are in use, system hooks can seriously degrade the performance of Windows. Because of this, you should restrict their use to special-purpose applications or development tools that help you to debug and test an application. When you have finished using a system hook, you should promptly remove the filter using the UnhookWindowsHook function : UnhookWindowsHook (nHookType,(FARPROC)HookFn ); PRTSC Library The PRTSC utility consists of a single DLL that contains one assembly language and four conventional C functions. Of these five routines, the keyboard hook function is perhaps the most interesting and unusual. Although DLLs like PRTSC form the basis of the Windows API, programmers who are new to Windows seldom understand them. This is partly due to the fact that they can be loaded only once and are in effect resources for use by other applications. Further complicating matters is the need to write at least one assembly language routine when developing a DLL. When you link a conventional Windows application with LINK4, it defines _astart as the program entry point. This function is defined in the standard Windows object library you link to your program and is responsible for performing the required housekeeping chores prior to calling WinMain, the perceived entry point for the application. When _astart is called, the CPU registers are defined as shown in Figure 3. Unfortunately, a DLL like PRTSC requires a different scheme. By design, PRTSC, like all other libraries, operates without a stack segment, using the caller's stack in place of its own. This difference is reflected in the use of LIBRARY instead of NAME for the program name in the module definition file PRTSC.DEF. When you make this change, LINK4 is instructed to use LibInit as the program entry point in place of _astart and to define the CPU registers as shown in Figure 4. LibInit, defined in PRTSC1.ASM (see Figure 5), is then responsible for performing any required housekeeping, including initialization of the local heap. Unfortunately, this subtle variation and the subsequent assembly language programming have prevented some people from experimenting with DLLs. With some extra code, the Windows object library could include an entry point for DLLs that performs this initialization and calls something like LibInit with the library instance handle and a pointer to the command line. The programmer would then write LibInit in his or her favorite language and perform any desired initialization steps before returning control to the system. In the case of PRTSC, LibInit simply calls PrtScInit, which is defined in PRTSC2.C , where all library initialization is performed. See excerpts in Figure 6. (The full source code can be downloaded from any of the MSJ bulletin boards--Ed.) During initialization PrtScInit checks the current display adapter type, activates the keyboard hook, and iteratively searches for the MS-DOS Executive window. The search ends when it has found the window, which is identified by the Session class name. Note that lstrcmp is used instead of strcmp because the szClassName variable is defined on the stack and is not directly accessible when using a near pointer (remember that SS != DS in DLLs). Using the handle to the MS-DOS Executive window, PrtScInit appends a Screen Capture... menu option to the end of the Special pull-down menu and subclasses the entire window. This effectively enables the library to intercept any message sent to the Executive window, including the message generated by our newly appended option. Although this may not be the most cooperative way to display the screen capture control panel, it demonstrates how one application, in this case a library, can subclass another. However, this is probably not an acceptable programming practice for a commercial application. The next function in PRTSC2.C is PrtScFilterFn. This routine effectively filters all of the messages sent to the MS-DOS Executive window. The only two messages of interest are the ones generated when the user selects the Screen Capture... menu option or when the window is destroyed. When subclassing an application with a function like PrtScFilterFn, there are four ways in which messages can be handled, as shown in Figure 7. The first message PrtScFilterFn intercepts is the one generated when the user selects the Screen Capture... menu option from the MS-DOS Executive window. In this case it displays a dialog box using the PrtSc dialog box template contained in the resource file appended to the library. Because the CMD_CAPTURE message is of interest only to the PRTSC library, it is not passed to the real window function. Also note that because the PRTSC library, like most other DLLs, has only one data segment, the dialog box is displayed without creating a procedure instance (using MakeProcInstance) of the dialog box window function. The second message that PrtScFilterFn intercepts is the WM_DESTROY message. In this case, screen capture is automatically turned off (assuming it is active) and the MS-DOS Executive window filter function is removed. This ensures that screen capture is not left active without a means of controlling it. Most of the intercepted messages are ignored by PrtScFilterFn and passed to the real window function; otherwise, the window would not work and the system would probably stop. Following the PrtScFilterFn in PRTSC2.C is the PrtScDlgFn function. This routine is responsible for processing all the messages relating to the screen capture control panel that is displayed when the Screen Capture... menu option is selected. In this function, the keyboard hook function is inserted or removed when the user selects a new screen capture mode. Again, note how the PrtScHook function is used directly, without calling MakeProcInstance to create a procedure-instance address. The last function in PRTSC2.C is PrtScHook. This is the most complicated routine in the library; it is responsible for intercepting each keystroke and checking whether it is the Alt-PrtSc key combination. The first thing this function does is examine the nCode parameter to see if some action is expected. If so, the virtual keycode and corresponding keyboard state are examined to see if Alt-PrtSc has been entered. When encountered, the top-level window handle is determined and the screen coordinates of the capture area calculated. In cases where window boundaries extend beyond the screen dimensions, appropriate clipping is performed to ensure a completely defined image. The remainder of the function is devoted to copying the screen image and placing it on the Windows clipboard. When this is completed, the clipboard is closed, which causes a WM_DRAWCLIPBOARD message to be sent down the clipboard viewer chain. All associated resources are then removed. If any part of this process fails, a message box is displayed, providing some feedback to the user on the problem that was encountered. Building PRTSC Building a DLL is only slightly different from constructing a traditional Windows application. Before doing this, however, you will need to enter or download the following source files: PRTSC (make file), PRTSC.DEF (module definition file), PRTSC.H (header file), PRTSC.RC (resource file), PRTSC1.ASM (assembly language startup code), and PRTSC2.C (C source code). In addition to these source files, you must install the following software tools on your system: Microsoft Macro Assembler Version 5.0 or later, Microsoft C Compiler Version 5.0 or later, and the Microsoft Windows Software Development Kit (SDK) Version 2.03 or later. When you have all the tools and source files in place, you can create the PRTSC library by entering the following command: MAKE PRTSC Using PRTSC Once you have created the PRTSC library, try it out by running Windows and double clicking PRTSC.EXE. When PRTSC is loaded, a small message box will be displayed in the center of your screen indicating that the Alt-PrtSc key combination is available for screen capture to the clipboard. If you select the Screen Capture... option under the Special pull-down menu of the MS-DOS Executive window, you can view the current settings, turn screen capture off, or select another capture mode. The Screen Capture menu option will be checked whenever Alt-PrtSc is active. If you run additional MS-DOS Executive windows after loading PRTSC, the Screen Capture... menu option will be listed only on the first instance. If you accidentally close this instance, screen capture will be turned off and you will no longer be able to load the library; only one instance of a library is allowed, and it has already been loaded. With a little effort, you can enhance the PRTSC library to prevent a lock-out by subclassing a second MS-DOS Executive instance when the first window is closed. Another feature of the PRTSC library you can experiment with is the monochrome conversion option, assuming you have a color display system. As you can imagine, the capture of color screen images to the clipboard can consume large amounts of system memory. Also, many Windows applications handle such bitmaps incorrectly, especially when they are transported in files to computers with different display subsystems. By choosing the Convert to Monochrome option you can automatically convert the screen images you capture to monochrome by using the conversion routines built into GDI. Although in certain cases this color mapping will produce unexpected results, like mapping yellow to black, it will perform acceptably with minor adjustments to your system color palette. You should also try to capture menus using PRTSC library. If you hold down Alt-spacebar, the system menu of the active window will appear. By keeping Alt depressed and using the cursor movement keys you can bring up other pull-down menus. Still holding Alt down, if you press PrtSc you can copy the contents of the visible menu (perhaps with some other information) to the clipboard. You can then edit out the parts you don't want and include the menu with the documentation associated with your application. Figure 1 FAR PASCAL FilterFunction( nCode, wParam, lParam ) int nCode; WORD wParam; LONG lParam; { if ( nCode == DO_SOMETHING ) { /*** DO YOUR PROCESSING HERE ***/ } else DefHookProc( nCode, wParam, lParam, &lpfnOldHook ); } Figure 2 Keyboard Hook Any keyboard event Journal Record Hook Any message retrieved from the event queue Journal Playback Hook Any message played back to the event queue Application Message Hook Messages to your application System Message Hook Messages to any application in the system Window Procedure Hook Messages to a window function (debug only) Get Message Hook Messages retrieved by GetMessage (debug only) Figure 3 BX Requested stack size CX Requested heap size DI Handle to application instance SI Handle to previous application instance ES Program segment prefix Figure 4: DI Handle to library instance DS Library data segment CX Requested heap size ES:SI Pointer to command line Figure 5 ; WINDOWS SCREEN CAPTURE - DYNAMIC LIBRARY ; ; LANGUAGE : Microsoft Assembler 5.1 ; SDK : Windows 2.03 SDK ; MODEL : small ; STATUS : operational ; ; Extrn PrtScInit:Near _TEXT SEGMENT BYTE PUBLIC 'CODE' ASSUME CS:_TEXT PUBLIC LibInit LibInit PROC FAR Push DI ; hInstance Push DS ; Data Segment Push CX ; Heap Size Push ES Push SI ; Command Line Call PrtScInit Ret LibInit ENDP _TEXT ENDS End LibInit Figure 6 #include <windows.h> #include <string.h> #include "prtsc.h" /* global data */ BOOL bMono; /* Convert to monochrome? */ WORD wArea; /* Current capture area */ WORD wColors; /* Number of system colors */ HWND hWndDOS; /* Handle to DOS session */ HANDLE hInstance; /* Library instance handle */ FARPROC lpfnDOSWnd; /* DOS session function */ FARPROC lpfnOldHook; /* Old hook function */ /* PrtScInit( hLibInst, wDataSegment, wHeapSize, lpszCmdLine ) : BOOL This function performs all the initialization necessary to use the screen capture dynamic-link library. It is assumed that no local heap is used; therefore there is no call to LocalInit. A nonzero value is returned if the initialization is successful. */ BOOL PASCAL PrtScInit( hLibInst, wDataSegment, wHeapSize, lpszCmdLine ) HANDLE hLibInst; WORD wDataSegment; WORD wHeapSize; LPSTR lpszCmdLine; { extern BOOL bMono; extern WORD wArea; extern WORD wColors; extern HWND hWndDOS; extern HANDLE hInstance; extern FARPROC lpfnDOSWnd; extern FARPROC lpfnNewHook; HDC hDC; /* Handle to temporary DC */ HWND hWndFocus; /* Window that has focus */ char szClassName[32]; /* Temporary class name */ /* Initialization - Alt = PrtSc active */ bMono = FALSE; wArea = CAPTURE_WINDOW; hInstance = hLibInst; lpfnOldHook = SetWindowsHook( WH_KEYBOARD,(FARPROC)PrtScHook ); hWndFocus = GetFocus(); hDC = GetDC( hWndFocus ); wColors = GetDeviceCaps( hDC, NUMCOLORS ); ReleaseDC( hWndFocus, hDC ); ■ ■ ■ } /* PrtScFilterFn( hWnd, wMessage, wParam, lParam ) : LONG FAR PASCAL This window function processes all the messages received by the MS-DOS session window. When the user selects the Screen Capture... menu option this function displays the screen capture control panel. All other messages are passed on to the window without modification. */ LONG FAR PASCAL PrtScFilterFn( hWnd, wMessage, wParam, lParam ) HWND hWnd; WORD wMessage; WORD wParam; LONG lParam; { /* Trap appropriate messages */ switch( wMessage ) { case WM_COMMAND : if ( wParam == CMD_CAPTURE ) { DialogBox( hInstance, "PrtSc", hWndDOS, PrtScDlgFn ); return( 0L ); } break; case WM_DESTROY : /* Window being destroyed - unhook everything */ if ( wArea ) { wArea = CAPTURE_OFF; UnhookWindowsHook( WH_KEYBOARD, (FARPROC)PrtScHook ); } SetWindowLong( hWndDOS, GWL_WNDPROC, (LONG)lpfnDOSWnd ); break; default : break; } /* Pass message on to window */ return(CallWindowProc(lpfnDOSWnd, hWndDOS,wMessage,wParam,lParam) ); } /* PrtScDlgFn( hDlg, wMessage, wParam, lParam ) : BOOL; This function processes all the messages that relate to the PrtSc dialog box. This function inserts or removes the keyboard hook function, depending on the user's selection. */ BOOL FAR PASCAL PrtScDlgFn( hDlg, wMessage, wParam, lParam ) HWND hDlg; WORD wMessage; WORD wParam; LONG lParam; { switch( wMessage ) { case WM_INITDIALOG : /* Initialize dialog box */ CheckDlgButton( hDlg, DLGSC_MONOCHROME, bMono ); EnableWindow( GetDlgItem(hDlg,DLGSC_MONOCHROME), (wColors > 2) ); CheckRadioButton( hDlg, DLGSC_OFF, DLGSC_SCREEN, DLGSC_OFF + wArea ); break; case WM_COMMAND : /* Window command */ /* Process submessage */ switch( wParam ) { case DLGSC_OFF : /* Turn screen capture off */ if ( wArea ) { wArea = CAPTURE_OFF; UnhookWindowsHook( WH_KEYBOARD, (FARPROC)PrtScHook ); EnableWindow( GetDlgItem(hDlg, DLGSC_MONOCHROME),FALSE ); } CheckMenuItem( GetMenu(hWndDOS), CMD_CAPTURE, MF_UNCHECKED ); break; case DLGSC_CLIENT : /* Capture client area of active window */ case DLGSC_WINDOW : /* Capture active window */ case DLGSC_SCREEN : /* Capture entire screen */ if ( !wArea ) { lpfnOldHook = SetWindowsHook(WH_KEYBOARD, (FARPROC)PrtScHook ); EnableWindow( GetDlgItem(hDlg, DLGSC_MONOCHROME), (wColors > 2) ); } wArea = wParam - DLGSC_OFF; CheckMenuItem( GetMenu(hWndDOS), CMD_CAPTURE, MF_CHECKED ); break; case DLGSC_MONOCHROME : /* Capture image in monochrome */ bMono = !bMono; break; case IDOK : EndDialog( hDlg, TRUE ); break; default : /* ignore everything else */ break; } break; default : /* message not processed */ return( FALSE ); break; } /* normal return */ return( TRUE ); } /* PrtScHook( nCode, wParam, lParam ) : WORD This function is called whenever the user presses any key. The Alt = PrtSc key combination is trapped, and a bitmap copy of the desired portion of the screen is copied to the clipboard. The return value is FALSE if the message should be processed by Windows; the return value is TRUE if the message should be discarded. */ WORD FAR PASCAL PrtScHook( nCode, wParam, lParam ) int nCode; WORD wParam; LONG lParam; { extern BOOL bMono; /* Convert to monochrome? */ extern WORD wArea; /* Area to capture */ extern FARPROC lpfnOldHook; /* Old keyboard hook */ WORD uWidth; /* Width of bitmap */ WORD uHeight; /* Height of bitmap */ WORD wDiscard; /* Return value */ POINT ptClient; /* Client point */ RECT rcWindow; /* Window rectangle */ HDC hScreenDC; /* Handle to screen DC */ HDC hMemoryDC; /* Handle to memory DC */ HWND hActiveWnd; /* Handle to active window */ HBITMAP hOldBitmap; /* Handle to old bitmap */ HBITMAP hMemoryBitmap; /* Handle to memory bitmap */ if (nCode == HC_ACTION) { /* This check traps the Alt = PrtSc key combination using bit 29 for the Alt key and bit 31 for the key being released. */ if ((wParam==VK_MULTIPLY) && ((lParam&0xA0000000) ==0xA0000000)) { ■ ■ ■ case CAPTURE_WINDOW : /* Retrieve active window dimensions */ GetWindowRect( hActiveWnd,&rcWindow ); break; case CAPTURE_SCREEN : /* Retrieve dimensions of entire screen */ rcWindow.top = 0; rcWindow.left = 0; rcWindow.right = SCREEN_WIDTH; rcWindow.bottom = SCREEN_HEIGHT; break; } /* Note that the window boundaries that need to be adjusted as part of the window may not be visible on the screen. This eliminates the problem of a partially defined bitmap. */ /* Adjust boundaries to screen clipping region */ if (rcWindow.right > SCREEN_WIDTH) rcWindow.right = SCREEN_WIDTH; if (rcWindow.bottom > SCREEN_HEIGHT) rcWindow.bottom = SCREEN_HEIGHT; if (rcWindow.left < 0) rcWindow.left = 0; if (rcWindow.top < 0) rcWindow.top = 0; /* Compute display size of window */ uWidth = rcWindow.right - rcWindow.left; uHeight = rcWindow.bottom - rcWindow.top; /* Open clipboard */ if ( OpenClipboard(hActiveWnd) ) { /* Empty clipboard */ EmptyClipboard(); /* Create screen DC & compatible memory DC's */ hScreenDC = CreateDC( "DISPLAY", NULL, NULL, NULL ); hMemoryDC = hScreenDC ? CreateCompatibleDC((hScreenDC):NULL; /* Create color or monochrome bitmap */ if (hMemoryDC && hScreenDC) { hMemoryBitmap = CreateCompatibleBitmap(bMono ? hMemoryDC :hScreenDC,uWidth, uHeight); } else hMemoryBitmap = NULL; if ( hMemoryBitmap ) { /* Select bitmap & copy bits */ hOldBitmap = SelectObject (hMemoryDC, hMemoryBitmap); BitBlt( /* Dest DC */ hMemoryDC, /* x upper left */ 0, /* y upper left */ 0, /* Dest width */ uWidth, /* dest height */ uHeight, /* Source DC */ hScreenDC, /* Source X upper left */ rcWindow.left, /* Source Y upper left */ rcWindow.top, /* ROP code */ SRCCOPY); SelectObject(hMemoryDC, hOldBitmap ); SetClipboardData(CF_BITMAP, hMemoryBitmap); } else MSGBOX( hWndDOS,"Insufficient memory!"); /* Close clipboard */ CloseClipboard(); /* Delete DCs */ if ( hMemoryDC ) DeleteDC( hMemoryDC ); if ( hScreenDC ) DeleteDC( hScreenDC ); } else MSGBOX( hWndDOS, "Unable to open clipboard!" ); } else MSGBOX( hWndDOS, "Active window is iconic!" ); } else wDiscard = FALSE; } else wDiscard = (WORD)DefHookProc( nCode, wParam, lParam, (FARPROCFAR *) &lpfnOldHook); /* Return value */ return( wDiscard ); } Figure 7 Ignore the message and block it Ignore the message but pass it on Handle the message and block it Handle the message and pass it on CHECKERS PART I [Design Goals for Building a Complete Graphical Application] Charles Petzold How about a nice game of checkers? Soon you will be able to play a game of checkers under OS/2 Presentation Manager. In the next few issues of MSJ we will present a complete checkers program for Presentation Manager (hereafter referred to as PM) called CHECKERS.EXE. You can play CHECKERS against yourself, the program, a person across a network, or an external dynamic-link library that implements a checkers-playing strategy. Writing a game program is an excellent way to learn about a graphical user interface, because games make use of graphics and are often highly interactive. You can use this article and the following ones to create a fun game that demonstrates many aspects of OS/2 and Presentation Manager programming, including graphics, keyboard and mouse input, menus, dialog boxes, and child windows. All too often the programs published in books and magazines are very short. They show programming techniques in isolation. These articles, on the other hand, will illustrate structure and organization associated with real-world coding, including the use of dynamic-link libraries (for the checkers-playing strategy), named pipes (for playing the game over a network), and multiple threads. We'll look at algorithms for working with logical structures that are common in games, and perhaps even some object-oriented techniques that will help generalize several components of the program. Because of its detail, CHECKERS will be longer than any other program this magazine has published. This article describes what I intend to put into the program and some of the problems I expect to encounter. Code with the components of the program that draws the checkerboard and playing pieces, as shown in Figure 1, will be published in the next issue. Using the additional user interface code from a future issue, you'll be able to play a game against yourself. Modes of Play CHECKERS will feature several basic modes of play available from a menu option. You can: ■ Play against yourself, by alternately playing the black and white pieces. ■ Play against the computer (the default). The logic of the checkers-playing strategy will be implemented in a dynamic-link library. ■ Play against an alternative dynamic-link library. If you would like to code your own checkers-playing strategy, you can create a dynamic-link library and hook into it from CHECKERS. ■ Play one dynamic-link library against another. In this case, you just sit back and watch the game. ■ Play against another person, running a copy of CHECKERS across a network. This facility will use named pipes. ■ Re-create a previous game. CHECKERS will allow you to store a log of a game (using standard checkers notation) as an ASCII file with the extension CKR. The program will also allow you to load a CHR file, to re-create the moves of the game at a speed you define. Program Structure Of course, all these options require that some serious consideration be given to the program structure. The best approach seems to be controlling games with a "supervisor" that I intend to implement as a Presentation Manager object window. (Object windows are not visible on the screen, but they can receive and send messages like other windows. You can use an object window to implement object-oriented programming techniques through Presentation Manager architecture rather than through the syntax of your programming language.) The supervisor will be responsible for maintaining the current board layout and keeping a log of the game. As is usual, menu commands will be processed by the program's client window. When you initiate a new game from the menu, the client window will inform the supervisor that a new game has been requested and who the players will be. If the game is to be played from a log file, the supervisor will be responsible for re-creating the game. Otherwise, each of the possible players (the user, a dynamic-link library, or a named pipe connection) is known to the supervisor as a window handle. Thus, for each game, the supervisor has two window handles, one for the black player and one for the white player. Alternating between black and white, the supervisor will use a message to notify a player window when it should make a move. Depending on who the players are, these player windows can get information about a move from the user, a dynamic-link library, or a named pipe. The player windows will inform the supervisor when a move has been completed. The supervisor can then notify the other player window about the move and request that that window make a move. The supervisor will also be responsible for determining when a player has won a game, and perhaps even for determining when a game has ended in a draw. The general program structure (as I conceive it now) is shown in Figure 2. The client window and the user window are normal Presentation Manager windows. The supervisor window, dynamic-link library access window, and named pipe window are object windows. Suppose that you're playing black and that your opponent is a dynamic-link library playing white. In this case, the supervisor window has two window handles: black is the handle of the user window and white is the handle of the dynamic-link library window. The supervisor sends the user window a message telling it to make a move. The user window displays an appropriate mouse pointer and waits for you to make a move. When the move is completed, the user window informs the supervisor window of the move. The supervisor then sends a message to the dynamic-link library window requesting a move. When the dynamic-link library determines what the move should be, it sends a message back to the supervisor window. The supervisor informs the user window of this move so that the display can be updated. The supervisor then sends a message to the user window asking for the next move. When one of the players is a dynamic-link library or another person over a named pipe, multiple threading will be required while the supervisor window is awaiting word of the next move. The dynamic-link library access window and the named pipe windows will be responsible for creating these threads. Notation/Representation I mentioned earlier that CHECKERS will be capable of storing a log of the game in an ASCII file. This log will use standard checkers notation, which is shown in Figure 3. The black squares are simply numbered 1 through 32. The numbering makes more sense if you turn the board around so that black is on top, as is usually done when showing checkerboard layouts in books. (Figure 3 is shown with black on the bottom to be consistent with Figure 1.) A game can be represented by showing each move with the starting and ending square numbers separated by a dash. Here's the beginning of a game with two jumps in the first four moves: Black: 10-15 White: 24-19 Black: 15-24 White: 28-19 There are only 32 squares on which pieces can reside, making it very convenient to represent the board in a C program. The number of black squares corresponds to the minimum number of bits in a long integer, as required by the ANSI C standard. By simply subtracting 1 from each square number, the board positions can correspond to the bits of a long integer, where 0 is the least significant bit and 31 is the most significant bit. Such a representation is discussed in Christopher S. Strachey's paper "Logical or Nonmathematical Programs" originally published in Proceedings of the Association for Computing Machinery Conference, Toronto (1952, pp. 46-49) and reprinted in Computer Games I, edited by David N.L. Levy (Springer-Verlag, 1988). Although Strachey's notation is pretty obvious once you start working with it, it's probably not something I would have stumbled upon myself. At any time during a game, the board can be represented using three 32-bit integers: white (W), black (B), and king (K). For example, at the beginning of a game, the three integers have the following values: B = 00000FFFH W = FFF00000H K = 00000000H There's a little redundancy in this notation because each square requires 3 bits (one in each of the integers) for a total of 8 different states. In reality, each square can be only one of 5 states (empty, black, white, black king, and white king), but the notation is so convenient that we can ignore the waste. You can apply bitwise operations to these integers to derive some other characteristics of the board. For example, the squares on which black kings currently reside can be represented in C notation as: B & K You can determine all the empty squares (E) using: E = ~B & ~W As we'll see, this type of logic is very important in the CHECKERS program, both in determining whether a user is making a legal move and in determining all possible moves a piece can make. For example, there are 9 positions on the board where an unkinged black piece can move from a square position n to a square position (n+3) without jumping an opponent's piece. You can represent these positions with a variable called M: M = 00E00E00E0H To calculate the current unkinged black pieces that might be able to move to squares with numbers that are 3 higher than their current position, use the following formula: B & M To calculate the destinations of these pieces you can simply shift left by 3 bits: (B & M) << 3 But it's only possible to make these moves if the destination square is empty: ((B & M) << 3) & E Shifting this expression to the right by 3 bits gives us a 32-bit integer that describes the position of all the current unkinged black pieces that can move to a square 3 higher in number: (((B & M) << 3) & E) >> 3 White kings can make these same moves: (((W & K & M) << 3) & E) >> 3 Of course, this gets more complex when you take into account the pieces that can move to squares that are 4 higher or 5 higher in number, and when considering the possible jumps. But the concepts remain the same. It is an extremely useful type of representation that avoids otherwise lengthy logic. Visuals and Interaction As you can see in Figure 1, CHECKERS will feature a checkerboard in its client window. The window that draws the board and receives keyboard and mouse input will be a child of the client window. The supervisor communicates with this child window and two others to request information about moves and to inform the window about moves made by the opponent. The board's colors may be changed using a menu option. The checkerboard looks three-dimensional and is sized to fit within the client window while maintaining the correct aspect ratio. To move the pieces, you can use either the mouse or the keyboard. Using the mouse you'll be able to pick up a piece from a square and move it to another square. The easiest way to do this would be to have predefined mouse pointers that look like the pieces. But the pieces will not generally be the same size as a mouse pointer, so PM's normal pointer logic can't be used. Instead, you'll move bitmaps around the window. CHECKERS will also have a keyboard interface. You can move the mouse pointer to a square using the cursor movement keys, pick up a piece by pressing the space bar, move the piece to another square, and set it down using the space bar again. CHECKERS will prevent a user from making an illegal move and require that the user make a jump when one is available and continue jumping until no more jumps are possible. This logic may also be implemented in a Presentation Manager object window. Playing Strategy Playing a game against the program is the feature many users will find most appealing about CHECKERS. This feature requires that the program include a reasonable checkers-playing strategy. This strategy will be as simple as possible, because PM programming is difficult enough in itself. The checkers-playing strategy will look ahead through all possible moves, counter-moves, counter-counter-moves, and so forth (to a level that I'll have to determine empirically), and determine the best move by calculating a simple score, where an unkinged piece is counted as 1 and a kinged piece is counted as a number somewhere between 1 and 2. My standards are very low: I want the CHECKERS program to beat me (or come to a draw) most of the time, and that should not be difficult at all! I claim no skill in playing checkers. I am not interested in developing the best possible checkers-playing strategy right now. However, CHECKERS will provide a framework for others who are interested in this aspect of the program. Because the checkers-playing strategy will be implemented in a dynamic-link library, readers of MSJ can develop their own checkers-playing modules that CHECKERS can easily access. The interface to this dynamic-link library will be documented in a future issue. The game will also be able to play two dynamic-link libraries against each other, so we could conceivably we can stage checkers tournaments where the humans would watch while the programs played. Now it's time for this programmer to stock up on necessary beverages, lock himself in his room, disconnect the phone, post a note on his door saying "E-mail only," and get to work. I'll emerge in time for the next issue to discuss the code that draws the checkerboard and the playing pieces. The Rules of the Game The game of checkers (called draughts in Great Britain) has obscure beginnings. It possibly originated in the 1500s as a merging of chess and the Spanish game Alquerque de doze, but it may have come from the Orient, related to Parcheesi and tic-tac-toe. Checkers is played on a chess board, which is an 8-by-8 grid of alternating black and white squares. The squares are not always black and white--the black squares may be any dark color and the white squares any light color--but the squares are referred to as black and white regardless of their actual colors. The official colors in US tournaments are green and buff. I decided to use green and light gray for the default square colors, as shown in Figure 1. The two players sit on opposite sides of the board. The board is oriented so that the edge of the board closest to each player has a black square on the left and a white square on the right. The game is played with 12 black pieces and 12 white pieces. (Again, these are the colors used to refer to the pieces. The actual colors may be different. Some commercial games of checkers use black and red pieces, but in tournament play the pieces are usually red and white.) One player controls the black pieces and the other controls the white pieces. At the beginning of the game, the pieces are arranged on the 12 black squares closest to each player, as shown in Figure 1. The players alternate turns, with black moving first. Initially, pieces can move forward (that is, towards the opponent's side of the board) and to a diagonally adjacent, unoccupied black square. If a diagonally adjacent black square is occupied by an opponent's piece, and the black square beyond that one is unoccupied, a player must move his or her piece to the unoccupied square. The opponent's piece is jumped (or captured) and removed from the board. The move must continue with additional jumps if they are available. Here's where the rules get controversial. Many players observe the tradition of "huffing." If a jump is available and a player does not take the jump, the opponent can require that the player take back the move, or that the piece that did not take the jump be forfeited and removed from the board. However, most contemporary rulebooks take a different approach: if a jump is available, a player must take it, and must continue jumping opponent's pieces until no longer able to do so. This is the rule I will impose in the Presentation Manager CHECKERS program. There will be no huffing. When a piece reaches the opponent's edge of the board, it is kinged or crowned. This is indicated by placing another piece of the same color on top of the piece. When a piece is first kinged, its move ends even if it can continue jumping. In subsequent moves, kings can move forward or backward along the diagonals. A player wins after capturing all the opponent's pieces or when the opponent can no longer move any piece. Many games between two good players end in draws. Usually a draw must be decided by a referee based on the inability of either player to gain any advantage after 40 or 50 moves. There are some variations of the game. Some versions of checkers allow kings to make long jumps, passing unoccupied black squares on a diagonal and landing on the square beyond an opponent's piece. Sometimes (particularly in Russia), the game is played on a 10-by-10 board. The PM CHECKERS program will play the standard game only. SpyGlass: A Utility for Fine Tuning the Pixels in a Graphics Application Kevin P. Welch One of the most time-consuming aspects of programming in the Microsoft Windows environment is getting things to look just right on the screen. Invariably (it seems) any user interface change creates alignment problems requiring several tweaks of one or two pixels before correct alignment is restored. Although such off-by-one errors are easy to see on a coarse, low-resolution display, they become a little more difficult when using a high-quality one with resolution in excess of 100 pixels per inch. SpyGlass is a BLOWUP.EXE inspired utility (see "BLOWUP: A Windows Utility for Viewing and Manipulating Bitmaps," MSJ Vol. 2 No. 3) that enables you to enlarge selected portions of your display dynamically while maintaining a constant pixel-to-aspect ratio. In addition, it serves as a simple demonstration of several subtle graphics device interface (GDI) programming techniques that might be of use in your own applications. And although SpyGlass won't eliminate off-by-one errors, it will make them a little easier to find when tuning your application. Using SpyGlass To use SpyGlass, click inside the window client area with the left mouse button. A small rectangle (in proportion to the SpyGlass window) will appear in place of the mouse cursor. Then, while the left button is depressed and you drag the mouse around, the rectangle acts like a cursor. It becomes a magnifying glass, dynamically magnifying whatever portion of the display it covers, thereby enlarging the images in the SpyGlass window. If you click the right button while dragging the mouse, the screen image enclosed by the magnifying glass will be enlarged inside the SpyGlass window. You can click the right button as many times as you like, taking pictures of various portions of the display (see Figure 1). If you hold down the right button while dragging the mouse, a continuous series of enlargements is produced inside the viewport. The enlargements may appear a little jerky on an 80286-based computer, but they look quite nice on an 80386 machine--especially one with a hardware graphics coprocessor. Releasing the left button when you finish dragging the mouse will copy the final image from the SpyGlass window to the clipboard. If you want to capture different sized images, you can change the magnifying glass proportions by adjusting the SpyGlass window dimensions or by selecting a different enlargement factor from the application's system pull-down menu. Coordinate Systems Each Windows application maintains its own coordinate system. SpyGlass is no exception. But unlike most other Windows applications, SpyGlass performs all its work in screen or display coordinates. The origin for most application coordinate systems is defined as the upper-left corner of the client area. Although this is normally adequate when you work within your own window, it is insufficient for SpyGlass since the magnifying glass can roam all over the display. Therefore, both the magnifying glass and the viewport area are defined in terms of screen or display coordinates (DC). This enables SpyGlass to use the screen display context or DC when performing each enlargement. As mentioned previously, the size of the magnifying glass is determined by the dimensions of the SpyGlass client area, adjusted by the currently selected magnification or enlargement factor. When the zoom factor is one, the interior portion of the magnifying glass is the same size as the SpyGlass client area. As you increase the enlargement ratio, the magnifying glass becomes proportionally smaller (see Figure 2). Although the current implementation of SpyGlass supports only a small number of enlargement ratios, you could easily change the source code to include a wider range. If you experiment with other values, be aware that SpyGlass will be most efficient when using powers of two for enlargement factors. This is because the underlying StretchBlt function operates best when doubling or quadrupling each pixel size instead of performing many fractional enlargements. If you get even more adventurous, you could experiment with values less than one. Understanding SpyGlass To build SpyGlass, you will need the Microsoft Windows Version 2.1 Software Development Toolkit (SDK) and a Microsoft C Compiler (Version 5.0 or later). Before constructing the program with the MAKE SPYGLASS command, you will need the files listed in Figure 3. (The files are available for downloading from any of the MSJ bulletin boards--Ed.) Structurally, SpyGlass is a relatively simple program, acting in most situations like a bitmap clipboard viewer. Internally, SpyGlass consists of only two functions--a main procedure that defines the SpyGlass window and retrieves or dispatches all application messages, and a function that processes all window-related messages. Throughout the source code, note the use of the macros defined in SPYGLASS.H . The WIDTH and HEIGHT macros (see Figure 4), for example, compute the corresponding width and height of a given rectangle, respectively. Both use a feature of the C compiler preprocessor called token pasting. To see how token pasting works, note that each instance of the variable x is preceded by a double number sign (##). When the ## is interpreted by the preprocessor, it allows tokens to be used as actual arguments that can be concatenated to form other tokens. Without this replacement capability, you would not be able to access the individual structure elements of a token within a macro. As is the case with most Windows applications, the SpyGlass window message processing function is the heart of the program. From an operational perspective, four major events are of interest; they are listed in Figure 5 and discussed below. SpyGlass is activated when the left mouse button is depressed inside the window client area. This causes a WM_LBUTTONDOWN message to be sent to the application. Since it is possible to receive extraneous mouse messages, a check is made to see if the window is in an inactive state. If it is, mouse capture is enabled via a SetCapture function call. This causes the next important event: notifying the window of all subsequent mouse movements via a series of WM_MOUSEMOVE messages. Mouse movements are captured by SpyglassWndFn. Since SpyGlassWndFn is by nature reentrant (or called before it returns), a Boolean flag is set to indicate the active window state. This allows the function to screen out unwanted messages, acting only on those of immediate interest. Besides capturing mouse movements, SpyGlassWndFn defines both the viewport and the magnifying glass rectangles in screen coordinates. The viewport region is calculated by retrieving the window client area (in client coordinates) and converting them to screen coordinates. The magnifying glass region is also based on the size of the window client area, but it is adjusted using the selected enlargement factor and by aligning the origin with the current mouse position. The resulting rectangle is then used to replace the normal mouse cursor. Note how the magnifying glass dimensions are enlarged by one pixel in each direction. This allows the program to work with the interior of the rectangle without erasing the inverted line around the border. The resulting visual effect is considerably smoother and eliminates unnecessary redrawing. When the magnifying glass rectangle is tied to the mouse and WM_MOUSEMOVE messages are received, the rectangle is hidden and then redrawn in the new mouse position. Before processing the message, the function checks whether the capture flag is set. This check allows it to distinguish the message from those mouse messages received when capture is inactive. Operationally, the mouse movement messages are handled in two different ways. If the magnifying glass intersects the SpyGlass window, it is redrawn after the viewport has been updated. This additional delay (associated with the overhead of transferring the screen image from the magnifying glass to the viewport) causes the magnifying glass to flicker and appear unresponsive. If the magnifying glass does not intersect the SpyGlass window, it is immediately repositioned, followed by the transfer of the screen image to the viewport (see Figure 6). An earlier version of SpyGlass did not attempt to take advantage of those two situations. Instead, it only redrew the magnifying glass after updating the viewport. But several users commented on the unresponsive magnifying glass, prompting this algorithmic change and resulting additional complexity to the program. Unfortunately, this seems to be the case with many other user-interface issues: in theory they are simple, but they actually require considerably more tuning than one might expect. One situation still ignored by SpyGlass is when the magnifying glass extends beyond the borders of the display. Currently the results produced are highly dependent on the characteristics of the active display driver. Some drivers automatically erase areas outside the screen, others leave them as they were, and still others leave them undefined. Typically, issues such as this only surface during the last few weeks of beta testing and sometimes they stay unresolved indefinitely. Another event of interest occurs when the right mouse button is depressed. This sends a WM_RBUTTONDOWN message to SpyGlassWndFn. If the magnifying glass is active, the selected portion of the screen is enlarged in the viewport. As with the WM_MOUSEMOVE message, the magnifying glass is hidden if it intersects with the SpyGlass client area. Note how the BitBlt function is used in place of StretchBlt to transfer the screen image when the enlargement factor is one. This is because the BitBlt function is inherently faster than StretchBlt, especially when implemented by the device driver at the hardware level. The final event of interest occurs when the left mouse button is released at the end of a drag operation. The resulting WM_LBUTTONUP message causes the mouse capture to be released, the magnifying glass to be erased, and the original cursor to be restored. In addition, the final viewport image is copied to a bitmap, which is subsequently transferred to the clipboard. One critical step of clipboard data management should be emphasized. This involves the handling of the memory bitmap containing the viewport image. Note how this bitmap is unselected from the memory display context before being placed on the clipboard. Bitmaps that are transferred to the clipboard while they are selected in a display context are usually inaccessible to members of the clipboard viewer chain, potentially causing some unusual data management problems. You should know that calling SetClipboardData causes the immediate transmission of a WM_DRAWCLIPBOARD message down the clipboard viewer chain. Since SpyGlass is a member of the viewer chain, the capture flag is not reset until SetClipboardData returns. This eliminates an extra update of the SpyGlass client area; an update that would be visible to the user. Variations Once you understand the inner workings of SpyGlass, you might want to try some experiments with the program. One interesting effect can be achieved when you set the enlargement factor to one and capture recursive images of SpyGlass itself. The resulting display is very much like pointing a high-quality video camera at its own output monitor (see Figure 7). If you are a little more adventurous, you can change SpyGlass, disabling some of the subtleties discussed earlier with a few well-placed comments. If you try this, it will be clear that the additional complexity does make a difference and transforms a good implementation into a great one. If you feel even more intrepid, you can try converting SpyGlass to run under OS/2 Presentation Manager (hereafter PM). In addition to the normal Windows to PM conversion tasks, you will have to pay attention to a few important details. The first of these details involves the way in which PM handles menus. When you convert SpyGlass you will have to retrieve the handle to the submenu in the zero position of the system menu explicitly before you can attach and manipulate the various enlargement commands. Second, instead of creating a new display context for the DISPLAY device, you will be able to retrieve a handle to the screen presentation space directly. And when working with this presentation space, you will be able to use a single GpiBitBlt function call in place of the Windows BitBlt and StretchBlt equivalents. Third, since PM doesn't support the clipboard viewer chain concept, you will have to remove all the code that relates to the clipboard viewer chain. OS/2 systems contain provisions for only one active clipboard viewer. Finally, when you try to place the final bitmap image on the clipboard, you will have to go through a complicated process of retrieving a memory display context (for further information, refer to the SNAP.EXE source code distributed with the Microsoft OS/2 Software Development Kit Version 1.1). The end result of your changes to SpyGlass will be a program that acts much the same in both Windows and PM. The same underlying design and tuning principles used with the Windows version apply equally well to PM. In addition, these enhancements can also be applied in your own applications. Although these subtle refinements are easy to perform with SpyGlass, performing such magic with a large application can be extremely time-consuming. But the results are well worth the extra effort. Figure 3: SPYGLASS Make file for application SPYGLASS.DEF Module definition file SPYGLASS.H Header file SPYGLASS.RC Application resource file SPYGLASS.C Source code SPYGLASS.ICO Icon referenced by resource file Figure 4 #define WIDTH(x) (##x.right - ##x.left) #define HEIGHT(x) (##x.bottom - ##x.top) Figure 5 Message Response WM_LBUTTONDOWN Mouse capture activated WM_RBUTTONDOWN Selected portion of screen enlarged WM_MOUSEMOVE Magnifying glass moved WM_LBUTTONUP Mouse capture deactivated