home *** CD-ROM | disk | FTP | other *** search
Text File | 2013-11-08 | 1.7 MB | 40,324 lines |
Text Truncated. Only the first 1MB is shown below. Download the file for the complete contents.
- 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 mous