PCjs Machines

Home of the original IBM PC emulator for browsers.

Logo

Microsoft Systems Journal (Vol. 4)

The following document is from the Microsoft Programmer’s Library 1.3 CD-ROM.

Microsoft Systems Journal Volume 4

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

Volume 4 - Number 1

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


Quotron Uses Windows to Develop New Market Analysis Tools for Real-Time Data

───────────────────────────────────────────────────────────────────────────
Also see the related article:
  Utilizing the Object Oriented-Nature of Windows
───────────────────────────────────────────────────────────────────────────
Tony Rizzo and Karen Strauss

Traders know that making or losing a million dollars often depends on only
one thing──information. This goes a long way in explaining that "Wall
Street data obsession"──the drive to be the first to get the latest market
information.

In 1851, Reuters delivered market information by carrier pigeon. On Wall
Street traders rushed stock quotes written on pads of paper to brokerages
via messengers called pad-shovers. In 1867 the ticker tape machine was
patented and the market underwent a revolution. Quotes became available on a
tick-by-tick basis; when a stock moved, a mark appeared on ticker tape
machines in every brokerage house on the Street.

The ticker tape evolved into the electronic wall ticker, which flashes a
continuous stream of quotes. Electronic data collection arrived on traders'
desks in 1960 in the guise of the Quotron(R) I. QUOTEVUE(R) followed in
1967, the first machine to provide quotes via a screen display.

Before long a brokerage's stature came to be measured by the presence or
absence of Quotron terminals on the desks of its brokers. These terminals
provided up-to-the-minute quotations──and functioned as a desktop
electronic ticker tape. Quotron terminals eventually became the status
quo.


Quotron 1000

In the financial world a firm's competitive edge depends on its ability to
gather data and, even more important, on its ability to quickly analyze and
act on it. The latest technology available for this task is the Quotron
1000(TM) (Q1000). The Q1000 is a UNIX(R)-based system of networked
minicomputers and software. Quotron provides a large package of financial
services and international networking capabilities, most of which are
based on the Q1000.

Quotron computer systems are located in regional control centers (RCCs) that
are found throughout the United States, Europe, Japan, and elsewhere.
Workstations consist of dumb terminals and personal computers
connected to the Quotron computers through various communications
networks (see Figure 1). The majority of workstations within the Quotron
system are dumb terminals, but personal computers are making major inroads,
slowly taking over the desktops of traders and brokers. Quotron offers a
package called the PC1000, which lets a PC act as a workstation on the
Quotron network.

As the amount of financial information available to the broker grows, it
becomes vital to provide better ways to manage data flow. Personal computers
are an ideal medium for doing this. First, PCs easily communicate with
networks such as that offered by Quotron. Second, real-time data can easily
be collected and off-loaded onto a PC. Once data is locally available,
users can run it through any analysis and charting package.

But even this level of sophistication cannot match the newest wave of
technology. The simple ability to download data onto a PC, even from a real-
time data feed, is no longer enough. Now brokers must analyze on a tick-by-
tick basis, using data from both the domestic and foreign markets at the
moment the data becomes available.

At the same time, the old character-based interfaces of dumb terminals and
PCs can no longer provide a useful window into this data stream. They are
giving way to exciting new graphical interfaces. A graphical interface
provides powerful new ways of viewing the constant streams of data that
confront users. Working at the leading edge of graphical technology,
Quotron has announced a new set of three PC-based applications designed to
function in the immediacy of such an environment.


Quotron Open Windows

These applications are called Quotron Open Windows, and they work under the
Microsoft(R) Windows operating environment. They were designed and built in
a joint effort with Inmark Development Corporation, a firm experienced in
developing financial applications.

QuotTerm(TM) is a terminal emulation program that duplicates the standard
Q1000 terminal. It provides the additional functionality of Windows.
QuotTerm uses a proprietary line to the Q1000.

QuotData(TM) is an application that allows users to collect and download
data from a standard Q1000 data feed. It provides a means of building a
user-defined database, and lets a user track up to 2500 securities on a
tick-by-tick basis. QuotData maintains the database while running in either
the foreground or the background.

Via Windows' Dynamic Data Exchange (DDE) protocol QuotData acts as a server
for any Windows application, and updates these applications on a real time,
tick-by-tick basis. QuotData also builds a historical database at market
close, made up of daily opening, high, low, and closing prices of each item
in the database. In addition, a historical database containing 10 years of
data is being added to the Quotron network. All of this will be available to
applications via DDE.

QuotChart(TM) is a real-time charting and analysis tool. QuotChart gets its
data from QuotData and provides an extensive set of sophisticated charts
that reflect tick-by-tick changes in the underlying financial data.

Quotron Open Windows requires either an 80286 or 80386 processor, at least
2Mb of RAM, a hard disk drive, one serial port, an EGA, VGA, or higher
resolution graphics adapter that supports Microsoft Windows, a bus mouse,
and a Q1000 port.


Speaking of Windows...

Quotron Open Windows was created by Clifford P. Ribaudo, Inmark's President,
and Mark Anders, its Executive Vice President. In 1985 they built and
marketed Market Maker, a financial analysis and charting tool. Market Maker
is character-based and served as the model for development of the new
Quotron products. "A lot of ideas for Quotron Open Windows came from this
one. We learned about what to do and what not to do," says Ribaudo.

After completing Market Maker, Ribaudo and Anders began searching for a new
programming environment. First, they wanted multitasking support on DOS-
based machines. Second, they wanted better graphical support for their
charting tools. "We explored TopView(R), DesqView, GSS(R), CGI; but none of
these really suited our needs," says Anders. The environments that supplied
multitasking were all character-based. The graphics packages that were
available in 1985 were built on standards such as CGI and GSS and were
generally not very robust.

Then, according to Anders, "We booted up Windows, which we'd gotten through
a $25 offer that came with a Quadram EGA card we had purchased. We found
that we were looking at a unique environment that really seemed to meet our
needs." A quick call to Microsoft for more details led to the discovery of
the Windows Software Developer's Toolkit (SDK).

"Windows had a graphical user interface that was part of a real multitasking
system," says Anders. "There were pull-down menus, support for a mouse, and
multitasking." Says Ribaudo, "It seemed to us an excellent model for
creating interactive applications.

"One of the best things about Windows was that suddenly we found ourselves
within an environment that provided many of the tools we needed──dialog and
list boxes, check boxes and radio buttons──already implemented as part of
the environment. When we developed Market Maker, the character-based
interface meant that we ourselves had to carefully calculate and hard
code the locations of boxes, menus, and so on. Suddenly we no longer had to
pinpoint and track the location of every object in our application."

Adds Anders, "One major advantage of Windows is that it really forced us to
take a careful look at our user interface. Windows really helped us to
organize and maintain a consistent interface. After some extensive
testing, Windows clearly emerged as the programming platform of choice."


Windows 2.x

Both Ribaudo and Anders were glad to see Windows 2.x released. In addition
to overlapping windows and a cleaner user interface, it had improved
documentation. This aided their learning process. However, says Anders,
there is no escaping the fact that "there is a steep learning curve. It was
a struggle. I had no experience on other windowing and event-driven
environments to make it easier. It was the first time I had seen anything
like it. Now, however, it feels as if I was always comfortable with it. At
the time it seemed like I would spend weeks at a time stuck on one thing."

"Control flow and message passing was the hardest to sort out. Windows has a
real personality of its own," adds Ribaudo. The two note, however, that
the difficulty of learning Windows is often overstated and that it didn't
take them that long to get the hang of it.

Once they decided to build the applications under Windows, they set about
adapting their Market Maker technology. This eventually led to Quotron
Open Windows.


QuotTerm

The Quotron dumb terminal and PC1000 software do a great job of providing
analysts with the basic data needed for even the most complex analysis. The
first challenge Inmark and Quotron faced was to come up with a good reason
why a company should replace the dumb terminal or PC running the PC1000
software with a Windows product.

Quotron and Inmark felt they had to fully emulate the standard Quotron
screen of both the PC1000 software and the dumb terminal. At first glance it
seems a step back to want to turn the PC into a dumb terminal. But the key
issue was to give the analyst a safety cushion. Complete emulation of the
older environment would let any person familiar with the standard Quotron
terminal quickly use a PC equipped with QuotTerm. "The dumb terminal had to
be supported or we wouldn't be able to move people off of it onto a PC,"
says Ribaudo. "That, and the availability of charting and analysis tools,
and a personalized historical database, looked to be enough to convince
any broker to switch."

QuotTerm therefore provides this total emulation, down to the same screen
locations and keystrokes for each service (see Figures 2 and 3☼). The
QuotTerm application links directly to a Q1000 computer, and operates
independently of the other available tools. But since QuotTerm works under
Windows there is much more functionality. The screen colors are all
modifiable, there is complete device-independent printer support, there is
the ability to selectively modify information-field blink rates, as well as
the ability to move and resize the QuotTerm window. With the mouse the user
can click on symbols displayed on the screen in order to retrieve full
quotes, headlines, and stories. And a user can run other Windows
applications while QuotTerm is active.


QuotData

QuotData communicates directly with the Q1000. It monitors real-time data
and updates historical files. QuotData talks with other applications via a
proprietary link or DDE. Basically a communications/database front end
for the Q1000 system, it can be considered the hub around which all other
applications work. QuotData connects to the Q1000 system and requests
real-time updates on a symbol-by-symbol basis using the Quotron micro-sdf
(selective data feed) capability. Micro-sdf provides a proprietary
protocol for the transmission of real-time price updates from the Quotron
network through asynchronous lines to other computers. This protocol is
implemented by two processes, called microread and microwrite, which run on
the Quotron 1000.

Microread blocks reading from the serial port that is awaiting requests
from the PC. When the port gets a request, the system forwards it to the
selective data feed driver, via a UNIX message queue. Seldf then requests
price updates from the network and transmits each message to a microwrite
over another message queue. Microwrite puts the data in packets and
transmits it to the PC.

Windows dialog boxes make it simple for the user to make selections,
add/delete symbols, set the history parameters, and establish communications
links. Users can define a symbol list and indicate if the symbol has
associated volume and if it trades on any exchange with after-market hours.
The history function lets users specify the amount, in days, of historical
data they wish to store and save on disk.

QuotData provides data to other applications running under Windows via two
types of interapplication transfers. The first is a proprietary data channel
that Inmark uses to talk to other Quotron/Inmark applications such as
QuotChart. This channel is basically a shared memory queue that the server
writes to and that any Quotron/Inmark application needing the data can read.
Available memory is the only limitation to the number of these conversations
that can take place at once.

Second, and perhaps even more important, QuotData fully supports the
standard Windows DDE protocol. QuotData can carry on multiple DDE sessions
with any Windows application that supports DDE and wants to be "ADVISED"
with real-time market data. Ribaudo and Anders will build full DDE support
into all of their future products. Says Ribaudo, "What is important, and
what has excited developers who have seen QuotData, is that because of DDE
they will be able to develop Windows-based applications to work directly
with QuotData and need never worry about external connections to the Q1000
itself." Refer to Figure 4 for a high-level overview of the data links
supported.

Communication through DDE is relatively simple. QuotData will support Text,
Comma Separated Values (CSV), and BIFF format. It will return four different
types of information, as shown in Figure 5. The information types shown in
Figure 6 will be supported in the near future.

Microsoft Excel is a good example of a program that was built to work with
DDE. In Microsoft Excel, the user builds a spreadsheet that will create
whatever report he/she may need. Then it links to QuotData through DDE and
gets either historical data or tick-by-tick updates as required.

The DDE data that an application needs to know in order to talk with
QuotData is shown in Figure 7. In the spreadsheet, every cell needing data
would contain a statement such as that shown in Figure 8A, where =INDEX is
the Microsoft Excel function needed to establish the link with QuotData, and
row# is a value ranging from 1 to 4, indicating what type of data it needs
(see Figure 5). Column# is always 1. For instance, if the line shown in
Figure 8B was defined in a spreadsheet cell, it would return the last sale
price of the security represented by the symbol AAPL (see Figure 9☼).


QuotChart

QuotChart offers an extensive set of sophisticated charting and technical
analysis tools. These tools operate on the real-time data supplied by
QuotData via the proprietary channel mentioned above.

QuotChart makes available all the major technical analysis studies with a
wide variety of charts (see Figure 10). Additional studies can be performed
and overlays created using tools such as Gann angles, cycle rule, Fibonacci
rule, moving averages, and trend lines. A tool called the vertical wand
lets the analyst select different "vertical periods" (sections) of a chart.
Once the analyst outlines a section (see Figure 11☼), he/she can do things
such as zoom in on that section.

Aside from its graphics capabilities, one of the things that sets Windows
apart from the other environments Ribaudo and Anders considered is the
object-oriented nature of the Windows message-based architecture. As
Anders puts it, "Under Windows what you really have are a collection of
objects that communicate with each other through an established messaging
system-messages are relayed from object to object. This is a really useful
concept and supplies us with an elegant solution to some difficult design
problems."

The real problem lies in being able to handle a constant, and constantly
changing, stream of data quickly and efficiently. Further, there is the need
to apply different types of analysis to this changing data. If a user wants
a moving average on one chart, and a stochastic on another, the system must
be able to deal not only with the data but with the underlying
mathematical formulas and relationships as well. As Ribaudo says, "We
need to be able to provide a means to extend and modify the analytics on the
fly."

By using a special object-oriented language that they themselves developed,
and which works under Windows, Ribaudo and Anders solved this problem. Their
language consists of mathematical objects, which process streams of market
data that arrive as messages and are then quickly "relayed from object to
object."

The processing structures in QuotChart are specified so that they look like
a combination of a macro language and standard mathematical equations.
Equation strings are specified at run time and built "below the surface,"
in response to selections that the user enters into dialog boxes. This
facilitates the rapid development of new analytic features. Says Ribaudo,
"To create a new type of analysis we need only create the new `object
primitives' in C code and add them to the `instruction set.' The primitives
are then combined in the language to create the new market study." For a
detailed technical description of this language, see the accompanying
sidebar, "Utilizing the Object-Oriented Nature of Windows."


Pages and Templates

QuotChart also provides two other very useful features: pages and templates.
These permit saving of frequently used chart configurations. Pages let users
predefine and store every last detail of a chart or series of charts,
including all relevant data. The charts on a page all use the same "time
axis." A template defines the structure of a page and doesn't contain
specific references to data, although symbols can be saved if desired. A
template brings up a chart or collection of charts that perhaps define some
standard analysis that's applicable to different portfolios. It can be
saved, assigned to function keys, and called up at any time and used with
any portfolio.

With pages, the analyst simply calls up a page and the predefined charts
pop up, are linked to the server and updated to reflect the most current
market information. Up to four charts can be saved per page, and two pages
(up to eight charts) can be displayed on the screen at the same time. An
enormous amount of information can be viewed simultaneously. Figures 12
and 13☼ show the page abilities of QuotChart.

According to Ribaudo, the Windows environment makes all of this charting
activity relatively simple. Windows itself takes care of all the resizing
issues. "If a chart's size changes, Windows handles most of the display
details, so that creating tools such as a zoom feature for particular areas
of a given chart is quite simple. Windows makes the difference." Compare the
relatively static charts of Market Maker shown in Figure 14☼ with the
charting capabilities of QuotChart, shown in Figure 15☼.


What's Next?

Both Anders and Ribaudo are committed to the Windows 2.x environment. Says
Anders, "You don't spend two and a half years coding a project under a
graphical user interface if you don't like the interface or the coding
methods." After working on Quotron Open Windows, a major new product by any
measure, they are both glad to finally see a "real" product.

It is clear to Anders and Ribaudo that graphical interfaces are the key to
the next generation of software. When asked what their future directions
might be, their answer was quick and to the point. "As soon as this gets out
the door and the planned enhancements are in place, we are going to begin
looking at the port to OS/2 and Presentation Manager.


Figure 1:  An overview of the Quotron Network

                            ┌───────────┐
                    ┌─X.25──┤ Additonal │
     ▓▓▒▒░░░░▒▒▓▓   │       │ Databases │
     ▓▓Uni-Link▓▓───┘       └───────────┘
     ▓▓Network ▓▓───┐
     ▓▓▒▒░░░░▒▒▓▓   │ Other   ┌───────────┐
          │         └Protocols┤ Other     │
   Quotron's Paged            │ Databases │
       Format                 └───────────┘
    ┌─────┴─────┐
    │ Quotron's │Request for historical data
    │   CARS    │packed into the page format for
    │   Host    │transmission to CARS by Q1000
    └─────┬─────┘
          │                ▓▓▓▒▒░░░▒▒▓▓▓
          └────────────────▓▓▓▒▒░░░▒▒▓▓▓
                           ▓▓Quotron's▓▓
                           ▓▓Network  ▓▓
   Paged response          ▓▓▓▒▒░░░▒▒▓▓▓
   unpacked by Q1000       ▓▓▓▒▒░░░▒▒▓▓▓
    ┌────────┐              │    │    │
    │ Remote │        ┌─────┘    │    └───────────────────────┐
    │ Users  │        │          └────────────────┐           │
    └──────┬─┘    ┌───┴───┐                   ┌───┴───┐   ┌───┴───┐
           │      │       │ ┌─────────────┐   │       │   │       │
           └──────┤ Q1000 ├─┤ Peripherals │   │ Q1000 │   │ Q1000 │
                  │       │ └─────────────┘   │       │   │       │
                  └──┬─┬──┘                   └┬──┬──┬┘   └───┬───┘
             ┌───────┘ │             ┌─────────┘  │  └──┐     └────────┐
             │         │             │        ┌───┘     │              │
        ┌────┴───┐┌────┴───┐    ┌────┴───┐┌───┴────┐┌───┴────┐    ┌────┴───┐
        │   PC   ││   PC   │    │   PC   ││  SUN   ││  SUN   │    │   PC   │
        │        ││        │    │        ││        ││        │    │        │
        └────────┘└────────┘    └────────┘└────────┘└────────┘    └────────┘
         A request for historical data generated by an application running
         on then PC, SUN(R) or Q1000 uses a standardized request format


Figure 4:  A high-level overview of current Quotron Open Windows Data Links.

┌──────────┐          ┌──────────────────────────────────┐
│ Regional │          │   UNIX-based Q1000               │
│ Control  │◄────────►│  ┌────────────────┐  ┌────────┐  │  ┌───────────┐
│ Center   │          │  │Logical Database├─►│Terminal├────►│ Quotron   │
│ (RCC)    │          │  └───────────┬───┘  │        │  │  │ Terminals │
└──────────┘          │  ┌──┴───┐  ┌──▼───┐  │Support ├────┐└───────────┘
                      │  │Micro-│  │Micro-│  └────────┘  │ │
                      │  │ read │  │write │              │ │    ┌───────┐
                      │  └─────┘  └──┬───┘              │ │    │  PC   │
                      │     │         │                  │ │    │running│
                      │     └──█████◄─┘                  │ │    │Windows│
                      └────────▀▀▀▀─────────────────────┘ │    │2.x or │
         ┌───────────────────────│─────────────────────────│────┤higher │
         │          ┌────────────┘                         │    └──────┬┘
         │ ┌────────▼────────┐                     ┌───────▼─────────┐ │
         │ │■──────────────↓↑│                     │■──────────────↓↑│ │
         │ │                 │                     │                 │ │
         │ │    QuotData     │                     │    QuotTerm     │ │
         │ │                 │                     │                 │ │
Via a    │ └──────────┬─┬───┘                     └─────────────────┘ │
Proprietary         │  │ └────────────DDE─────────────┐                │
Data Link───────────┤  └────DDE───────┐               │                │
(DDE will           │                 │      ┌────────▼────────┐       │
be supported        │         ┌───────▼─────────┐────────────↓↑│       │
in the next    ┌────▼────────────┐────────────↓↑│ Other        │       │
release) │     │■──────────────↓↑│ Microsoft    │ Windows      │       │
         │     │                 │   Excel      │ Applications │       │
         │     │   QuotChart     │              │──────────────┘       │
         │     │                 │──────────────┘                      │
         │     └─────────────────┘                                     │
         └─────────────────────────────────────────────────────────────┘


Figure 5:  QuotData returns information to Microsoft Excel.

Last Price
Net Change
Volume of Last Trade
Total Volume for the Day


Figure 6:  New QuotData information that will become available through DDE.

Bid Price
Bid Size
Ask Price
Ask Size
Year High
Year Low
Open Interest
Previous Close
Previous Bid
Dividend
Yield
Earnings
PE Price/Earnings
X Dividend
Fiscal Year End


Figure 7:  Microsoft Excel uses this information to communicate with
           QuotData via DDE.

Application Name    QUOTDATA
Topic Name          TICKDATA
Item Name           Security Symbol (e.g. MSFT)


Figure 8A:  Microsoft Excel command structure for obtaining QuotData
            information via DDE.

=INDEX(QUOTDATA|TICKDATA!Symbol,column#,row#)


Figure 8B: Sample Microsoft Excel DDE command.

=INDEX(QUOTDATA|TICKDATA!AAPL,1,1)


Figure 10:  Charting and analysis features of QuotChart

volume histogram
departure
ratio
spread
moving average and convergence/divergence (MACD)
fast and slow stochastics
high, low, close bar charts


───────────────────────────────────────────────────────────────────────────
Utilizing the Object-Oriented Nature of Windows
───────────────────────────────────────────────────────────────────────────

Mark Anders and Clifford P. Ribaudo

When we set out to create QuotChart, we had a simple objective; create a
graphical, technical analysis package that could handle real-time tick-by-
tick data. Technical analysis involves applying various mathematical
formulas, or studies, to financial price data. Some simple studies include
the calculation of moving averages or the range of a data element over a
given period of time. Often these studies either cause a smoothing of data
or amplify some rate of change, so that trends in the price can be detected.
By real time we mean that a recalc is performed each and every time a trade
occurs, in much the same way that a spreadsheet changes when you modify one
cell. QuotChart differs from many other real-time charting packages in that
it does a recalc on every price change, or tick. With most other packages,
if you are looking at a chart where each x-axis unit is ten minutes, a
recalc on some or all of the studies will only be done after that ten minute
period is finished.

While writing Market Maker, we learned that some of the most important work
done during the creation of an application is in the planning stage, before
the actual coding begins. One of our objectives was to make the application
as modular as possible. Aside from being good programing practice, since we
hadn't chosen an operating environment at the time that we began designing
and writing QuotChart, it was important that the user interface layer be
separate from the areas responsible for performing calculations and plotting
the data on the display. In general our design decisions had to be made not
only with a view towards what we planned to release as Version 1.0, but also
attempting to anticipate where we wanted to take the product in the future.

Because of the overriding need for flexibility, it was decided that the core
of the program would be an object-oriented language tailored to process the
incoming data stream, and that the user interface layer would generate
formulas written in this language. The object-oriented nature was necessary
so that each instance of a function call could remember the state of its
calculations for faster recalcs.

There were disadvantages to this approach. First, there was overhead
involved because a formula had to be created, parsed, and code of some type
generated. In addition, a compiler had to be written for the language.

The advantages, however, were numerous. Isolating the user interface from
the core code meant that as long as the syntax of the language remained the
same, you could change the actual implementation of language without having
to modify the user interface layer. The language can be viewed as a
communications mechanism between the user interface and the way the program
internally organizes its data. Because of this defined interface between the
two layers, the details of how the core code works is completely invisible
to the user interface. Since the interface has no knowledge of any internal
data structures, which of course could change, it is less likely to be
broken when modifying the core, or damage any other areas of the program
when it is modified. Portability is also increased. Since the core is only
passed strings to compile and execute, it doesn't know or care what the user
interface is like. Only those objects having to deal directly with the
outside environment, for example the object that actually draws a line,
would require modification to a run in a new environment.

Each object in the language represents a particular operation on a data
stream. Once the piece of code is written which implements that operation,
you can reuse it in a variety of settings. For example, perhaps the most
frequently used object in the system is the object called Line. As its name
implies, it draws a line on the display (or printer). It takes three
arguments, the first being the data stream from which it receives its data.
The remaining two, which are optional, are names for the line; the first
used for display to the user, the second being a unique object name that
other objects can use to reference Line's data. The formula used to create a
line chart of the last or closing price of Microsoft (MSFT) would be:

  Line(Close("MSFT"),"Last Price of Microsoft", "l1")

This formula computes and draws the last price for Microsoft. If the system
was to show data from that line, or allow the user to use the values
contained in that line as input for another study, it could refer to it by
the name Last Price of Microsoft.

Another commonly used object is called SMA (for Simple Moving Average) which
computes the sum of the last n prices and divides that by n. Often a
technical analyst will overlay moving averages on a chart. If, for example,
you wanted to overlay a line of the 7-unit simple moving average of the last
price of Microsoft on the above chart you would add the following formula to
that chart:

  Line(SMA(Close("MSFT"),7), "7 unit Simple Moving Average","l2")

This could be considered a direct application of the moving average object.
Moving averages are also used as elements of studies. For example, Moving
Average Convergence Divergence (MACD) takes the difference between two
moving averages of different lengths to create one line, called the MACD
line, and feeds this result into another moving average to create a second
line, called the signal. MACD uses a different type of moving average called
an Exponentially Weighted Moving Average (EMA). The formula for this is:

  Line (EMA(Line((EMA(Close("MSFT"),12)-EMA(Close("MSFT"),26)),"MACD","11"),
        9), "Signal","l2")

This example serves to illustrate a few points concerning the flexibility
gained by the implementation of the language. First, the objects created can
be used in a number of different contexts. For instance, notice that the
line called MACD takes its input from EMA and returns its output (the Line
object returns its input unchanged) to be used as input for another EMA.
Another benefit is not as obvious. Because EMA and SMA are both moving
averages, much of the code used to implement how each behaves is shared, so
that EMA and SMA are subclasses of the moving average class of objects. SMA
is also a subclass of the sum object, which computes the sum of the last n
data elements. There is an object called Hist which is a relative of Line
but produces histogram type output. The code used for the actual drawing is
the only code which they do not share.

The fact that many objects share code means writing less code, and when you
need to add new functionality to a class of objects, you only have to do it
in the superclass. For instance (no pun intended!), we needed to be able to
display the value of all lines at a certain x value, which of course was
easily done by writing the corresponding code in the superclass of Line and
Hist.

There was another persuasive reason for having the language at the core.
While a user interface that allows the user to select from options, such as
the period for a moving average, is powerful, eventually some users are
going to want to program their own studies. If this capability was not
planned for, it would require substantially more work to implement than if
it existed from the beginning. In this case, what would really be provided
is a free-form user interface to the same language and not an afterthought.

One of the interesting things about the creation of QuotChart is that most
of the design and implementation of the language was done before Microsoft
Windows was selected as the operating environment. We were very lucky that
Windows was object-oriented and both lent itself to and encouraged modular
design. The ability to easily create menus and have tools such as the dialog
editor meant that adding new features was often just a matter of drawing a
new dialog box and providing it with the ability to generate a new formula
string. It can be tricky programming under Windows, but it has really helped
us maintain our goals of modular design without having to invent our own
object-oriented user interface.


Porting Apple Macintosh Applications to the Microsoft Windows Environment

Andrew Schulman and Ray Valdes☼

You're happy with your Macintosh program. It cooperates with other programs
under MultiFinder(TM), it runs like the wind in color on the Mac(R) II, and
even your customers with Mac 512Ks are fairly happy with the program. Your
development goes smoothly using the Lightspeed(R) C compiler and TMON
debugger and, though you wish Lightspeed's editor weren't so simplistic,
you've put together some dynamite EMACS macros using Quickeys(TM). You wish
you had C++, but Apple is coming out with MPW(TM) C++ soon. And you've got
to love that Mac II.

Why then would you want to port your program to the PC, Windows, and the
OS/2 Presentation Manager (hereafter referred to as PM)?


A Wider World

Windows has become a de facto standard for graphical windowed software in
the IBM world, as evidenced by its incorporation into the IBM(R) OS/2
Presentation Manager.

Windows can be your entrée to the larger world of the Graphical User
Interface (GUI) standard. Not only is Windows on the path to OS/2
Presentation Manager, and from there to the promise of SAA, but the Windows
programming model is much closer to that of X Windows than is the Mac's.

There's another sense in which porting to Windows will widen your
perspective. Not only is the PC software market significantly larger than
the Mac market, but there is a much wider range of hardware. Right now, Mac
software only runs on screens with square pixels. Windows is fairly device-
independent so that, if you port to Windows, your software will run even
on screens and printers with pixel ratios like 12:5, 1.43:1, and 1.37:1. You
may think such screen ratios odd, and we would agree with you, but these
ghastly monitors represent the majority of the real world.


Working with Windows

If you're a Macintosh programmer who needs to learn how to program for
Windows, there is some good news and some bad news.

The good news is that everything you know is not wrong. You already know
the central ideas behind Windows programming: event-driven input; text is
just another form of graphics output; relocatable memory; and resources.

Ironically, a Mac programmer who has never sat down at an IBM PC is in a
better position to learn Windows than a seasoned DOS programmer who has not
yet gone through the infamous "everything you know is wrong" learning curve.
If you know Mac programming, then under Windows much of what you know is
still true.

A Mac programmer has already learned that application code is inert and
passive, and simply waits to be activated by a data structure (called an
EventRecord on the Mac and an MSG under Windows). A C programmer on the Mac
has probably said good-bye to old friends like printf and gets and, sure
enough, you won't use these for Windows programs either. Relocatable memory
blocks? Mac programmers handle these routinely, so the fact that Windows
moves memory around is not an issue. Your Mac knowledge can be leveraged
in programming for a market with millions of IBM and IBM-compatible
computers.

After you've worked with the application program interfaces (API) of several
graphical windowed environments, you begin to realize how similar they are.
There are many strong resemblances between the Macintosh Toolbox and the
Windows API, even some one-to-one correspondences. Take the function
PtInRect: how could determining whether a given point falls within a given
rectangle not be the same in all systems?


Everything Is Different

There are many other formal analogies. For example, the Macintosh memory
manager provides a function, NewHandle, which returns a handle to a block
of relocatable memory, and the Windows kernel provides GlobalAlloc which,
when called with the GMEM_MOVEABLE flag, also returns a HANDLE to a block of
relocatable memory.

A Windows HANDLE sounds just like a Macintosh Handle. There are many such
seeming similarities between the Mac and Windows. Believe it or not, that is
the bad news. While the Mac, Windows, and Presentation Manager are in some
formal sense identical and after a while feel all the same, it's the little
subtle differences that matter when you're porting code. Although they are
conceptually identical, when you get to the details, a Windows HANDLE is
almost nothing like a Macintosh Handle.

Here are further illustrations of the same point: Windows regions are not at
all like regions on the Mac; the Windows function FindWindow has nothing
to do with the Mac function FindWindow; the Windows equivalent for the Mac
function FrameRect is the function Rectangle and not the function
FrameRect.

In this article, we will first port a tiny fragment of Mac graphics code to
Windows, illustrating the similarities and differences between Mac's
QuickDraw(TM) and Windows' graphical device interface (GDI), and we will
show one approach to portable code that takes advantage of the
similarities, while isolating you from the differences. We then present a
complete application that shows a unified approach to Macintosh and Windows
events, windows, and memory management. A PC port is not only desirable, but
possible, as portable code is both desirable and possible.


The Update Event

Kernighan and Ritchie in The C Programming Language, (Prentice Hall, 1988),
2nd edition, p. 5 say, "The only way to learn a new programming language is
by writing programs in it," but probably the only way to learn a new
windowed graphical environment's API is by writing parts of programs in it.
Even "Hello World" in such an environment typically involves more code
than a newcomer can be expected to type, especially on a new machine using
an unfamiliar editor.

One of the more bizarre (until you get used to it) features of GUI
environments actually comes to our rescue in the form of redrawing old
output. Whether on the Mac or Windows, your program must be prepared to
reproduce its output at any time. The reason for this is that the window to
which you send your output (text or graphics) may get damaged.

When this happens, the Mac sends your application an updateEvt, and Windows
sends the damaged window a WM_PAINT message. On the Mac one responds to an
updateEvt by bracketing QuickDraw calls between a BeginUpdate and an
EndUpdate and, similarly, in Windows the response to a WM_PAINT message is
to sandwich GDI calls between a BeginPaint and an EndPaint. So you can see
that programming for Windows won't be that unfamiliar.

Since your application must be prepared to meet an update event by redrawing
the window's contents, an application can confine all of its drawing to
this one place.

Let's begin by writing some code that will get called whenever our window
receives a WM_PAINT message. What we propose to do is take the HELLO
"application" (it's actually more of a skeleton) from the Windows Software
Development Kit (SDK), remove the call to the function HelloPaint, replace
it with some Mac code, and alter the Mac code into something that will
compile and run on the PC. We will start with as little alteration as
possible, then gradually work up to full Windows code, writing a real
version that runs under both systems.

The Mac code found in the function StopSign illustrates QuickDraw's MoveTo,
Move, and Line functions, and is loosely borrowed from Stephen Chernicoff's
superb book Macintosh Revealed (Hayden Books, 1987), 2nd edition, Vol. I,
pp. 189-191. The code appears in Figure 1, along with bits of surrounding
context, so you can see how the function StopSign would get called on the
Mac. The function scales the stop sign to the size of the window into which
it's being drawn. If you resize the window, you not only generate a
mouseDown inGrow; you also generate an updateEvt that causes the stop sign
to be rescaled and redrawn to the new window dimensions.


The Hard Part

A Mac programmer who wants to put this Mac code into a Windows program faces
one hurdle that has nothing to do with the code itself: you have to set up
the SDK and the C compiler on your PC/AT(R) or 386 machine, you need a text
editor, and you'll soon want a set of tools like grep and diff.

Note that Windows/386 lets you perform tasks from within the graphical
windowed environment (see Figure 2☼), whereas Windows/286 makes you edit
and compile in character mode, just dropping into Windows long enough to
test the resulting program. Integrated programming environments such as
QuickC(R) compiler and Turbo C(R) unfortunately will not generate code for
Windows.

If you're used to the "double-click and you're done" simplicity of
installing software on the Mac, then installing tools like C 5.1 and the
Windows SDK on your 286 or 386 is going to be more difficult. All we can
tell you is, once you can compile HELLO.EXE from the SDK, then writing code
is going to seem easy.


Take One

Figure 3 shows our first take at a Windows equivalent. While we could get
the exact same Mac code to run on a PC by emulating a GrafPort and a set of
functions to manipulate it, we've already worked on such a project and the
resulting performance was unacceptably slow. Instead we're using "native"
Windows code, while trying to preserve as much of the feel of the Mac as
possible.

The only parameter the Macintosh version of StopSign needs is a WindowPtr,
which comes along with the updateEvt in event.message. Because WindowPtr is
just another name for GrafPtr, StopSign uses this single parameter to get
both the portRect into which to scale the stop sign, and the GrafPort where
output calls are directed.

In the Windows version, notice that we pass in two parameters: an HWND and
an HDC. An HWND is a handle to a window and an HDC is a handle to a Device
Context (DC), which is a data structure maintained by Windows that, like a
GrafPort, maintains graphics states such as the current pen, clipping
region, current position, font, and so on.

While the Mac keeps a very close association between a window and a GrafPort
(in fact, a WindowRecord is a GrafPort, with some extra fields stuck on the
end), under Windows an HWND often has only an HDC associated with it while
the application is servicing a WM_PAINT message. One of the most important
tasks of BeginPaint is to give an HDC to an HWND that's received a WM_PAINT.
While the pairs BeginUpdate/EndUpdate and BeginPaint/EndPaint are
syntactically nearly identical, each serves a little different purpose.


The "Magic Cookie"

From the code in Figures 1 and 3 we can see that, where the  Mac version
grabs the dimensions of the window's content region by directly peeking at
port->portRect, under Windows we use the API function GetClientRect. The
portRect and the client rect are conceptually similar, except the Mac
inconveniently includes scroll bars as part of the portRect and we have to
compensate for them.

A more important difference, though, is that under Windows we made a
function call to get at something that is simply a field in a data structure
on the Mac. This is a key philosophical difference between the Mac and
Windows: Mac programs directly manipulate the fields of a GrafPort (for
example, thePort->info) while Windows programs have no knowledge of the
internal structure of a DC or of nearly any other Windows data structure,
and have to use function calls to find things out (for example,
GetInfo(hDC)).

However, don't we have a Handle to a DC? No, we have a HANDLE. A Windows
HANDLE is not a pointer to a pointer (**hThing is the thing itself) like a
Macintosh Handle; it is a "magic cookie." In the call GetClientRect(hWnd,
&r), hWnd is the "magic cookie": we pass it in, and when the function
returns, r contains our information. We don't know where in the WND data
structure the client rect is kept, and we need not; GetClientRect is a black
box. In complete contrast, thePort->portRect is more like Pandora's box. The
Windows API is not perfect, and in some areas it is inferior to the
Macintosh Toolbox; but for data abstraction Windows wins hands down.

In other words, the Windows API has a function-call interface (narrow
channels of communication between your program and the environment); the
Macintosh approach of working directly with the data structures and low-
memory globals approach works well right now, but the channel is too wide to
constitute an interface.


Being Explicit

One of the most noticeable differences between the Mac code in Figure 1 and
the Windows code in Figure 3 is that, on the Mac a line may be drawn with
Line(8 * scale, 0), while in the Windows version we say Line(hDC, 8 * scale,
0).

That one little hDC parameter is quite important. Whereas QuickDraw calls
operate implicitly on thePort, and therefore establish the current port
with SetPort (squirreling away a pointer to the previous port so that it can
be restored at a later time), every GDI call takes an HDC as an explicit
parameter.

This passing around of hDC from function to function as though it were a hot
potato will seem tedious for tiny applications with only one window and
only one DC but, for real applications with multiple windows, it can
result in cleaner code than the frequent GetPort/SetPorts one often sees in
Mac code.


The Fleeting GrafPort

To establish a border of 5 units on all four sides of the stop sign, both
versions reduce the size of the drawing area by 10 units and shift its
origin southeast by 5 units. On the Mac, this is done with SetOrigin. The
previous origin is saved so that, when we finish, we can restore things to
the way we found them.

Under Windows we could use SetViewportOrg, but using OffsetViewportOrg
seemed more suitable, since then we don't need to know what the previous
origin was. Why viewport? We'll get back to that when we discuss our second
version of the stop sign.

A more important point is that in this case it is not absolutely
necessary to undo the OffsetViewportOrg at the end of the Windows version.
Remember that an HDC is not tied tightly to a Windows window as a GrafPort
is to a Mac window. Each time BeginPaint is called, we get a fresh DC. Any
alterations we made to a DC, such as altering the origin or selecting a
different pen or font, disappear after calling EndPaint. This means that, if
you're about to call EndPaint anyway, you may be able to avoid undoing some
changes; but it also means that you have to set up what you need each time
you call BeginPaint.

Many Mac programs will have a hard time with this fleeting GrafPort,
because they assume they can ask at any time about, say, GrafPtr->pnLoc.
When these programs are ported to Windows, the assumption is that at any
time you can call, say, GetCurrentPosition(hDC), not realizing that you
often don't have an HDC.

There is a way for a Windows program to give each of its windows a DC that
persists even after you've called EndPaint and for which EndPaint is, in
fact, a NOP. When you establish a window class during the initialization
of a Windows program, you can specify its style as CS_OWNDC. All Windows
created with this class then have their own DC.

Don't do this! However tempting it is for ported Mac code to use CS_OWNDC,
don't succumb to this temptation. Using CS_OWNDC results in a lot of
unnecessary window invalidation and generally causes problems in the form
of weird repainting bugs.


Emulating Toolbox Calls

Returning to the code in Figure 3, note that we had to write our own Line
and Move functions. Where the Mac has LineTo, MoveTo, Line, and Move,
Windows has only LineTo and MoveTo. Here we get into one strategy for
portable code: rather than rewriting StopSign so it uses only LineTo and
MoveTo, we wrote our own Line and Move for Windows.

For simple functions like these, emulating the Toolbox under Windows works
well. While MoveTo moves the current position to an absolute location in
the current GrafPort (on the Mac) or the designated HDC (under Windows),
Move offsets the pen relative to its current position. To write a Windows
version of Move, we need to know the current position. GetCurrentPosition
returns (x,y) packed into an unsigned long; the individual components are
broken out using the LOWORD and HIWORD macros from windows.h. Always use
these macros to extract such packed information. Never write your own code
to do so.

While we're supplying these Mac-like routines, it is useful to make them
behave as much like other Windows functions as possible. Although we never
use the return value, we pass it along. And for consistency's sake we use
the FAR PASCAL modifiers. Both the Macintosh Toolbox and the Windows API use
the Pascal calling convention, but for different reasons. FAR is something
that we'll explain later, when we talk about memory.

There's one other change to make: adding stop.c to the Hello make file.
While editing the make file, add the -W2 switch to the compiler command line
so that you get adequate warnings from the C compiler.

To compile the code, all you have to type is MAKE HELLO. All sorts of
inscrutable compiler switches will fly by on the screen. If all goes well,
make will also run the linker and the RC resource compiler.

We've been focusing on the changes to the Mac code in Figure 1 to turn it
into the Windows code in Figure 3, but the two versions are actually
similar. Both use the same formula to compute the scale by which all
coordinates are multiplied. The scale is divided by 18 because that's the
largest unit of measurement in the sequence of MoveTo/Line/Move calls.


A PC is Not a Mac

It's time to run our modified HELLO.EXE. If you're developing under
Windows/386, you can just double-click on HELLO.EXE. If you're using
Windows/286, first you'll have to go into Windows. Either way, you start up
the new application and... it's awful! Unless you're running on a VGA or a
monitor with square pixels, the stop sign is horribly elongated. It's
particularly nasty on a CGA (see Figure 4☼). The stop sign resizes itself
nicely enough when we resize the window (so at least GetClientRect is
working for us), but the shape remains distorted.

Here we discover that a PC, even a PC running Windows, is not a Mac. Macs
have screens with square pixels, and more and more PCs (and all PS/2(R)
machines) come with square-pixel monitors, while Windows programs have to
run on a variety of monitors, good, bad, and ugly (otherwise known as VGA,
EGA, and CGA). Windows programs can make fewer assumptions than Mac
programs.

For a guide to the world of PC graphics adapters, see Richard Wilton's
Programmer's Guide to PC & PS/2 Video Systems (Microsoft Press, 1987).
Microsoft Windows will shield you from having to know whether you're running
in 640 x 480 or 640 x 200─if you let it.

The stop sign is elongated because we're using Windows' default mapping
mode, MM_TEXT, in which units of measurement, such as the ones we use in our
MoveTo/Move/Line calls, represent pixels on the screen. This results in
graphical output that depends on the display being used, but it is closest
to the arrangement on the Mac-where all the coordinates represent pixels on
the screen, 72 to the inch, and where there is a good match between printers
and monitors.

This presents a problem: using the default MM_TEXT mapping mode may be the
easiest way to port Mac code to Windows, because Mac programs assume that
a unit is a pixel and they do their own scaling. But bear in mind that
squares will only look square on a VGA. Using the closest analogies between
two systems does not always produce good results, so beware of using MM_TEXT
for graphics. The units-are-pixels arrangement just happens to works well on
monitors with square pixels by coincidence. This is why your development
hardware should include at least one CGA monitor. Don't do your testing on
one of those beautiful 8 1/2 x 11 paper-white monitors; everything looks
great on those, making them nice to work with, but worthless for testing.


Take Two

Do we to have to check what monitor we're running on, and scale differently
in the x and y directions? No, in fact, we can dispense with the (* scale)
stuff altogether for the PC version, and let Windows do all our scaling for
us.

Windows has programmable coordinate systems. We can set up a mapping mode so
that the stop sign will come out square, no matter what screen is used. We
can set up our own units of measurement so that, for example, 18 units
always fills the window no matter what the window's size.

But we don't want arbitrary scaling along both the x and y axes; we want the
stop sign to fill the window as completely as possible, without distorting
its shape. We can do this using the MM_ISOTROPIC mapping mode, which is used
in the analog CLOCK program that comes with Windows (and the source code for
which comes with the Windows SDK).

The Mac doesn't really have any built-in facility for programmable
coordinate systems. Macintosh pictures have a picFrame which is scaled to a
destination rectangle when you call DrawPicture, resulting in an arbitrary
scaling along the x and y axis. This is similar to Windows' MM_ANISOTROPIC
mapping mode. The Mac also has the ScalePt and MapPt functions, but there
isn't anything as general as Windows' mapping modes.

In order to use the isotropic mapping mode, Windows needs to know what unit
of measurement you're using and what rectangle these should be mapped onto.
For example, our stop sign seems to be in a unit of measurement whose
largest unit is 18, and we want 18 to map to the extent of the portRect,
minus our border of 5 units on each side-actually not that difficult.

The rectangle that you're mapping into (the destRect, so to speak) is called
the "viewport," and the unit that you're mapping from is called,
unfortunately, the "window."

We've been discussing our second version for quite a while, but it's just a
tiny change in Figure 5. We set the map mode to MM_ISOTROPIC, and we set our
window and viewport extents. Instead of actually deleting all the (* scale)
code, we left it in and just set the scale to 1. If you enter the changes in
Figure 5, recompile (MAKE HELLO), and run the new HELLO.EXE, you'll see a
big improvement. The stop sign keeps its shape, no matter what the window
shape, the window size, or what monitor is used.

Because the mapping mode takes care of the conversion of "logical units" (a
very powerful idea) to device units, we can also use it for the rotation of
objects. To rotate the stop sign, just replace the line:

  SetWindowExt(hDC, 18, 18);

with:

  SetWindowExt(hDC, -18, 18);
  SetWindowOrg(hDC,  18, 18);

More information on using mapping modes for rotation of objects can be found
in Brian Myers and Chris Doner's useful book Graphics Programming Under
Windows (Sybex, 1988).


Take Three

Notice that we've been passing StopSign the window handle hWnd so it can do
a GetClientRect each time we get an update event (WM_PAINT). Not only is
adding this extra parameter a bit of a nuisance, but often when we get a
WM_PAINT, the size hasn't changed.

We can make a minor change to HELLO.C to keep the current client rect
around, updating it whenever we get a WM_SIZE message. If you search through
HELLO.C, though, you won't find any explicit handling of size messages. This
seems like a Mac program that doesn't handle a mouseDown inGrow; how can we
be resizing our windows as we've been doing, if the application doesn't
handle this event?

In Windows, "window classes" really do behave like classes in object-
oriented programming: they have a default behavior. Messages not
explicitly handled by a window procedure in its switch statement, fall
through to the default case where they are passed to DefWindowProc. A
Windows program doesn't manage the tracking of the mouse during window
resizing, the way a Mac program does by calling GrowWindow and SizeWindow.

Figure 6 shows the changes needed so that we can maintain the client rect
ourselves, rather than asking Windows for it every time. We trap for the
WM_SIZE message now, updating the global variable client_rect. StopSign no
longer takes an HWND parameter; instead, it inspects the global variable
client_rect. In a sense we've thinned StopSign's interface, since now it
takes one parameter instead of two, but now we're peeking at global
variables, which is worse. We'll fix this in our fifth version.


Take Four

If it makes sense for us to maintain our own state for the window size, it
makes even more sense to do the same for the current position, which we've
been inquiring after each time in our functions Move and Line. We could add
two more global variables, curr_x and curr_y, and use them and update them
in Move and Line-except that the current position is also changed by the
Windows functions MoveTo and LineTo. Of course, we didn't write our own
versions of MoveTo and LineTo, since Windows supplies these itself. In
order to keep curr_x and curr_y up to date, we'll have to write tiny front-
ends, MyMoveTo and MyLineTo, which simply update our variables and then
call Windows' MoveTo and LineTo.

It may sound silly, but writing this front-end is our most powerful idea
so far. Making our own versions of the "primitives" supplied by the
operating environment is the first real step toward portable code.

If you don't have a certain routine, it's often useful to create a
hypothetical one; textbooks on computer science call this "wishful
thinking." Later on, you write the routine that you hypothesized you had.
Conversely, when writing portable code it's often useful to do the
opposite; the API provides a routine, but you pretend that it doesn't. You
call your own interface routine instead; this routine can call the
underlying service. When switching from one environment to another, all that
you rewrite are your interface routines.

Now that we have a little set of four output routines and two variables that
they monitor, it makes sense to package all this in a separate module. In
our third version we added a global variable (client_rect) that's visible
all over the place, but here we've done things properly; curr_x and curr_y
are statics that are visible only to the routines that use them in our new
module, DRAW.C (see Figure 7).

DRAW.H provides the external interface to the functions in DRAW.C. It also
uses some #defines to remap all LineTo calls to MyLineTo and all MoveTo
calls to MyMoveTo. For consistency checking, DRAW.C #includes DRAW.H. But,
in DRAW.C we actually have to call the "real" MoveTo, not MyMoveTo. DRAW.C
#defines the preprocessor symbol DRAW_C; if DRAW.H sees that DRAW_C is
#defined, it doesn't change the MoveTo and LineTo calls. Writing portable
code certainly seems to involve a lot of wrestling with the preprocessor.

Don't forget to update the HELLO make file again, this time adding DRAW.C
and DRAW.H.


Take Five

At this point, we had better get this code running back on the Mac again. By
writing front-ends for LineTo and MoveTo, we are well on the way to
portable code. Instead of raw in-line calls to Windows, we're calling our
own routines, which then take care of calling Windows. Except for that hDC
in the function parameters, the routines would just as well call the Mac
Toolbox.

The first thing we must do is get rid of that HDC parameter, yet somehow
still have an HDC to pass to the Windows routines. We want each environment
to do scaling in its "native" way, yet not have separate code at the
application level.

Figure 8 shows the changed portions of HELLO.C and new STOP.C. It also shows
a fragment of a Macintosh main calling the new portable StopSign function.

What is most noticeable about the new STOP.C is that it does not #include
"windows.h" and it does not #include "QuickDraw.h". Instead we include a
small file, called CANVAS.H, which is the external interface to a new
module, CANVAS.C (see Figure 9), that provides one common interface to our
two different operating environments. For instance, STOP.C calls the
procedure offset_org, which sets the origin of a drawing context (a
GrafPort or an HDC). If compiling for the Macintosh (#ifdef MAC), offset_org
takes care of calling SetOrigin; otherwise it calls Windows'
OffsetViewportOrg.

But OffsetViewportOrg requires an hDC parameter and, on the Mac-depending
on which GrafPort we want to do a SetOrigin on-we may need to switch
thePort. How can we provide the same external interface to these two
operations which are perhaps conceptually similar, but so completely
different in practice?


HCANVAS

Continuing with this example, the first parameter to offset_org is hCanvas,
a handle to a CANVAS, our new data type provided by CANVAS.C. A CANVAS
contains all the information needed to talk to a GrafPort on the Macintosh
and to an HWND or HDC under Windows.

On the Mac, the window field of a CANVAS contains a WindowPtr; under Windows
it contains an HWND. On the Mac, offset_org fiddles with the portRect and
possibly switches thePort. Under Windows, we use the macro
CANVAS_HDC(hCanvas) to extract the HDC from a CANVAS so we have an hDC to
pass to OffsetViewportOrg. We manage to pass around some number (an
HCANVAS) at our application level, yet we still have an HDC when the time
comes to make the Windows call. Using the HCANVAS to get at a GrafPort or an
HDC is handled entirely inside CANVAS.C.

But how can an HDC be kept around inside a CANVAS when we know that the HDC
is fleeting? We are not using the CS_OWNDC trick. Instead, while we are
servicing a WM_PAINT message, the CANVAS contains a valid HDC. It is put
there by the function begin_update. When we are done servicing an updateEvt
or WM_PAINT, we call end_update which on the Mac calls EndUpdate and
DrawGrowIcon, but under Windows calls EndPaint and, most important, sets
the HDC in the CANVAS to zero.

Any canvas function, like offset_org, whose Windows version requires an
HDC, first checks to see if (!CANVAS_HDC(hCanvas)). If offset_org has been
called between calls to begin_update and end_update, then there will be a
valid HDC and we can make the Windows call. Otherwise, CANVAS_HDC(hCanvas)
will be zero.

In the code presented in Figure 9, we return when the HDC is zero. This is
particularly important because a debugging version of Windows checks for
invalid HDCs. If it finds one, it tries to send a FatalExit (RIP) code out
to an AUX device and, if you don't have an AUX device, Windows hangs with
the "Cannot write to device AUX" dialog box, waiting for you to attach an
AUX device. For an introduction to debugging Windows, see Durant, Carlson,
and Yao, Programmer's Guide to Windows (Sybex, 1988), 2nd edition, Chapter
15.

Rather than simply returning, our code should do something in order to
acquire an HDC. This technique is used in the functions env_StartDrawing
and env_EndDrawing in ENVRNMT.C, from which CANVAS.C has been extracted, and
which is used in the larger application we will discuss later in this
article. The Windows version of env_StartDrawing sets the HDC field in a
CANVAS by calling GetDC(CANVAS_HWND(hCanvas)). GetDC is a Windows function,
and CANVAS_HWND is our macro for extracting the HWND stored in a CANVAS. Our
function env_EndDrawing clears the HDC field with:

  ReleaseDC(CANVAS_HWND(hCanvas), CANVAS_HDC(hCanvas));
  CANVAS_HDC(hCanvas) = 0;

Env_StartDrawing and env_EndDrawing do something completely different on the
Mac. At the application layer, though, the effect is the same.

Looking back at STOP.C in Figure 8, note that the multiplication of (x,y)
coordinates by a scaling factor is gone. All scaling has been moved to
CANVAS.C. The large macro with the strange name MAYBE_SWITCHING_PORT is
called from the Macintosh versions of move, line, move_to, and line_to, and
multiplies the (x,y) coordinates by CANVAS_SCALE(hCanvas).
CANVAS_SCALE(hCanvas) is set by the function set_scale on the Mac, but under
Windows, set_scale sets the map mode, the window extent, and the viewport
extent.

As seen in Figure 9, a CANVAS contains all sorts of things besides GrafPorts
or HWNDs and HDCs. Everything that in previous versions was scattered all
over the code, like the global variable client_rect from the third version,
or the statics curr_x and curr_y from the fourth, are now part of a CANVAS.
The CANVAS has become our own machine-independent GrafPort or DC.


Raw or Cooked

While STOP.C contains no explicit Windows calls, it is still Windows code;
it is simply portable Windows code. At the same time, while it contains no
explicit Mac Toolbox calls, it is also Macintosh code, because the functions
it calls in turn make Mac Toolbox calls.

It is important to note that you can have Windows code that doesn't contain
"raw" calls to OffsetViewportOrg or to BeginPaint. Something can be Mac code
without containing raw calls to SetOrigin or BeginUpdate.

Code found in good commercial applications does not resemble the code
published in Microsoft Systems Journal or in MacTutor because, in order to
show how to use an API, such sources must of necessity present nonportable
system-dependent calls intermixed with higher-level code. We call this the
raw style of coding, in contrast to the "cooked" form shown in STOP.C.

Calling the cooked transform_something, rather than the raw API call
TransformSomething, is only the beginning. Most commercial applications
require a higher level of abstraction than that. Many commercial
applications use typedefs and #defines to alter C so that it's unclear to
outsiders and newcomers. However, this is essential for keeping the code
close to the problem at hand, and for maintaining portability.

Portable code is broken into an application layer (core) and an environment
layer (edge). Ideally, the application layer doesn't change, no matter what
system it's running under, and only #includes your own .h files. This means
you must ensure that the application layer of your application knows nothing
about files such as "windows.h" or "QuickDraw.h." This is a litmus test to
determine how well you have isolated yourself from system-level
dependencies.

Depending on the level of your commitment to portability, the "only our own
.h files" rule might even extend to the C standard library. For instance,
while Microsoft C has the memset routine-declared in <string.h> for ANSI C
compatibility and in <memory.h> for UNIX System V compatibility-Lightspeed C
on the Mac has a function setmem which does the same thing, except its
arguments are in a different order and its declaration is in <unix.h>.
Using these routines is preferable to writing your own, because the compiler
manufacturers are supposed to implement them efficiently (using REP STOSB on
the PC, for example), but you also want to be shielded from minor
differences. One solution might be a file, STD.C, to handle all nonstandard
standard library routines, and so the environment layer might even include
such standard routines as memset.

In order to highlight the correspondence between Mac Toolbox functions and
their Windows equivalents, our files CANVAS.C (see Figure 9) and MEMMGR.C
(see Figure 13) are designed to compile on either environment using the flag
#ifdef MAC. But as Rex Jaeschke points out in his new book, Portability and
the C Language (Hayden Books, 1988), p. 9, "A general misconception is
that exactly the same source code files must be used on all targets such
that the files are full of conditionally compiled lines. This need not be
the case at all." There ought to be two separate CANVAS.C files-one for the
Mac and one for Windows.

There is one serious problem with our use of #ifdef MAC. We have assumed
that we are either compiling for the Mac or for Windows. But what about the
OS/2 Presentation Manager, X Windows, or Display PostScript(R) systems like
NeWS, or the NeXT computer? In actual code, you would want multiple
versions of a file like CANVAS.C, one for each environment. Additionally,
rather than #ifdef MAC, we might have used the various compilers' predefined
symbols, such as THINK_C for Lightspeed C 3.0 or the Macintosh symbol
defined by Apple's MPW C compiler.

The code that we show in CANVAS.C and MEMMGR.C is filled with #ifdef MACs,
and while it should be done with a separate CANVAS.C or MEMMGR.C for each
environment, remember that there are no #ifdef MACs at our application
level.

While we're proposing a separate environment-specific module for each
environment, also remember that the external interface does not change.
There might be a CANVAS.C for the Mac, a CANVAS.C for Windows, and another
for X Windows, but there need only be one CANVAS.H. We set up a common
interface to disparate systems, using C files such as a Modula-2
IMPLEMENTATION module and H files such as a DEFINITION module.

There is an argument against using layers for reasons of portability:
efficiency. But note that the same argument can be made against using C
rather than assembler. Writing raw Windows or Mac code is in a way
equivalent to writing in assembler.


Pictures and Metafiles

It is true that, now that we're going through the extra layer of CANVAS.C,
our stop sign code runs a little slower. Each time we call move_to or line,
the first parameter is checked to ensure that it's a valid HCANVAS. Before
we can call the underlying Mac or Windows call, we must extract the
appropriate field from the CANVAS data structure.

It would be preferable to have all this validation the first time we call
move_to or line, without continual checking every time we get an updateEvt
or WM_PAINT message. If only there was some way to compile our CANVAS calls
when the program starts up, and then play back the compiled object whenever
we get an updateEvt or WM_PAINT.

The Macintosh has a very general facility for doing just that: pictures.
Between calls to OpenPicture and ClosePicture, any QuickDraw calls are
compiled into a GrafPort's picSave field, instead of being drawn to the
screen. The picture can be displayed by calling DrawPicture. When you're
done with it, the picture's memory is freed by calling KillPicture.

Similarly, Windows has metafiles. Between calls to CreateMetaFile and to
CloseMetaFile, GDI calls can be sent to a metafile HDC instead of to a
visible HDC on the screen. The metafile can be drawn by calling
PlayMetaFile. To free the memory occupied by a metafile, call
DeleteMetaFile.

In Figure 9, the CANVAS open_picture, close_picture, draw_picture, and
kill_picture functions provide a common interface to Mac pictures and
Windows metafiles. In Figure 10, StopSign uses these routines, creating the
picture once, but drawing it every time.

This presents an interesting question. Since the CANVAS (GrafPort or DC)
onto which we are drawing the picture can change in size, how does our
scaling take place? We've saved QuickDraw or GDI calls into a picture when
the CANVAS was one size, but when we play the picture back the CANVAS size
may have changed.

As we mentioned during the discussion of scaling in our second version, Mac
pictures have a picFrame which is scaled to a destination rectangle when you
call DrawPicture. This is similar to the Windows MM_ANISOTROPIC mapping
mode, in that there is arbitrary scaling in both dimensions.

In Windows, the same metafile can be drawn into any mapmode/viewport/window
configuration. As Charles Petzold explains in Programming Windows
(Microsoft Press, 1988), p.628, the contents of a metafile "are simply
units. They will take on meaning only when you play the metafile." What is
displayed when you play a metafile depends, not on the configuration when
you saved the file, but on the configuration when you play it back. Scaling
also works here.

We have made Windows metafiles seem similar to Mac pictures. A picture can
contain any QuickDraw calls, but only a subset of GDI calls can be compiled
into a metafile; in particular, no Getxxxx functions can go into a
metafile, and this was the real reason for removing the call to
GetCurrentPosition from Line and Move in our fourth version.

Since Windows is more restrictive in this situation, we'll have to restrict
ourselves on the Mac as well if we want our code to be portable. We feel
that accepting these restrictions is better than the other choice, as taken
in the XVT Toolkit: that of using Windows bitmaps rather than metafiles as
the analog for pictures.

In addition to the drawing opcodes that you usually put into a picture on
the Mac, you can also use the PicComment function to introduce arbitrary
data into a picture. One might, for example, put PostScript commands in a
picture comment. They are called comments because they are usually ignored
by DrawPicture. However, by setting the commentProc field of the grafProcs
field of a GrafPort, you can hook into the stream of PicComments and process
them [see Scott Knaster, Macintosh Programming Secrets (Addison-Wesley,
1988), pp. 174-181].

Windows doesn't have anything exactly like this. Using EnumMetaFile to
enumerate all the records within a metafile and PlayMetaFileRecord to play
back an individual GDI opcode, you can define your own metafile processing.
Windows metafiles can easily be stored on disk and shared between
programs.

Note that PostScript and the Apple(R) LaserWriter(R) are fully supported by
Windows, but in a manner different from the LaserWriter driver's use of
PicComments.


Proof-by-Existence

We can agree that an intermediate layer between an application and its
environment is a useful mechanism for providing portability. The tricky part
is the detail involved in implementing this layer. Windows and the Mac
Toolbox are two very complex and rich environments, each with its own
idiosyncrasies. In each environment there lurk several "gotchas" waiting to
trip the unsuspecting programmer.

Things that seem second nature to an experienced Mac programmer are
unavailable on Windows, and vice versa. Often these discrepancies are only
discovered when attempting to write a large application and port it from one
environment to the other.

The sample application described in this article represents our attempt to
create a medium-sized generic application that raises as many potential
problems as possible to provide a proof-by-existence of the validity of our
approach. Time did not allow completion of a truly representative
application, but the program does illustrate relevant issues regarding
windows, events, graphics drawing, memory allocation, and command
processing.


Application Structure

The application, GENAPP, is a first draft of a draw program that allows
creation of graphic objects and selection and manipulation of these
objects. Draw programs are often called object-oriented programs by way of
contrasting them with bitmap-oriented paint programs. With paint programs
the user manipulates the bits on the screen almost like physical entities.
With a draw program the user manipulates abstract geometric objects such
as rectangles or polygons through their screen representation or view.
These geometric objects exist in some abstract mathematical space (the
world coordinate space) separate from their screen representation.

GENAPP highlights this distinction, between the set of abstract geometric
objects and their representation on the screen display. This is achieved by
providing both a graphics view and a textual view of the data model. For
example, a rectangle object has both a graphic representation (the
rectangular shape drawn on the screen) and a textual representation (a line
of text describing the rectangle's attributes). The user can select and
manipulate objects through either the graphics view or the text view.
Moreover, there can be multiple graphics views and/or text views onto the
same data model. Finally, the application can maintain multiple data models
concurrently, in the same way word processing programs let you open
multiple documents for editing at the same time.

The program is split into these two major parts:

  ■  an environment-specific layer that serves as an intermediary between
     the rest of the application and the host environment, be it PC or Mac.
     This is in the file ENVRNMT.C (filenames have been truncated to eight
     characters or less because of MS-DOS requirements).

  ■  an environment-independent core section that represents the heart of
     the application. In a real-life substantial application, this might
     represent many modules and tens of thousands of lines of code. In our
     example, it is one file called APPLCATN.C, containing many of the
     components of larger applications.

The structure of the sample application parallels in miniature the
architecture of many illustration, word-processing, page layout, and CAD/CAM
programs.

The major elements of the environment-independent section (APPLCATN.C) are
as follows:

  ■  The document manager: a document is an entity that contains a single
     data model and any number of views onto that data model.

  ■  The view manager: a view maintains a consistent screen representation
     of a document's data model. There can be either graphics or text
     views. A view is closely linked with a CANVAS.

  ■  The data model manager: a data model is owned by a document and
     contains a collection of objects. In our toy application, this is the
     only entity that is dynamically allocated via the memory allocation
     routines provided by ENVRNMT.C.

  ■  The object manager: implements operations on individual objects,
     such as drawing or highlighting an object or changing its attributes.
     Many of these operations are carried out by calling routines in
     ENVRNMT.C

  ■  The application-level event loop: requests application-level abstract
     events (APPEVENTs) from the environment-specific layer (which has its
     own internal event loop as well). Depending on the type of event
     received, calls are made to appropriate application-level modules such
     as the view manager or document manager to handle these events.

  ■  The menu command dispatcher: called by the application event loop
     upon receipt of a COMMAND event.

The header file APPLCATN.H contains function prototypes for all the
routines of the managers listed above. It is therefore a useful summary of
what has been implemented and what has been left out.


Environment-Specific Layer

The file ENVRNMT.C contains environment-specific modules to deal with the
following functions:

  ■  Memory allocation: functions to allocate relocatable chunks of memory,
     to resize these chunks, to lock them and get a pointer to the object's
     contents, to unlock an object, and finally to deallocate or dispose
     of an existing memory block.

  ■  Menu handling: environment-specific menu initialization, as well
     as handling of the system menu (known on the Mac as the Apple menu,
     which contains coresident desk accessory programs).

  ■  Event processing: the function GetAppEvent, which handles all
     environment-specific events and transforms them as appropriate into
     application-level events.

  ■  Window management: functions to create a physical window on the
     screen display (controlled by our abstract entity known as a CANVAS),
     and functions to activate, resize, update, and move these physical
     windows. The internal CANVAS data structures are maintained in
     consistency with the physical windows in the environment.

  ■  Graphics rendering: these functions are closely linked with the window
     management functions above by means of the CANVAS entity. The
     functions draw basic graphic shapes on the screen (rectangle, line,
     ellipse, and text), and maintain a coordinate system which is
     independent of the screen aspect ratio.

  ■  Environment initialization: on the Mac, initialize the appropriate
     managers. Under Windows, register the application window classes.

The key factor is that the external interface to this environment-
specific code is, like our earlier CANVAS code, environment-independent.


What's Missing

Due to publication deadlines, large areas of functionality provided by the
Mac and Windows are completely ignored by GENAPP and its environment layer.
These include all functions having to do with: printing, reading and
writing of files, putting up dialog boxes, dealing with scroll bars and
scroll events, and tracking the mouse to interactively resize an object.

With a few exceptions, it is our opinion that these functional areas are
conceptually similar enough that adding them to the existing
environment/application framework can be a largely straightforward (albeit
lengthy) exercise for the reader. Still, there are probably enough gotchas
for the exercise to remain interesting.

One messy area is the tracking of the mouse, for interactively resizing or
rubberbanding a graphic object. An approach that should prove viable here is
the one used by the Mac TEClick, TrackGoAway, or DrawWindow routines. In all
these cases, the mouse click is used to initiate an internal mouse tracking
process that continues until the mouse button is released. The actual
rendering of the graphic object can then be implemented by an application-
supplied callback routine.

GENAPP provides a skeleton for adding this kind of functionality. Also
missing from the sample application is a lot of detail on existing sections
in ENVRNMT.C. For example, the memory management section has no function
that returns the size of a memory block. Adding one would be trivial for
both the Mac and Windows (through calls to Mac GetHandleSize or Windows
GlobalSize). It happens that our sample application does not need this
particular function.

Our section on graphics rendering only supplies a few basic primitives
(for rectangle, ellipse, and text). Both the Mac and Windows have many more
drawing primitives that can be mapped to each other. One known area of
difficulty is the treatment of fonts and text attributes. Windows has a
complex largely undocumented font selection method which is both very
different from that on the Mac, and substantially changed for Presentation
Manager.


Getting Input

So far, we have examined how an application draws correct output to the
screen display. This is done in a portable fashion by introducing an
intermediate layer between the application and the GUI. An equally
important subject is how the application program gets its input from the
user.

Programmers writing nongraphics applications formerly had to deal with
characters entered at the keyboard, or perhaps coming in from a serial
port. As Mac programmers know, the Mac environment dramatically extended the
range of possible inputs, to include not just keystrokes, but also mouse
actions (mouse up, mouse down) and other possible inputs (network, device
driver, and internally derived inputs). Because of the high degree of
interactivity between program and user, it became useful to conceive of an
application's structure as passive and event-driven, rather than as active
and data-oriented (as found, for example, in old-fashioned batch utilities
that transform or filter simple streams of data).

Unlike batch-oriented programmers, Mac programmers are familiar with the
concept of structuring an application as a passive event loop that waits for
and responds to a variety of events. These events include inputs from the
user as well as internally derived inputs such as window activation and
window updating events.

To encompass all the information an application would need about an event,
the Macintosh defines the EventRecord data structure, and Windows defines
the similar MSG data structure. Both structures include what type of event
occurred (event.what on the Mac, msg.message under Windows), the time it
happened (event.when or msg.time), as well as where the mouse was
(event.where or msg.pt).

The Mac and Windows are very similar here, but the terminology may be a
little confusing. While Mac events are called messages in Windows, note that
the Mac EventRecord also refers to a message. This is the extra information
that comes along with an event; its meaning differs depending on event.what.
For instance, if (event.what == keyDown), then event.message will contain
the ASCII code and key code. Under Windows, this same type of information is
kept in the fields wParam and lParam (see Figure 11).


Many Messages

Most of the changes from Mac events to Windows messages involve making
familiar concepts more flexible and more general.

Inside Macintosh defines eleven types of system events. By way of contrast,
the standard Windows header file defines 96 types of messages. What do these
96 messages consist of?

  ■  Twenty types of mouse messages (rather than two mouse event types on
     the Mac). These types are necessary to handle one, two, and three
     button mice. They also treat a double-click as its own type, rather
     than relying on the application program to detect it (assuming that the
     window has been registered as one accepting double-clicks). In
     addition, there is a parallel set of mouse messages dealing with
     events in the non-client area of a window (known to Mac programmers
     as what lies outside the inContent part of the window: title bar,
     close box, etc). Unlike the Mac, where programs have to query the
     environment with GetMouse calls during null events, Windows considers
     simple movement of the mouse to be sufficiently interesting to merit
     its own WM_MOUSEMOVE message. Note that the WM prefix stands for
     Window Message, as every MSG includes the window for which it is
     intended.

  ■  Twelve types of keystroke messages (rather than three on the Mac). Some
     of these deal with system keys, which are distinguished from other
     keys.

  ■  Fifteen types of clipboard messages (rather than none on the Mac).
     These messages inform the application of changes in the content of the
     clipboard, in the chain of clipboard viewers, or in various other
     clipboard-related requests. See "Exchanging Data Between Applications
     Using the Windows Clipboard," MSJ, (Vol. 3, No. 5).

  ■  Twenty-three types of window messages (rather than two on the Mac).
     Some of this population explosion results from parallel sets of
     messages dealing with events in the content area of a window and
     outside the content area. Additional message types come from the fact
     that the Windows environment does the equivalent of a Mac FindWindow
     call for you in response to a mouse-down event. This means that a user
     action such as clicking on the close box of a window is abstracted into
     a WM_CLOSE message, rather than being passed to the application as a
     mouse-down event requiring further interpretation and decoding.
     Similarly, Windows does not require the application program to worry
     about mouse-down events in the menu bar. These are tracked by the
     environment and abstracted into appropriate menu COMMAND messages.

Even though Windows has a WM_NULL message, this is not nearly as important
to Windows programs as a nullEvent is to Mac programs. Many Mac programs
use nullEvent to handle a multiplicity of housekeeping tasks, such as menu
coloring. This has been a problem for Mac programmers wanting to modify
their programs to be MultiFinder-friendly. Apple now strongly discourages
programmers from doing a lot of processing when handling a nullEvent.

This brings up a major difference between Windows and the Mac: Windows was
designed from the start to be multitasking. In practice, this is a difficult
job because Windows is not an operating system, but an application-level
piece of software running on top of MS-DOS(R). This affects its performance,
not its design. Because Windows was designed to have multiple applications
executing concurrently, it is not surprising that there is much greater
support for interapplication communication and data transfer. This is
done through flexible user-defined messages (much better than app1Evt on the
Mac), clipboard messages, and message protocols such as Dynamic Data
Exchange (DDE).

An analogous situation exists on the Mac with regard to networking. The
Mac was designed to support a cheap built-in local area network (the
AppleTalk(R) port has always been standard on Macs, so it is not surprising
that the Toolbox has a network event type). In contrast, most PC-compatible
machines that Windows runs on presently lack hardware to support local area
networks, so this message type is missing from Windows.


Events and Messages

Fortunately for the programmer, though Windows gives you 96 types of
messages, it also provides a default handler for them, called DefWindowProc.
This brings up a significant difference between event/message handlers on
the Mac and on Windows.

On the Mac, applications wait in a loop, requesting events with
GetNextEvent, or with WaitNextEvent, which is used by MultiFinder-aware
applications and specifying which events they are interested in handling
by means of an eventMask. The following code should be very familiar to Mac
programmers:

  while (! done)
  {
  if(GetNextEvent(
      everyEvent, &theEvent))
  {
    switch (theEvent.what)
      {
       case mouseDown:
         doMouseDown;
         break;
       case updateEvt:
         doWindowUpdate;
         break;
           ∙
           ∙
           ∙
       }
   }
  }

In Windows, all programs need a callback function for each type of window
they create. This callback function is known as a window procedure, or
WndProc. You must call Windows first, to get it to call you, with a message
loop that looks similar to the Macintosh event loop:

  while (GetMessage((LPMSG) &msg, NULL, 0, 0))
  {
    TranslateMessage((LPMSG) &msg);
    DispatchMessage((LPMSG) &msg);
  }

DispatchMessage is the Windows routine that decides which of the various
message handling WndProcs it will call. In some ways it is similar to
FindWindow on the Mac, transforming a low-level raw event, like a mouse-
down, into a slightly higher-level, more abstract event, like a menu-bar
click.

Windows has a routine, WindowFromPoint, that does the same thing as
FindWindow, but it is hardly ever used because of the all-important
DispatchMessage.

By the way, note that the return value of the Windows GetMessage is a
Boolean value telling the application when to quit. In contrast,
GetNextEvent returns a Boolean value telling the application whether or not
it should handle the particular event that just passed.

The message-handling WndProc on Windows (the one called by DispatchMessage)
typically looks like this:

  long far pascal myWndProc( hWnd, message, wParam, lParam) HWND hWnd;
   int message;
   WORD wParam;
   LONG lParam
   {
     switch (message)
     {
       case WM_SYSCOMMAND:
         doSysCommand( hWnd, wParam,lParam);
         break;
       case WM_CREATE:
         doCreate(hWnd, wParam, lParam);
         break;
           ∙
           ∙
           ∙
     }
   }

As you can see, the parameters to a WndProc callback are nothing more than
the key components of the Windows MSG structure shown in Figure 11.

Parenthetically, note that Windows uses a Pascal calling convention similar
to that on the Mac, though for different reasons. The Microsoft C compiler
requires this non-K&R keyword to be in a different place from where the
Macintosh C compiler looks. Note also the additional non-K&R keyword, far,
which refers to how this function is addressed in memory.


Object-Oriented Windows

The significant difference between Mac and Windows event handling is
that-as opposed to a single event stream on the Mac that contains both
application-specific events as well as system-level events (desktop events
such as menu-bar click) for all windows-Windows uses multiple message
streams flowing to each type of application window. Windows applications,
therefore, have as many message handlers as there are window types.

Why did Microsoft make this change? It comes from an approach that extends
the Macintosh Toolbox's modular structure toward object-oriented
programming. On the Macintosh the various subsystems are controlled by
managers, which generally own one particular data structure (such as an
EventRecord or WindowRecord). Application programmers call Toolbox routines
to manipulate these data structures. (Ignore for now the fact that Apple
makes these data structures available for direct manipulation by the
programmer.)

In Windows, a window is not just a data structure manipulated by
WindowManager routines. It is conceived as an objectlike entity of a
particular type, or class, and therefore exhibits behavior associated with
that class. A window's behavior is defined as its particular responses to
messages it may receive. The generic or archetypal window has generic
behavior that is defined by the default message handler. This default
message handler (DefWindowProc) is provided by the Windows environment.

Conceptually the desktop can be thought of as the root or parent window to
multiple applications. Applications each have their own main window (with
its associated menu bar of commands), and may have any number of subsidiary,
or child windows. These windows may all be the same type (representing a
document or page), or they may be various types (document window, palette
window, status window, etc.). Each of these types can therefore behave
differently. Similarly the system has different classes of windows for
different purposes: dialog boxes, alert boxes, controls, menus.

When an application starts up, it registers its classes of windows with the
system. It must provide a message handling procedure for each registered
class. This message handler is part of a chain of message handlers, and is
passed all messages that flow through from the system to that particular
window type. The handler will examine the message stream for message types
it is interested in, and pass all other messages to the next handler on this
chain, which may simply be the default window class handled by
DefWindowProc.

When we said earlier that clicking on the close box of a window is
abstracted into a WM_CLOSE message-rather than being passed to the
application as a raw mouse-down event-this wasn't quite true. The
application does see the low-level event (it's called WM_NCHITTEST, for non-
client hit test), but most applications just pass it on to DefWindowProc,
where it's gradually transformed into higher-level events such as WM_CLOSE.

Programmers writing applications specifically for Windows have learned to
use the technique called sub-classing, which modifies the behavior of some
other existing window class (system or application) by chaining a message
handler in front of the handler for an existing window class. This new
handler needs to implement the behavior being added or changed, and can
then pass other messages to the existing handler to implement the standard
behavior.

This is an elegant, general, and flexible scheme that unfortunately does
not map very well to the more limited system on the Macintosh. In the Mac's
defense, it should be noted that there are certain window features on the
Mac which are not provided by Windows. For example:

  ■  On the Mac, you can specify at window-creation time the particular
     layering order (that is, front-to-back) for the window you are
     creating.

  ■  You can also, if you wish, specify a callback procedure to radically
     customize a window's behavior. For example, you can specify a window-
     frame routine that might draw elliptically shaped windows rather than
     rectangular ones. This type of function is called a windowDefProc on
     the Mac, which sounds like but has nothing to do with Windows'
     DefWindowProc.

Writing portable applications that run on more than one environment
requires either a subset of features common to both environments, or
simulating the missing functionality on the lesser environment. In the case
of window types, we have chosen the former approach in our sample
application. The window types used by our application are very simple, and
map easily to both Mac and Windows. In the case of events/messages, we have
chosen an approach that simulates a small amount of functionality that is
missing on the Macintosh but present under Windows.


Application Events

In our sample application, there is an intermediate layer that shields the
core part of the program from the differences between Macintosh events and
Windows messages. This is done by defining an entity known as an application
event or APPEVENT.

The structure of our application is similar to a Macintosh program, in
that there is a single top-level event handler that loops through and
processes events as they occur. The event handler gets these APPEVENTs from
the environment through calls to GetAppEvent.

As an example, the application-level event loop in APPLCATN.C looks
something like this:

  while (! done)
  {
    env_GetAppEvent(&theAppEvent);
    switch (theAppEvent.event_type)
    {
  case CMD_EVENT:
    doCommand( &theAppEvent);
    break;
  case MOUSEDOWN_EVENT:
    doMouseDown( &theAppEvent);
    break;
      ∙
      ∙
      ∙
    }
  }

The above example is very similar to the Mac event loop shown earlier. The
differences are in the data structure used (of type APPEVENT rather than
type EventRecord), and the kinds of events processed. There are 12 different
types of APPEVENTs, representing what's needed to achieve our purpose.

If our sample application were to evolve into greater functionality and
complexity, it is likely that additional types of events would be needed.
But this would be some number less than 20, rather than the 96 types defined
for Windows. The 12 APPEVENT types are shown in Figure 12.

The alert reader will note that there are six types of view events, and
views, while not yet discussed, are presumably analogous to windows. But
the Macintosh has only two event types dealing with windows (update and
activate). Where do these other types come from?

Our intermediate environment-specific layer contains an internal event
loop that actually handles the "real" events provided by the Macintosh or
Windows environment. The particular environment-specific events are
transformed by this internal handler into the more abstract APPEVENTs seen
by the application. Windows, with its 96 message types, has to screen and
dispose of uninteresting messages internally. The Macintosh has to handle
more primitive actions (such as a mouse-down in the menu bar), and translate
these into menu CMD_EVENTS similar to the WM_COMMAND messages found in
Windows.


Memory Management

Memory allocation is one area where the Mac and Windows approaches seem
extremely similar. In fact, there appears to be almost a one-to-one
correlation between Mac and Windows memory functions. This close
correlation masks some very significant differences that can cause major
difficulties for experienced Mac programmers.

However, once a Mac programmer accepts some rigid but useful constraints,
and becomes aware of differences in the underlying hardware memory
architecture, writing portable code can be straightforward──as the sample
code in APPLCATN.C shows, calling environment-independent memory
allocation primitives in ENVRNMT.C.

The memory allocation code in ENVRNMT.C is basically a series of one-liners,
with one line calling a Mac function and another the corresponding Windows
routine. For instance, env_AllocMem allocates a relocatable memory block by
calling Mac NewHandle or Windows GlobalAlloc, and env_GraspMem locks a
memory block by calling Mac HLock or Windows GlobalLock. Our environment-
independent memory functions are shown in Figure 13.

These environment-independent one-liners contribute to the deceptive
appearance of similarity. On both systems, you request a relocatable
chunk of memory and receive what both systems call a handle to an object.
But, as we have already said, a Mac Handle is not a Windows HANDLE. A Mac
Handle is a 32-bit pointer to a pointer to the actual object in memory. A
HANDLE in Windows, however, is an abstract 16-bit integer used internally
by the environment to keep track of memory objects.


Magic Cookie or **?

Macintosh programmers have come to rely on the double-indirection property
of handles in order to reference an object's content. That is, if ppo is a
Macintosh handle (that is, a pointer to a pointer to an object), then the C
language construct *ppo gives you the pointer to the object (po), and **ppo
gives you the object's contents (o). For example:

  typedef struct {
      int first_field;
      int second_field;
    } SOME_OBJECT;
  Handle aHandle = NewHandle(sizeof(SOME_OBJECT));
  (**aHandle).first_field = some_value;
  (**aHandle).second_field = another_value;

In contrast a Windows programmer obtains a pointer to an object by means
of a Lock request to the system. This always happens before referencing
the object's content, and is then followed by a corresponding Unlock
request after use:

  HANDLE aHandle = GlobalAlloc(GMEM_MOVEABLE, sizeof(SOME_OBJECT));
    SOME_OBJECT far *ptr = (SOME_OBJECT far *)
    GlobalLock(aHandle);
    ptr->first_field = some_value;
    ptr->second_field = another_value;
    GlobalUnlock(aHandle);

Although the Mac has similar functions to lock and unlock a memory object,
many Mac programmers will only lock an object when repeated references are
going to be made. For the quick-and-easy assignment of a value into a single
field of an object, it is much more convenient to use the double-
indirection method illustrated above. Referencing of a memory object's
content via the "star-star" approach is an atomic or indivisible operation
(at the C language statement level, though in C++ one could overload the *
dereferencing operator), and therefore does not require locking an object,
because the Mac Toolbox has no opportunity to shuffle memory around within
that C language construct.

Even when using the Mac lock/unlock functions, many Mac programmers have
learned the exact circumstances under which the Mac Toolbox will shuffle
memory, and therefore often use a pointer to a memory object without locking
the object. This won't work in Windows. The Mac programmer has two choices
for handling this discrepancy between the two environments. One is to
simulate the feature missing from the environment by means of an
intermediate layer of software at the application level. The other is to
restrict oneself to the functionality that is common to both
environments.

In the case of Mac events and Windows messages discussed previously, it was
relatively easy to simulate the Windows message types that were "missing"
on the Macintosh: COMMAND_EVENT, QUIT_EVENT, etc. In the case of memory
management, however, this kind of simulation is much more difficult.

We once wrote a large piece of code to simulate Macintosh-style ** handles
on Windows, but there was a significant loss in performance. Our current
opinion is that the Mac programmer is better off learning a little
discipline and limiting himself to the more restricted style of Windows
memory management.


Additional Discrepancies

It is helpful to note that the lock/unlock functions do not nest on the
Macintosh but do on Windows. Every Lock statement needs to be balanced by
a corresponding Unlock in Windows. On the Mac, a single Unlock on a
particular object will take effect immediately, regardless of how many Lock
requests have previously occurred. The Mac uses a single bit per object to
maintain lock information, while Windows uses a 16-bit counter incremented
and decremented by respective Lock and Unlock requests. In GENAPP the
lock operations do not nest at all, so this will work on either system.

Another difference is that Windows' GlobalReAlloc, unlike the Mac's
conceptually identical SetHandleSize, can return a different handle than the
one given at object creation time. This means that applications should
have a single place where a handle's value is stored; otherwise copies of
that handle that are left lying around in other data structures may become
invalid when that block is resized by some other part of the program.

A further difference between corresponding functions is that Windows
allocates memory in units of 16 bytes. Say you ask for a 1-byte object in
Windows, then later request its size with a call to GlobalSize (which is
equivalent to GetHandleSize on the Mac). The size returned by GlobalSize
will be 16 bytes, while on the Mac it will be 1. For certain application
programs that use this value to control further processing, it would be an
annoying, but resolvable, difference.


Environment-Independent Memory Functions

As stated earlier, you can sidestep the mismatch between Windows and Mac
memory functions by using an environment-independent layer of functions
common to both environments. This approach will impose some constraints on
the Mac programmer.

Consider the following function from the sample application in APPLCATN.C.
AddGraphicsObjectToModel adds a graphics object to the previously allocated
data model. Writing the code as a Mac programmer would, with ** memory
references and no locking, we get the code shown in Figure 14.

In AddGraphicsObjectToModel, the argument data_model is of type HMODEL, and
is a handle to a collection of graphic objects. Don't confuse graphic
objects (which are geometric entities like a rectangle object or an Ellipse
object), with memory objects. The data_model is a memory object which was
previously allocated with a call to AllocMem.

The data_model, as a dynamic array of graphic objects, must grow in size in
order to contain the new graphic object being added to the model. This
occurs with a call to ReAllocMem, which on the Mac calls SetHandleSize and
in Windows calls GlobalReAlloc. Once the data_model has grown to a larger
size, our sample function calls a subordinate routine named
MakeGraphicsObject to set the fields in the graphic object with attribute
information (such as rectangle dimensions or whether it is round-
cornered).

The code in Figure 14 would need several modifications to run under
Windows:

  ■  The data_model argument needs to be a pointer to a handle rather than a
     handle, because its value may change when the memory block is resized
     (though in this particular piece of code it won't).

  ■  The data_model needs to be locked and unlocked twice-first to get the
     value of the num_objects field (and increment this field), and
     second to call MakeGraphicsObject with a pointer to the new graphics
     object. Between these two steps, the data_model needs to be unlocked so
     that it can be resized.

  ■  The pointer to the data_model needs to be of type PMODEL rather than
     MODEL * due to the segmented memory architecture of the Intel CPU.
     Likewise, the argument to MakeGraphicsObject needs to be of type
     POBJECT rather than OBJECT *.

The revised portable version of AddGraphicsObjectToModel is shown in
Figure 15.


Still More Differences

There are some big differences between Windows and the Mac in their
underlying operating system and CPU architecture. These differences are
not the sort that manifest themselves in formal discrepancies between
memory functions in one environment or the other (such as those we have been
discussing until now). The manifestation is more subtle, yet differences
are more significant.

These hardware and operating system differences, in order of importance, are
as follows:

  ■  the 640Kb memory limitation imposed by MS-DOS

  ■  the size difference between near and far pointers caused by the
     segmented architecture of the Intel CPU

  ■  the differences in byte ordering between the Motorola(R) and Intel(R)
     CPUs.

In practice, these differences cause more problems for Mac programmers than
the imprecise correlation between memory management functions. The
following sections describe these differences in more detail, presenting
strategies to deal with the consequent problems.


The NUXI Problem

Hacker folklore says that it was UNIX(R) programmers who first faced the
differences in machine byte ordering between different brands of CPUs. These
programmers were porting UNIX software from one hardware platform to
another and found that, if you stored the word UNIX as a sequence of bytes
on one machine (for example, the PDP-11 minicomputer), then moved this
data to another machine that had a different memory architecture (the Sun
workstation, for instance), the stored text would be printed out as "NUXI"
(depending on how the memory access was made).

Other literature calls this the difference between "big endian" and "little
endian" machines. Given a 16-bit number (that is, two bytes worth), one CPU
will store the high-order value of that number in the byte with the bigger
memory address, while another CPU will do the opposite. A similar situation
exists with 32-bit long integers in the storage and accessing of the high-
order and low-order words of that numerical quantity.

GENAPP does not encounter this problem because, in its current form, it
does not support reading and writing of data models to disk, and therefore
data cannot be transferred from one machine to the other. In real-world
applications, we have found the following rules useful for staying out of
NUXI trouble:

  ■  When problems occur, it is because a value was written by one method
     (for example, bytes), then read back on a different machine by a
     different method (for example, words). Avoid this.

  ■  Reading and writing an object a byte at a time will give the same
     results on both machines.

  ■  Reading an object's value by using the same C language construct that
     wrote that object's value will not cause problems (or, at least won't
     cause problems that can't be remedied by machine-specific header file
     definitions).


So Near, Yet So Far

The segmented architecture of the Intel CPU used by Windows machines causes
different sized pointers. These two sizes of memory pointers are called near
and far, and place the elusive goal of portability a little farther away
from the Mac programmer.

Many Mac programmers have learned to write software only on the Macintosh
and often produce reckless code like the following call to the Mac
Toolbox NewWindow function:

  WindowPtr w = NewWindow( 0, &rect, "\pUntitled", 1,0,-1,0,0);

The least of the sins committed by the above code is the lack of naming
conventions to give a hint as to the types of arguments to the NewWindow
function. It would be nice if C let you use labels inside a function call,
so that you could use named parameters:

  WindowPtr w = NewWindow(wStorage: 0, boundsRect: &rect, title:
            "\pUntitled", visible: 1, procID: 0, behind: -1, goAwayFlag:
            0, refCon: 0);

But even if Windows had a NewWindow function that took the same types of
arguments as the Mac function, the above code will behave horribly. For one
thing, since the Mac Toolbox knows only about Pascal strings, the window
title "Untitled" must be converted from a C string (null-terminated string)
to a P string (whose first byte contains the strlen) In this code our Mac
programmer has used the Lightspeed C '\p' escape, which turns a C string
into a P string. On a PC, though, Microsoft C just sees the '\p' as another
character.

There are Mac programmers who have more experience writing software for
other machines (a VAX or Sun workstation), and have therefore a more
careful approach to writing portable code. We have seen such programmers
write the above function call as follows:

  WindowPtr w = NewWindow( NIL, &rect, CtoPstr("Untitled"),
            TRUE,0,-1L,FALSE,0L);

These programmers will perhaps become still more careful after the jarring
revelation that, in the Windows/Intel environment, not all pointers are
created equal and are not interchangeable with long integers. The -1L
argument above will throw the rest of the arguments out of phase on the
stack. That argument, which is supposed to be a pointer to data, should
instead be ((WindowPtr) -1). Likewise, many Motorola and VAX(R)
programmers define NIL to be 0L in their standard header file, when it
should be ((void *)0) or ((char  *)0).

Pointers on Intel CPUs can be 16-bit near pointers, pointing to a location
in the current local address space (a memory segment up to 64Kb in size).
Or they can be 32-bit far pointers, pointing to a location in some other
segment in the global address space (whose size is 20-bits, or 1Mb, on the
808x family of processors, but larger on the 80x86 generation of CPUs).

Pointers to program code are not necessarily the same size as pointers to
program data, depending on a particular convention (called the memory
model) which is chosen at compile time. Furthermore a program's static
data can exist in a different data segment from the program's automatic
variables. Automatic variables are allocated on a program's execution
stack, which can be chosen at compile time to exist in a different memory
segment from the program's static data.

Memory models are defined as follows: small, medium, compact, large, and
huge. These names refer to the various possible permutations of near/far
pointers as they relate to a program's code and data spaces. For additional
details, check out the appropriate section of the C compiler manual you are
using for Windows.

Before you Mac programmers get too complacent about such things as
different-sized pointers, we might point out that, even with the big linear
address space of the Motorola chips, Apple has gone to a great deal of
trouble to simulate a segmented memory architecture for the Mac, as
evidenced by the Segment Loader. Segmented memory makes a lot of sense
when you want movable, discardable resources.


Saluting Typedef

What all this near/far pointer situation means in practice is that, when
passing a particular pointer as an argument down a chain of subordinate
functions, the routines further down need to be aware of the near/far
ancestry of the pointer they have been passed. In some cases it might even
be necessary to have two duplicate routines, one of which accepts near
pointers as arguments while the other one takes the far variety.

In this near/far situation, typedef assumes an importance much greater than
its creators may have envisioned. In the original UNIX source code, written
for a PDP-11 where ints and pointers were all the same size, typedef is not
used very often even with structs.

When porting to the more hostile Intel environment, it is critically
important to typedef everything, especially pointers. In GENAPP, we were
writing APPLCATN.C on the Mac at first, and originally typedef'd our data
model as shown in Figure 16.

A new data model was then dynamically allocated at run time by a call to
env_AllocMem (see Figure 17).

On the PC, however, we were using medium model (which defaults to 16-bit
near pointers to program data, and 32-bit far pointers to program code). A
problem arose because the memory allocation routines, even in medium model,
allocate memory objects from the global heap and have to return 32-bit far
pointers to these objects. Declaring a pointer to a data model as MODEL *
meant that the pointer size defaulted to the 16-bit near size. Our solution
was to create a separate type for a pointer to model as follows:

  typedef MODEL far *PMODEL;
   /* a 32-bit pointer to data model */

The above NewModel routine then had to be revised (see Figure 18).

This change produced a ripple effect in our application due to the fact that
the data model was used as a collection of graphic objects. The Object
Manager section of APPLCATN.C contained routines to manipulate these
graphic objects. Many of these routines were passed an OBJECT * as an
argument. This code needed to be changed to use the type POBJECT in place of
OBJECT *, where POBJECT was declared as follows:

  typedef OBJECT far * PMODEL;
  /* a 32-bit pointer to graphic object */

In addition to the unsung typedef, there is a newer C language construct
that assumes great importance in this near/far pointer situation: function
prototyping. Function prototyping is an enhancement to the original C
language definition and is part of the proposed ANSI standard.

Function prototyping and typedefs are the two most powerful weapons in the
war against stray pointer bugs. In GENAPP, the header files APPLCATN.H and
ENVRNMT.H contain function prototypes for every function in the program,
and were invaluable in bringing both real and potential problems to our
attention.


Large Model Drawbacks

Mac programmers, when encountering these unexpected ripple effects of
near/far pointers in an existing program, may feel a sense of delight in
discovering the compiler convention called large model, which establishes
32-bit pointers to both program code and program data. This closely
parallels the Mac; however, there are several strong reasons not to use the
large model convention:

  ■  The large model can cause a significant increase in program size.

  ■  This increase in size can have a severe impact on program performance.

  ■  The data segment for static data becomes fixed and unrelocatable,
     affecting both performance and the possibility of having multiple
     instances of the program running concurrently.

  ■  Certain tools and third-party libraries don't support large model (for
     example, the XVT library is only available in medium model).

This just shows that utilizing the closest analogies between two systems may
make your port easier in the short run but will not necessarily improve your
code.

  640Kb + 4Mb = 640Kb

Ironically the biggest single memory-related problem that Mac programmers
will encounter on Windows is not discernible anywhere in the program
listing of GENAPP. This obstacle is the 640Kb memory limitation imposed by
DOS.

Unlike the problems already discussed, this one has no visible effect on our
program code (in terms of the language syntax or constructs used). And it
can be safely ignored by toy programs like GENAPP. But writers of larger
real-world applications on Windows will face this issue time and again.

Apple encourages Mac programmers to think of the Toolbox as if it were
almost part of the hardware. The mouse tracking software on the Mac is as
close to the hardware as you can get without being hardwired into the 68000
instruction set. This is why the cursor movement on even an old Mac 128
feels smoother and more responsive than the cursor motion on a 386 machine
running Windows.

Windows is based on lessons learned in part from the Mac, so its API is more
elegant and ambitious than the Mac Toolbox. Although its programming
interface is better, the resulting programs sometimes have a slightly clunky
feel compared to their Mac versions.

The Windows environment is, strictly speaking, an application-level
program running on top of an operating system which was never designed to
support it. The 640Kb that MS-DOS gives you is meant to be shared among the
operating system, the windowing environment, your application program's
code and data, and the code and data of any other application programs that
the user will hope to run at the same time.

Mac programmers are used to taking the cover off their machine, putting in
4Mb worth of memory chips and having close to 4Mb of memory show up on the
Finder About box. On DOS systems, you can take the cover off the machine,
put in 4Mb worth of memory chips, and you'll still have less than 640Kb
available when you start Windows.

Windows/386 helps alleviate much of this problem for non-Windows old apps
(such as Lotus(R) 1-2-3(R)), by giving each of them their own 640Kb virtual
machine, but ironically Windows apps (such as Meta Software's Design) are
stuck having to share the same cramped 640Kb machine with Windows itself.

Is it any wonder that long-time DOS users are excited about OS/2, which
breaks the 640Kb barrier? Mac programmers may be unaware of the various
kludges that let you add memory to your machine without breaking the 640Kb
barrier. Expanded memory is a pretty good kludge, and you'll probably need
to know about it to get your Mac program running on the PC. The ultimate
kludge is the recently unveiled HIMEM.SYS, which goes through major
contortions in order to provide DOS with another 64Kb. HIMEM.SYS is a sure
sign that DOS is in its death throes. Forward to OS/2 and Presentation
Manager!

There are several practical ramifications of this particular MS-Windows
constraint:

  ■  The usual space/time trade-off when writing programs no longer holds.
     Programmers on other systems are used to increasing a program's data
     requirements in order to improve execution speed (for example, by
     caching computed results for later use). On Windows, if you increase
     the run-time size of your application, you may invoke lots of disk
     swapping and memory shuffling and suffer a huge performance loss.

  ■  Space is always at a premium, and needs to be considered in all
     decisions (for example, in deciding whether to use large model or
     medium model).

One advantage of the additional discipline imposed in writing for Windows
is that memory-conserving techniques have become more valuable on the Mac
with the advent of MultiFinder, as users find that 2Mb is not really very
much when you are running several applications. The small tightly coded
application is bound to be preferred in any environment over its fatter,
slower competitors. In many other ways as well, MultiFinder is helping
bring the Macintosh and Windows worlds closer together.


You Can Do It!

We have presented many details in this article that make it seem as if the
path from the Macintosh to Windows is a dangerous one. The large PC market
is inviting, but is it worth the difficulties with nonsquare pixels, eight-
character filenames, and having to really lock memory handles? The PC world
may seem strange compared to the simplicity of the Mac, but it is an
important market, and Windows and OS/2 have done a lot to improve the
quality of PC computing.

Don't be misled by all the minor problems discussed here into thinking that
porting Mac programs to the PC is enormously difficult. Our interest in
seeing much of the Mac software migrate to the PC, made us want to include
as many details as possible.

On the other hand, we have only scratched the surface. We covered
QuickDraw/GDI, event management, and memory management, and we presented a
general approach to porting code. However such topics as printing,
scrolling, the clipboard, dialog boxes, text editing, file management, and
more are completely missing from this discussion.

In some of these areas, the parallels between Windows and the Mac are very
close, while in others the differences are sufficiently large that it should
be interesting developing a higher-level interface. Finally an additional
benefit you will find, is that programming for more than one environment
will make you a better programmer in your favorite environment.


Figure 1:  Code Fragments from stop_mac.c

#include "MacTypes.h"
#include "Quickdraw.h"
                                    ∙
                                    ∙
                                    ∙
#define SCROLL_BAR_WIDTH    16
#define BORDER              5

void main()
{
    EventRecord event;
                                    ∙
                                    ∙
                                    ∙
    switch (myEvent.what)
    {
        case updateEvt:
            BeginUpdate((WindowPtr) event.message);
            StopSign((GrafPtr) event.message);
            EndUpdate((WindowPtr) event.message);
            break;
                                    ∙
                                    ∙
                                    ∙
    }
}

void StopSign(port)
    GrafPtr port;
{
    GrafPtr oldPort;
    Rect r;
    Point oldOrigin;
    short scale;

    GetPort(&oldPort);
    SetPort(port);
    r = port->portRect;         /* structure copy */
    r.bottom -= ((2 * BORDER) + SCROLL_BAR_WIDTH);
    r.right -= ((2 * BORDER) + SCROLL_BAR_WIDTH);
    scale = min(r.bottom - r.top, r.right - r.left) / 18;

    oldOrigin = topLeft(port->portRect);
    SetOrigin(oldOrigin.h + BORDER, oldOrigin.v + BORDER);

    MoveTo(5 * scale, 0);
    Line(8 * scale, 0);
    Line(5 * scale, 5  * scale);
    Line(0, 8  * scale);
    Line(-5  * scale, 5  * scale);
    Line(-8  * scale, 0);
    Line(-5  * scale, -5  * scale);
    Line(0, -8  * scale);
    Line(5 * scale, -5  * scale);

    MoveTo(0, 5  * scale);
    Line(18  * scale, 0);
    MoveTo(0, 13  * scale);
    Line(18  * scale, 0);

    MoveTo(4  * scale, 7  * scale);
    Line(-2  * scale, 0);
    Line(0, 2  * scale);
    Line(2  * scale, 0);
    Line(0, 2  * scale);
    Line(-2  * scale, 0);

    MoveTo(7  * scale, 7  * scale);
    Line(0, 4  * scale);
    Move(-1  * scale, -4  * scale);
    Line(2  * scale, 0);

    MoveTo(10  * scale, 7  * scale);
    Line(2  * scale, 0);
    Line(0, 4  * scale);
    Line(-2  * scale, 0);
    Line(0, -4  * scale);

    MoveTo(14  * scale, 7  * scale);
    Line(0, 4  * scale);
    Move(0, -4  * scale);
    Line(2  * scale, 0);
    Line(0, 2  * scale);
    Line(-2  * scale, 0);

    SetOrigin(oldOrigin.h, oldOrigin.v);
    SetPort(oldPort);
}


Figure 3:  Take One of Windows Version

/* from HELLO.C */
                                    ∙
                                    ∙
                                    ∙
     case WM_PAINT:
          BeginPaint(hWnd, (LPPAINTSTRUCT) &ps);
#ifdef OLD_CODE
          HelloPaint(ps.hdc);
#else
          StopSign(hWnd, ps.hdc);
#endif
          EndPaint(hWnd, (LPPAINTSTRUCT) &ps);
......................................................................

/* STOP.C */

#include "windows.h"

#define BORDER 5

BOOL FAR PASCAL Move(HDC hDC, int x, int y)
{
    unsigned long curr = GetCurrentPosition(hDC);
    return MoveTo(hDC, LOWORD(curr)+x, HIWORD(curr)+y);
}

BOOL FAR PASCAL Line(HDC hDC, int x, int y)
{
    unsigned long curr = GetCurrentPosition(hDC);
    return LineTo(hDC, LOWORD(curr)+x, HIWORD(curr)+y);
}

void StopSign(HWND hWnd, HDC hDC)
{
     RECT r;
     int scale;

     GetClientRect(hWnd, &r);
     r.bottom -= (2 * BORDER);
     r.right  -= (2 * BORDER);
     scale = min(r.bottom - r.top, r.right - r.left) / 18;

     OffsetViewportOrg(hDC, BORDER, BORDER);

    MoveTo(hDC, 5 * scale, 0);
    Line  (hDC, 8 * scale, 0);
    Line  (hDC, 5 * scale, 5  * scale);
                                    ∙
                                    ∙
                                    ∙
    Move(hDC, 0, -4  * scale);
    Line(hDC, 2  * scale, 0);
    Line(hDC, 0, 2  * scale);
    Line(hDC, -2  * scale, 0);

     OffsetViewportOrg(hDC, -BORDER, -BORDER);
}


Figure 5:  Take Two of Windows Version

/* from STOP.C */

void StopSign(HWND hWnd, HDC hDC)
{
    RECT r;
    int scale;

    GetClientRect(hWnd, &r);
    r.bottom -= (2 * BORDER);
    r.right  -= (2 * BORDER);
#ifdef USE_MM_TEXT
    scale = min(r.bottom - r.top, r.right - r.left) / 18;
#else
    scale = 1;
    SetMapMode(hDC, MM_ISOTROPIC);
    SetWindowExt(hDC, 18, 18);
    SetViewportExt(hDC, r.right, r.bottom);
#endif
                                    ∙
                                    ∙
                                    ∙
}


Figure 6:  Take Three of Windows Version

/* from HELLO.C */

RECT client_rect = {0};
                                    ∙
                                    ∙
                                    ∙
    case WM_PAINT:
        BeginPaint(hWnd, (LPPAINTSTRUCT) &ps);
        StopSign(ps.hdc);
        EndPaint(hWnd, (LPPAINTSTRUCT) &ps);
        break;
                                    ∙
                                    ∙
                                    ∙
    case WM_SIZE:
        /*
            more efficient way:
                client_rect.bottom = HIWORD(lParam);
                client_rect.right = LOWORD(lParam);
        */
        GetClientRect(hWnd, &client_rect);
        break;
.....................................................................

/* from STOP.C */

void StopSign(HDC hDC)             /* NEW CODE */
{
    extern RECT client_rect;
    RECT r;
    int scale;

    r = client_rect;        /* structure copy */

    r.bottom -= (2 * BORDER);
    r.right  -= (2 * BORDER);
                                    ∙
                                    ∙
                                    ∙


Figure 7:  Windows Version, Take Four

/* DRAW.H */

/* external interface to draw.c */
extern BOOL FAR PASCAL Move(HDC hDC, unsigned x, unsigned y);
extern BOOL FAR PASCAL Line(HDC hDC, unsigned x, unsigned y);
extern BOOL FAR PASCAL MyMoveTo(HDC hDC, unsigned x, unsigned y);
extern BOOL FAR PASCAL MyLineTo(HDC hDC, unsigned x, unsigned y);

#ifndef DRAW_C
#define MoveTo(hDC,x,y)       MyMoveTo((hDC),(x),(y))
#define LineTo(hDC,x,y)       MyLineTo((hDC),(x),(y))
#endif

.....................................................................

/* DRAW.C */

#include "windows.h"
#define DRAW_C
#include "draw.h"

/* not visible outside this module */
static unsigned curr_x=0, curr_y=0;

BOOL FAR PASCAL Move(HDC hDC, unsigned x, unsigned y)
{
     curr_x += x;
     curr_y += y;
     return MoveTo(hDC, curr_x, curr_y);
}

BOOL FAR PASCAL Line(HDC hDC, unsigned x, unsigned y)
{
     curr_x += x;
     curr_y += y;
     return LineTo(hDC, curr_x, curr_y);
}

BOOL FAR PASCAL MyMoveTo(HDC hDC, unsigned x, unsigned y)
{
     curr_x = x;
     curr_y = y;
     return MoveTo(hDC, x, y);
}

BOOL FAR PASCAL MyLineTo(HDC hDC, unsigned x, unsigned y)
{
     curr_x = x;
     curr_y = y;
     return LineTo(hDC, x, y);
}


Figure 8:  Portable Version of Stop Sign

/* from HELLO.C */
                 <...>
#include "canvas.h"
                 <...>
    /* in WinMain, after call to CreateWindow() */
    new_canvas((WINDOW) hWnd);
                 <...>
    /* in HelloWndProc... */
    HCANVAS hCanvas;
                 <...>
    case WM_PAINT:
#if 1
        hCanvas = find_canvas((WINDOW) hWnd);
        begin_update(hCanvas);
        StopSign(hCanvas);
        end_update(hCanvas);
#else
        BeginPaint( hWnd, (LPPAINTSTRUCT)&ps );
        StopSign(ps.hdc);
        EndPaint( hWnd, (LPPAINTSTRUCT)&ps );
#endif
        break;

    case WM_SIZE:
        resize(find_canvas((WINDOW) hWnd),
            LOWORD(lParam), HIWORD(lParam));
        break;

......................................................................

/* from MACHELLO.C */
...
#define MAC
#include "canvas.h"
                 <...>
    /* in main(), after call to NewWindow() */
    new_canvas((WINDOW) windowptr);
                 <...>
    case updateEvt:
        hCanvas = find_canvas(event.message);
        begin_update(hCanvas);
        StopSign(hCanvas);
        end_update(hCanvas);
        break;

......................................................................

/* STOP.C-uses CANVAS facility */

/* no windows.h */
/* no MacTypes.h, no QuickDraw.h */
#include "canvas.h"

#define BORDER      5

#define min(a,b)    ((a)<(b)?(a):(b))

void StopSign(HCANVAS hCanvas)
{
    short width, depth;

    get_size(hCanvas, &width, &depth);

    width -= (2 * BORDER);
    depth -= (2 * BORDER);

    set_scale(hCanvas, width, depth, 18);
    offset_org(hCanvas, BORDER, BORDER);

    move_to(hCanvas, 5, 0);
    line(hCanvas, 8, 0);
    line(hCanvas, 5, 5);
                                    ∙
                                    ∙
                                    ∙
    move(hCanvas, 0, -4);
    line(hCanvas, 2, 0);
    line(hCanvas, 0, 2);
    line(hCanvas, -2, 0);

    offset_org(hCanvas, -BORDER, -BORDER);
}


Figure 9:  Some CANVAS Routines, Extracted from ENVRNMT.C

/* canvas.h */

typedef unsigned HCANVAS;   /* handle to canvas */

#ifdef MAC
typedef void *WINDOW;           /* WindowPtr */
typedef void *HPIC;             /* PicHandle */
#define NEAR  /**/
#else
typedef unsigned short WINDOW;  /* HWND */
typedef unsigned short HPIC;    /* HANDLE */
#endif

HCANVAS new_canvas(WINDOW window);
HCANVAS find_canvas(WINDOW window);
void kill_canvas(HCANVAS hCanvas);
void move(HCANVAS hCanvas, short x, short y);
void line(HCANVAS hCanvas, short x, short y);
void move_to(HCANVAS hCanvas, short x, short y);
void line_to(HCANVAS hCanvas, short x, short y);
void get_xy(HCANVAS hCanvas, short *x, short *y);
void resize(HCANVAS hCanvas, short x, short y);
void get_size(HCANVAS hCanvas, short *x, short *y);
void offset_org(HCANVAS hCanvas, short x, short y);
void set_scale(HCANVAS hCanvas, short x, short y, short units);
void begin_update(HCANVAS hCanvas);
void end_update(HCANVAS hCanvas);
void debug(char *mask, ...);
void open_picture(HCANVAS hCanvas);
void close_picture(HCANVAS hCanvas, HPIC *hPic);
void draw_picture(HCANVAS hCanvas, HPIC hPic);
void kill_picture(HPIC hPic);

#define NUM_CANVASES        10

#define ILLEGAL_HCANVAS     0xFFFF
#define ILLEGAL_WINDOW      0

.....................................................................
/* canvas.c-abridged version of envrnmt.c */
/* just contains graphics-related routines for Mac and Windows */
/* Andrew Schulman  20-Oct-1988 */
/* revised by Ray Valdes 23-Oct-1988 */

#ifdef MAC
#include <unix.h>
#include "Quickdraw.h"
#include "WindowMgr.h"
#else
#include <memory.h>
#include "windows.h"
#endif

#include "canvas.h"

typedef struct {
    WINDOW window;
    short x,y;
    HPIC pic;
#ifdef MAC
    WindowRecord wRecord;
    short scale;
#else
    PAINTSTRUCT ps;             /* includes HDC */
    short right,bottom;
#endif
    unsigned long user_data;
    } CANVAS;

/* macros to simplify access to the environment block */
#define CANVAS_RIGHT(hCanvas)     (canvas[hCanvas].right)
#define CANVAS_BOTTOM(hCanvas)    (canvas[hCanvas].bottom)
#define CANVAS_X(hCanvas)         (canvas[hCanvas].x)
#define CANVAS_Y(hCanvas)         (canvas[hCanvas].y)
#define CANVAS_USERDATA(hCanvas)  (canvas[hCanvas].user_data)
#define CANVAS_WINDOW(hCanvas)
(canvas[hCanvas].window)
#define CANVAS_PIC(hCanvas)       (canvas[hCanvas].pic)
#ifdef MAC
#define CANVAS_GP(hCanvas)        ((GrafPtr)
CANVAS_WINDOW(hCanvas))
#define CANVAS_WP(hCanvas)        ((WindowPtr)
CANVAS_WINDOW(hCanvas))
#define CANVAS_SCALE(hCanvas)     (canvas[hCanvas].scale)
#define CANVAS_PORTRECT(hCanvas)  (CANVAS_WP(hCanvas)-
>portRect)
#else
#define CANVAS_PAINTSTR(hCanvas)  (canvas[hCanvas].ps)
#define CANVAS_HDC(hCanvas)
(CANVAS_PAINTSTR(hCanvas).hdc)
#define CANVAS_HWND(hCanvas)      ((HWND)
CANVAS_WINDOW(hCanvas))
#endif

CANVAS canvas[NUM_CANVASES] = {0};

/****************************************************************/
static void NEAR valid_canvas(hCanvas)
    HCANVAS hCanvas;
{
    if (hCanvas >= NUM_CANVASES)
        debug("No such hCanvas: %u", hCanvas);
#ifndef MAC
    /* Windows lets us do a little more error checking */
    if (! IsWindow(CANVAS_HWND(hCanvas)))
        debug("Illegal hWnd %u in hCanvas %u",
CANVAS_HWND(hCanvas), hCanvas);
#endif
}

/***************************************************************/
HCANVAS new_canvas(window)
    WINDOW window;
{
    register HCANVAS hCanvas;
    for (hCanvas=0; hCanvas < NUM_CANVASES; hCanvas++)
        if (CANVAS_WINDOW(hCanvas) == ILLEGAL_WINDOW)
        {
            CANVAS_WINDOW(hCanvas) = window;
            return hCanvas;
        }
    /* still here */
    return ILLEGAL_HCANVAS;
}

/* this is just temporary until we have application event queue */
HCANVAS find_canvas(window)
    WINDOW window;
{
    register HCANVAS hCanvas;
    for (hCanvas=0; hCanvas < NUM_CANVASES; hCanvas++)
        if (CANVAS_WINDOW(hCanvas) == window)
            return hCanvas;
    /* still here */
    return ILLEGAL_HCANVAS;
}

/*******************************************************************/
#ifdef MAC
#define memset(a,b,c)       setmem((a),(c),(b))
#endif

void kill_canvas(hCanvas)
    HCANVAS hCanvas;
{
    valid_canvas(hCanvas);
    /* set all fields to zero */
    memset(&canvas[hCanvas], 0, sizeof(CANVAS));
}

/*******************************************************************/
/* since Move,Line,MoveTo,LineTo are so similar, write them with macros */
#ifdef MAC
#define MAYBE_SWITCHING_PORT(hCanvas,FUNC,x,y)      \
{                                                   \
    if (CANVAS_GP(hCanvas) != thePort)              \
    {                                               \

        GrafPtr oldport = thePort;                  \
        SetPort(CANVAS_GP(hCanvas));                \
        FUNC(x * CANVAS_SCALE(hCanvas),             \
            y * CANVAS_SCALE(hCanvas));             \
        SetPort(oldport);                           \
    }                                               \
    else                                            \
        FUNC(x,y);                                  \
}
#else
#define UPDATING_XY(hCanvas,FUNC,OP,x,y)            \
{                                                   \
    if (! CANVAS_HDC(hCanvas))                      \
        return;                                     \
    CANVAS_X(hCanvas) OP x;                         \
    CANVAS_Y(hCanvas) OP y;                         \
    FUNC(CANVAS_HDC(hCanvas),                       \
        CANVAS_X(hCanvas), CANVAS_Y(hCanvas));      \
}
#endif

/*******************************************************************/
void move(hCanvas, x, y)
    HCANVAS hCanvas;
    short x,y;
{
    valid_canvas(hCanvas);
#ifdef MAC
    MAYBE_SWITCHING_PORT(hCanvas, Move, x, y);
#else
    UPDATING_XY(hCanvas, MoveTo, +=, x, y);
#endif
}

void line(hCanvas, x, y)
    HCANVAS hCanvas;
    short x,y;
{
    valid_canvas(hCanvas);
#ifdef MAC
    MAYBE_SWITCHING_PORT(hCanvas, Line, x, y);
#else
    UPDATING_XY(hCanvas, LineTo, +=, x, y);
#endif
}

void move_to(hCanvas, x, y)
    HCANVAS hCanvas;
    short x,y;
{
    valid_canvas(hCanvas);
#ifdef MAC
    MAYBE_SWITCHING_PORT(hCanvas,MoveTo,x,y);
#else
    UPDATING_XY(hCanvas, MoveTo, =, x, y);
#endif
}

void line_to(hCanvas, x, y)
    HCANVAS hCanvas;
    short x,y;
{
    valid_canvas(hCanvas);
#ifdef MAC
    MAYBE_SWITCHING_PORT(hCanvas,LineTo,x,y);
#else
    UPDATING_XY(hCanvas, LineTo, =, x, y);
#endif
}

void get_xy(hCanvas, x, y)
    HCANVAS hCanvas;
    short *x, *y;
{
    valid_canvas(hCanvas);
#ifdef MAC

    *x = CANVAS_GP(hCanvas)->pnLoc.h *
CANVAS_SCALE(hCanvas);
    *y = CANVAS_GP(hCanvas)->pnLoc.v *
CANVAS_SCALE(hCanvas);
#else
    *x = CANVAS_X(hCanvas);
    *y = CANVAS_Y(hCanvas);
#endif
}

/*******************************************************************/
#ifdef MAC
#define SCROLL_BAR_WIDTH        16
#endif

void resize(hCanvas, x, y)
    HCANVAS hCanvas;
    short x,y;
{
    valid_canvas(hCanvas);
#ifdef MAC
    /* nothing */
#else
    CANVAS_RIGHT(hCanvas) = x;
    CANVAS_BOTTOM(hCanvas) = y;
#endif
}

void get_size(hCanvas, x, y)
    HCANVAS hCanvas;
    short *x, *y;
{
    valid_canvas(hCanvas);
#ifdef MAC
    {
        Rect r;
        r = CANVAS_PORTRECT(hCanvas);  /* structure copy */
        *x = (r.right - r.left) - SCROLL_BAR_WIDTH;
        *y = (r.bottom - r.top) - SCROLL_BAR_WIDTH;
    }
#else
    *x = CANVAS_RIGHT(hCanvas);
    *y = CANVAS_BOTTOM(hCanvas);
#endif
}

void offset_org(hCanvas, x, y)
    HCANVAS hCanvas;
    short x,y;
{
    valid_canvas(hCanvas);
#ifdef MAC
    if (CANVAS_GP(hCanvas) != thePort)
    {
        GrafPtr oldport = thePort;      /* or use GetPort */
        SetPort(CANVAS_GP(hCanvas));
        SetOrigin(CANVAS_PORTRECT(hCanvas).left + x,
            CANVAS_PORTRECT(hCanvas).top + y);
        SetPort(oldport);
    }
    else
        SetOrigin(CANVAS_PORTRECT(hCanvas).left + x,
            CANVAS_PORTRECT(hCanvas).top + y);
#else
    if (! CANVAS_HDC(hCanvas))
        return;
    OffsetViewportOrg(CANVAS_HDC(hCanvas), x, y);
#endif
}

void set_scale(hCanvas, x, y, units)
    HCANVAS hCanvas;
    short x,y,units;
{
    valid_canvas(hCanvas);
#ifdef MAC
    CANVAS_SCALE(hCanvas) = min(x,y) / units;
#else

    if (! CANVAS_HDC(hCanvas))
        return;
    SetMapMode(CANVAS_HDC(hCanvas), MM_ISOTROPIC);
    SetWindowExt(CANVAS_HDC(hCanvas), units, units);
    SetViewportExt(CANVAS_HDC(hCanvas), x, y);
#endif
}

/*******************************************************************/
void begin_update(hCanvas)
    HCANVAS hCanvas;
{
    valid_canvas(hCanvas);
#ifdef MAC
    BeginUpdate(CANVAS_WP(hCanvas));
#else
    BeginPaint(CANVAS_HWND(hCanvas), &CANVAS_PAINTSTR(hCanvas));
    /* CANVAS_HDC() is set via CANVAS_PAINTSTR() */
#endif
}

void end_update(hCanvas)
    HCANVAS hCanvas;
{
    valid_canvas(hCanvas);
#ifdef MAC
    EndUpdate(CANVAS_WP(hCanvas));
    DrawGrowIcon(CANVAS_WP(hCanvas));
#else
    EndPaint(CANVAS_HWND(hCanvas), &CANVAS_PAINTSTR(hCanvas));
    CANVAS_HDC(hCanvas) = 0;        /* important! */
#endif
}

/*******************************************************************/
void open_picture(hCanvas)
    HCANVAS hCanvas;
{
    valid_canvas(hCanvas);
#ifdef MAC
    CANVAS_PIC(hCanvas) = (HPIC) OpenPicture(CANVAS_PORTRECT(hCanvas));
#else
    CANVAS_PIC(hCanvas) = CANVAS_HDC(hCanvas);      /* save old DC */
    CANVAS_HDC(hCanvas) = CreateMetaFile(NULL);
#endif
}

void close_picture(hCanvas, hPic)
    HCANVAS hCanvas;
    HPIC *hPic;
{
    valid_canvas(hCanvas);
#ifdef MAC
    ClosePicture();
    *hPic = CANVAS_PIC(hCanvas);
#else
    *hPic = CloseMetaFile(CANVAS_HDC(hCanvas));
    CANVAS_HDC(hCanvas) = CANVAS_PIC(hCanvas);     /* restore old DC */
#endif
}

void draw_picture(hCanvas, hPic)
    HCANVAS hCanvas;
    HPIC hPic;
{
    valid_canvas(hCanvas);
#ifdef MAC
    /* perhaps use this for ANISOTROPIC scaling */
    /* may have to switch grafPorts back and forth */
    DrawPicture(hPic, CANVAS_PORTRECT(hCanvas));
#else
    PlayMetaFile(CANVAS_HDC(hCanvas), hPic);
#endif
}

void kill_picture(hPic)
    HPIC hPic;
{
#ifdef MAC
    KillPicture(hPic);
#else
    DeleteMetaFile(hPic);
#endif
}

/*******************************************************************/

#include <stdio.h>
#ifdef MAC
typedef char *va_list;
#define va_start(ap,v)      ap = (va_list)&v + sizeof(v)
#else
#include <stdarg.h>
#endif

void debug(mask)
    char *mask;
{
    char buf[255];
    va_list args;

    va_start(args, mask);
    vsprintf(buf, mask, args);
#ifdef MAC
    ParamText(CtoPStr(buf), 0L, 0L, 0L);
    StopAlert(RESOURCE_ID, 0L);
#else
    MessageBox(GetActiveWindow(), buf, "Debug!", MB_OK);
#endif
}


Figure 10:  Using Pictures and Multiple Windows

/* from Windows HELLO.C */
                                    ∙
                                    ∙
                                    ∙
static int closed_canvases=0;
                                    ∙
                                    ∙
                                    ∙
    char buf[80];
    register int i;
    for (i=0; i<NUM_CANVASES; i++)
    {
        sprintf(buf, "Window #%u", i+1);
        hWnd = CreateWindow((LPSTR)szAppName,
                            (LPSTR)buf,
                            WS_TILEDWINDOW,
                            CW_USEDEFAULT,0,CW_USEDEFAULT,0,
                            (HWND)NULL,        /* no parent */
                            (HMENU)NULL,       /* use class menu */
                            (HANDLE)hInstance, /* handle to window instance *
                            (LPSTR)NULL        /* no params to pass on */
                            );

        (void) new_canvas((unsigned long) hWnd);
                                    ∙
                                    ∙
                                    ∙
    }
                                    ∙
                                    ∙
                                    ∙
    case WM_DESTROY:
        kill_canvas(find_canvas((unsigned long) hWnd));
        closed_canvases++;
        if (closed_canvases == NUM_CANVASES)
        {
            KillStopSign();
            PostQuitMessage(0);
        }
        break;
                                    ∙
                                    ∙
                                    ∙
...................................................................

/* from STOP.C-version uses pictures */
                                    ∙
                                    ∙
                                    ∙
static HPIC hPic=0;

void StopSign(HCANVAS hCanvas)
{
                                    ∙
                                    ∙
                                    ∙
    if (! hPic)
    {
        open_picture(hCanvas);
        move_to(hCanvas, 5, 0);
        line(hCanvas, 8, 0);
                                    ∙
                                    ∙
                                    ∙
        line(hCanvas, -2, 0);
        close_picture(hCanvas, &hPic);
        debug("picture created: %u", hPic);
    }

    draw_picture(hCanvas, hPic);
                                    ∙
                                    ∙
                                    ∙
}

void KillStopSign()
{
    kill_picture(hPic);
    debug("picture killed: %u", hPic);
}


Figure 11:  Macintosh EventRecord vs. Windows MSG

/* from EventMgr.h */
typedef struct {
    int     what;
    long    message;
    long    when;
    Point   where;
    int     modifiers;
    } EventRecord;

/* from windows.h */
typedef struct tagMSG {
    HWND  hwnd;
    WORD  message;
    WORD  wParam;
    LONG  lParam;
    DWORD time;
    POINT pt;
    } MSG;


Figure 12:  Environment-Independent Memory Functions

NULL_EVENT
QUIT_EVENT
CMD_EVENT
MOUSEDOWN_EVENT
MOUSEUP_EVENT
OPENVIEW_EVENT
CLOSEVIEW_EVENT
MOVEVIEW_EVENT
RESIZEVIEW_EVENT
ACTIVATEVIEW_EVENT
UPDATEVIEW_EVENT
CHAR_EVENT


Figure 13:  Environment-independent memory allocation functions from
            ENVRNMT.C.

#ifdef MAC
typedef char **     Handle; /* Macintosh handle */
typedef char *      MEMPTR;
typedef MEMPTR *    MEMBLOCK;
#else
typedef short       HANDLE; /* Windows handle */
typedef char far *  MEMPTR;
typedef HANDLE      MEMBLOCK;
#endif
typedef long        MEMSIZE;

MEMBLOCK    env_AllocMem(size)
MEMSIZE     size;
{
#ifdef MAC
    return (MEMBLOCK) NewHandle((Size) size);
#else
    return GlobalAlloc(GMEM_MOVEABLE, size);
#endif
}

VOID        env_ReAllocMem(handle,size)
MEMBLOCK *  handle;
MEMSIZE     size;
{
#ifdef MAC
    SetHandleSize((Handle)*handle,(Size)size);
#else
    *handle = GlobalReAlloc(*handle,size,GMEM_MOVEABLE);
#endif
}

MEMPTR      env_GraspMem(handle)
MEMBLOCK    handle;
{
#ifdef MAC
    HLock((Handle)handle);
    return (MEMPTR) *handle;
#else
    return (MEMPTR) GlobalLock(handle);
#endif
}

VOID        env_UnGraspMem(handle)
MEMBLOCK    handle;
{
#ifdef MAC
    HUnlock((Handle) handle);
#else
    GlobalUnlock((HANDLE) handle);
#endif
}

VOID        env_FreeMem(handle)
MEMBLOCK    handle;
{
#ifdef MAC
    DisposHandle((Handle) handle);
#else
    GlobalFree((HANDLE) handle);
#endif
}

MEMSIZE     env_CompactMem(size_wanted)
MEMSIZE     size_wanted;
{
#ifdef MAC
    return (MEMSIZE) CompactMem(size_wanted);
#else
    return (MEMSIZE) GlobalCompact(size_wanted);
#endif
}


Figure 14:  Nonportable Version of AddGraphicsObjectToModel

OBJECTID   AddGraphicsObjectToModel( data_model , object_attributes)
    HMODEL  data_model;     /* handle to a data_model */
    ATTR    object_attributes;  /* attributes of new graphics object */
    {
        /* id is an index into the data model, represents identifier
         * for newly created graphics object
         */
    OBJECTID    id;

        /* id of new graphics object, and post-increment
         * the number of objects in this data_model
         */
    id = (**data_model).num_objects++;

        /* grow the data_model to contain the new graphics object
         */
    env_ReAllocMem( (Handle) data_model,
    (MEMSIZE) (sizeof(int) + ((**data_model).num_objects *
                               sizeof(OBJ))));

        /* call  MakeGraphicsObject()  with a pointer to the
         * new graphics object
         */
    MakeGraphicsObject( &((**data_model).objects[i]),
                              object_attributes);

        /* return the id of the newly created graphics object */
    return id;
}


Figure 15:  Portable Version of AddGraphicsObjectToModel

OBJECTID   AddGraphicsObjectToModel( data_model , object_attributes)
    HMODEL *    data_model; /* not a handle, but pointer to handle */
    ATTR        object_attributes;
    {
    OBJECTID    id;
    PMODEL      m;  /* a pointer to data model */

        /* get pointer to data_model, and lock data_model in memory */
    m = (PMODEL) env_GraspMem(*data_model);

        /* get id of graphics object, post-increment num_objects */
    id = (m->num_objects)++;

        /* unlock the data_model so that we can resize it */
    env_UnGraspMem(*data_model);

        /* resize data_model, passing pointer to handle in case
         * the value of the handle needs to be changed by ReAlloc
         */
    env_ReAllocMem( (MEMBLOCK *) data_model,
               (MEMSIZE) (sizeof(int) + ((id + 1) * sizeof(OBJ))));

        /* lock the data_model again, and get a pointer to it */
    m = (PMODEL) env_GraspMem(*data_model);

        /* call the MakeGraphicsObject() routine with pointer to
         * new graphics object. The argument is of type POBJECT,
         * rather than OBJECT *, to compensate for segmented
         * memory referencing on Intel CPUs.
         */
    MakeGraphicsObject((POBJECT)&(m->objects[id]), object_attributes);

        /* unlock data_model */
    env_UnGraspMem(*data_model);

        /* return id of newly created graphics object */
    return id;
}


Figure 16:  Typedef'd Data Mode

typedef struct{
   int num_objects;    /* number of graphic objects */
   OBJ objects[1];     /* variable length array */
   } MODEL;
typedef Handle HMODEL; /* a handle to data model */


Figure 17:  Using env_AllocMem to Dynamically Allocate a New Data Model

HMODEL NewModel
{
   HMODEL  hModel; /* handle to data model */
   MODEL * pModel; /* pointer to data model */
   hModel = (HMODEL)  env_AllocMem(sizeof(MODEL));
   pModel = (MODEL *) env_GraspMem(hModel); /* lock */
   pModel->num_objects = 0;
   env_UnGraspMem( hModel ); /* unlock */
   return hModel;
}


Figure 18:  Allocating the New Pointer to Model Type

HMODEL NewModel
{
   HMODEL  hModel; /* handle to data model */
   PMODEL  pModel; /* pointer to data model */
   hModel = (HMODEL)  AllocMem( sizeof(MODEL) );
   pModel = (PMODEL)  GraspMem( hModel );
   /* lock mem object */
   pModel->num_objects = 0;
   UnGraspMem( hModel ); /* unlock memory object */
   return hModel;
}


Developing Applications with Common Source Code for Multiple Environments

───────────────────────────────────────────────────────────────────────────
Also see the related article:
  Development Tools for the Macintosh
───────────────────────────────────────────────────────────────────────────

Michael Geary☼

A tale of three systems could easily be told of software written for one
system and ported to another. "It was the best of programs, it was the worst
of programs..." Writing a program that will run on more than one kind of
machine or operating system isn't easy, especially if it's supposed to run
on Presentation Manager (hereafter referred to as PM), Windows, and the
Macintosh(R). Here we have three vastly different windowing/operating
environments running on three different operating systems on two
incompatible hardware platforms. Yet the three have much in common.
Windows and Presentation Manager share a common user interface, and the
Mac(R) user interface is quite similar. This might lead one to expect──or at
least hope for──some similarities in the programming environment, and
there are some. The three systems do provide many of the same general
features, but they are implemented in very different ways.

Windows and PM aren't too far apart; even though the detailed coding isn't
the same between them, PM's ancestry shows quite clearly. There is a direct
correspondence between many of the functions and messages in the two, and
overall program architecture is, or can be, virtually identical. There are
some major differences, to be sure. Presentation Manager applications can
take advantage of all the features of OS/2, including things that have been
on many a Windows programmer's wish list: true preemptive multitasking,
multiple threads of execution, named pipes. But PM doesn't have every
feature of Windows. One glaring omission is the multiline edit control. In
Windows you just set a style bit and you get a decent little text
editor-the same one used in Notepad, in fact. If you want this in
Presentation Manager, you'll have to code it yourself. Still if you stick to
their common features, Windows and PM are similar enough to make it
relatively easy to port code between them.

The Mac Toolbox (that is, the set of function calls available in the Mac's
ROM and System files) is a horse of a different color. Besides taking a
different approach to many things, the interface is generally built at a
lower level than its PC counterparts. While Mac applications are event-
driven, they don't really have the message-based architecture that is one
of the strengths of Windows and PM. Mac applications have to fuss around
with a lot of details to do things that are simple in the other two systems.
For example, it's easy to create a standard Mac window complete with a title
bar, close box, and zoom box-but if you want the user to actually be able to
move, size, or zoom the window, or even to close it, you've got some code to
write. When the mouse-down event message comes in, the application must
determine where the mouse was pressed (albeit with a simple function call)
and then call the proper function to manipulate the window. In Windows and
PM this is done for you by DefWindowProc or the frame window and control
classes, and the application is notified of the new window status via a
message which can be processed or ignored. This makes simple
programs simpler, while still giving programmers the opportunity to
customize this kind of window handling.

This particular difference in architecture complicates the portability issue
in more ways than just difficulty of programming. Some kinds of programs
are impossible to write on the Macintosh except by resorting to kludges that
can't be guaranteed to work in all cases. The toughest are utility
programs that manipulate the windows of other applications. Writing my Tiler
program was a simple job under Windows and would be just as easy under PM
(notwithstanding the fact that Tiler's functionality is already part of
the PM shell). All I had to do, after getting the other applications'
window handles and determining where the windows should be placed, was to
call SetWindowPos for every window. (In PM, just one WinSetMultWindowPos
call would do them all with less screen activity.) The interesting thing is
that Tiler works fine even with applications like Clock, whose window
contents are size dependent. Their windows receive WM_MOVE and WM_SIZE
windows, just as if the user moved or sized them, so any position- or size-
dependent calculations happen as a matter of course.

Several Mac applications do have tiling and other window arrangement
options-but only for their own windows. I don't know of any general-purpose
tiling desk accessory for the Macintosh. The Mac does have MoveWindow
and SizeWindow functions, but if a desk accessory turned them loose on an
application's windows, things would not go as planned. There is a big
difference between these functions and their equivalents in Windows or PM.
On the Mac, there is no WM_MOVE or WM_SIZE message; in fact, there isn't
even a window function that these messages can be sent to. In the normal
course of events the MoveWindow and SizeWindow functions are called only by
the application itself, whether because of mouse clicks or a menu item. The
only reason the application knows it must deal with a new window size is
because the application itself just called those functions. A Tiler on the
Mac would probably move the windows correctly, but it would not do any size-
related calculations. For example, scroll bars would remain at their old
relative positions instead of being adjusted to fit the new window size.

Even MultiFinder(TM) has to resort to various kinds of tricks to get its job
done. There is a revealing discussion of these kinds of struggles from an
insider's point of view in Phil Goldman's "MultiFinder Revealed," in the
August 1988 BYTE.


Mac Advantages

Just when you think you're involved in too many system details, you
discover some of the Mac's greatest strengths. For example, the Mac
Toolbox has hooks that let an application substitute its own code for the
normal Toolbox code. Windows and PM, of course, have a set of message hook
functions, and window functions themselves serve this same purpose. But on
the Mac, the intercept functions, called defprocs, are found in many more
places. For example, QuickDraw(TM) allows you to replace any of its lowest-
level drawing functions with your own, and you can give the Control Manager
a defproc to define special controls, like the speaker volume slider in the
Control Panel. The Control Manager takes care of mouse tracking and other
details, calling back to the defproc to do things like draw the slider and
its background. In Windows or PM you would have to implement this kind of
control yourself, with much less help from the operating environment.

The Mac provides a higher level of support in other areas, too. There are
Toolbox calls to put up standard file open and save dialogs, a far cry from
the situation in PM and Windows where each application implements its own
dialog for these functions. This fact made it possible for Apple to
introduce the Hierarchical File System right beside the Mac's old flat file
system, without breaking too many existing applications. Imagine the
situation if each application had implemented its own file dialogs; there
would have been no method built into them to switch directories. Since most
applications used the standard file dialogs, an update automatically
makes these applications reasonably current. Furthermore, this paves the
way for slick little utilities like Findswell. This program adds an extra
button to the standard file dialog in most applications; pressing this
button lets the directories on a disk be searched for a file name. Just
imagine trying to write a utility like this for Windows or PM, where each
application is burdened with providing its own file open and save
dialogs. No way!

As good as the Mac Toolbox is, it just doesn't have either the elegance of
a message-based architecture or OS/2 niceties like threads; Mac programmers
find themselves getting "down and dirty" more often than PM or Windows
programmers, I suspect. This brings out another strength of the Mac: its
more open architecture and documentation. Windows and PM prevent dirty
programming tricks by not documenting their internal architecture,
providing only high-level functions to manipulate things like window
structures. This may keep application programmers "well behaved," but it
makes life difficult when the facilities provided don't do the job.
Furthermore, this philosophy makes it hard to debug competently. The times
when my code crashed in Windows, I would have given my eyeteeth to know the
details of a window structure or the ever-mysterious Burgermaster, just to
give me a fighting chance of figuring out what my code was doing wrong.

The Mac and its documentation take an entirely different approach.
Inside Macintosh documents nearly all the internal structures used by the
Mac Toolbox, which are closely guarded secrets in PM and Windows. There are
lots of suggestions concerning what might change in future systems, but at
the top of my list is the suggestion that programmers get every bit of
information that might be useful. This would bother a traditional
operating system designer, because it makes it easy for the application
programmer to take control at any level. It's possible to run wild with
this and write a program that will break with every new system software
release, but on the Mac you can dig into a lot of the internal data
structures and still be "well-behaved." This openness has helped make many
Mac applications and utilities possible. Windows Version 2.03 did open up
the Windows architecture considerably, and Presentation Manager continues
that improvement.


Portability Issues

The deeper an application digs into the internal workings of any system, the
harder it is to port. This is true in traditional programming environments
as well as the complex windowing systems we're dealing with here. In the
worst case, it may be necessary to recode the entire application for each
system-a brute force method of porting, but sometimes there is just no
choice. One situation that can force complete recoding is the lack of a
common programming language. Fortunately several good C compilers are
available on the Mac, and C is the language of choice for Windows and PM
applications. The Mac Toolbox was designed to be called directly from
programs written in Lisa(R) Pascal, so it follows the calling sequence and
data formats of Lisa Pascal. Pascal is still very popular on the Mac, but
there are too many differences between the Pascal compilers on the different
machines to make it a good choice for portability.

If C++ were available for developing Windows, PM, and Mac applications, I'd
use it instead of C. With inline functions, classes, and inheritance──and
all its other object-oriented facilities──C++ is more than an incremental
improvement over C for writing portable programs. Given a good library of
classes, C++ could hide the underlying environments much more
conveniently, and probably more efficiently, than C. But C++ compilers
aren't yet available for every system we are considering, so C it is for
now.

Even though C doesn't help much with the differences in Application
Programming Interfaces (APIs) and application structure, it does a
reasonable job of letting you write code that will run on different
systems. There are still a few pitfalls, and the Pascal orientation of the
Mac Toolbox (and to a lesser degree of OS/2 and Windows) introduces
additional problems. The "traditional" portability issues have been well
covered in the literature, so I'll just touch on a few.

Byte and word ordering. The Motorola(R) 68000 and the Intel(R) x86 series
processors order their data in opposite directions. The 68000 is a "big
endian" machine, that is, the high-order byte or word comes before the low-
order one. The x86 machines are "little endian," with just the opposite data
ordering. This affects any binary files meant to be portable between
machines, along with data structure definitions and data access macros such
as HIWORD and LOWORD in Windows.

Number lengths. Is an "int" a 16-bit or 32-bit value? This problem is
easily avoided by never using the native C data types and instead typedefing
everything and using the new types only. A good convention is to typedef all
the C types with the same name in uppercase, for example, typedef short
SHORT. Then, for all code that interfaces with the underlying system, use
SHORT or LONG instead of INT, where SHORT is 16 bits and LONG is 32 bits.

ANSI C vs. K&R C. In the beginning everyone used Kernighan and Ritchie, but
now ANSI is coming into widespread use. Microsoft(R) C, used for most
Windows and PM applications, is nearly a full ANSI standard compiler
(granted that the ANSI standard is still in draft form). None of the Mac C
compilers are quite up to speed in this area. MPW(TM) C and Lightspeed(R) C
do include some of the more useful ANSI features such as prototypes and
the wonderful vsprintf function, but the implementations aren't as
complete. MPW C even has the chutzpah to accept prototypes syntactically
but fail to do any of the type checking implied by them. Hopefully updates
will remedy these kinds of deficiencies, but meanwhile it helps to use the
newer ANSI features sparingly.

C vs. Pascal calling sequences. Since the Mac Toolbox was written to be
called from Pascal, the popular Mac C compilers offer a choice between
Pascal and C calling sequences. Microsoft C also provides both kinds of
calling sequences, but its Pascal calling sequence is somewhat different
from the Mac's. Both provide for the main difference, parameters being
pushed left-to-right instead of right-to-left as in the C calling sequence,
and the pascal keyword is used in each case to switch to the Pascal calling
sequence. However, the pascal keyword has to be used in different places.
In Lightspeed C and MPW C, you would declare a pascal-sequence function
returning a long like this:

  pascal long foo( void );

Microsoft C, on the other hand, requires this:

  long pascal foo( void );

This difference makes the pascal keyword rather useless for porting, unless
you do something really clunky like this:

  #ifdef MICROSOFT_C
  #define P1
  #define P2 pascal
  #else  /* Macintosh */
  #define P1 pascal
  #define P2
  #endif
  P1 long P2 foo( void );

Rather than going to these lengths, it may be best to avoid the Pascal
calling sequence except for code that directly interfaces to the Mac
Toolbox or to the PM or Windows API functions. This should only cause
problems in code that assumes one calling sequence or the other. PMWIN.H,
the PM's header file, does have a number of structure definitions and macros
that assume the Pascal calling sequence.

C strings vs. Pascal strings. Nearly all the Mac Toolbox functions expect
strings to be in the Lisa Pascal format: a length byte followed by that many
bytes of text. (Pascal strings are limited to 255 bytes.) Contrariwise,
Windows, OS/2, and the C compilers on both machines use strings in the
zero-terminated C format (ASCIIZ). The Mac C compilers provide various ways
of dealing with this, both with conversion functions and, in some compilers,
with a 'p' prefix for string constants; "pString" would be the same thing
as "String", but in Pascal format. Strings loaded from resources are the
native type for each environment: Pascal on the Mac, C on Windows and PM.


User Interface Portability

When porting code between different windowing systems, the traditional
portability problems are really the least of our worries. Since programs on
these systems are so API-intensive and the APIs are so different, the
code that talks directly to the underlying environment must change for each
system (except for the possibility mentioned above, of using macros to make
PM and Windows look more alike). And the Mac's user interface, though
similar to Windows and PM, is not the same, though you can get them them to
be as similar as you want, depending on how much code you want to write.
After all, there's a bitmap graphics engine underneath each of these
systems, and you can draw whatever you want on the screen and interact
with the user however you choose, regardless of whether it fits the user
interface conventions of the system that you're on.

The only reason I mention this is that people have tried it. Don't. On the
Mac, your application should look and feel like a Mac application; on
Windows and PM, it should look and feel like a Windows/PM application. Then,
within those constraints, your Mac and Windows/PM versions should be as
similar as possible. Trust me on this. Or if you don't trust me, read the
reviews of Mac products that are "un-Mac-like," or of Windows/PM packages
that have their own ideas about user interaction. That's not to say that
logical extensions to either user interface aren't worthwhile, but
remember that Mac users will also be using other Mac software, just as
Windows and PM users will use other software for those systems. (Of course,
this doesn't apply to true standalone turnkey applications, where the
computer is dedicated to running specific software.)


Child Windows

What differences in these user interfaces do we have to consider? Most
significant are child windows, the appearance and features of a standard
document window, and the use of menus.

Windows and PM have child windows that can be nested to any level, overlap,
and be manipulated just like top-level windows. Many applications use these
invisibly as a programming convenience, but the Multiple Document Interface
(MDI) uses child windows visibly and extensively.

The Mac has no child windows. It is possible to build them, but this
involves a lot of reinventing the wheel. Fortunately the Mac's user
interface convention corresponding to MDI doesn't involve child windows. In
MDI, there is an application workspace window that contains the
application's menu bar, and document windows are created as child windows in
this window. Mac applications move everything up one level, as it were; they
take over the whole screen, with the application menu bar at the top, and
document windows are top-level Macintosh windows. There is a
straightforward path for porting multiple-document applications between the
Mac, Windows, and PM; use MDI on the latter two and the standard Mac
conventions on the Mac.

The Mac does have controls corresponding to all the control window classes
in Windows, although not to the new frame controls added in PM. These are
not implemented as child windows, but through a separate control manager
and other pieces of code. Although they are more difficult to program, these
controls are generally more flexible and powerful than the child window
controls in Windows or PM. In particular, the text editor and list manager
go far beyond their counterparts in PM and Windows.


Menus

Menus are quite different among the three systems. Although Windows and
Presentation Manager nominally have the same user interface, some things
like menus really aren't the same; they are just used in the same way by
convention.

Under Windows, top-level windows can have menu bars, but child windows
cannot, since the child window ID (dialog ID) and the menu handle are
overloaded into the same field in Windows' internal window structure. Each
item in the menu bar can be an actual menu item or have a pull-down menu
associated with it, and top-level windows and child windows can each have
Control (System) menu icons, with the associated pull-down menus. Menu items
in a menu bar or a pull-down menu can be either text or bitmaps.

A Presentation Manager menu is no more than a control window class. As with
any other window class, you can create a menu window at any level of the
window hierarchy. Menu bars and pull-down menus are just two variations on
the same menu window class. The standard frame window in PM creates a number
of different menu windows to implement the standard PM/Windows interface;
the menu bar is a menu window, the Control menu icon is another menu window,
and the Minimize/Maximize icons make up a third menu window. (It seemed
strange to me at first to think of those icons as menus, but after all a
menu can include bitmaps, and these icons are just like the system menu
icon, but without pull-down menus.) The various pull-down menus are
themselves menu windows, invisible (and in fact children of the
HWND_DESKTOP window) until needed for display. But PM menus are not limited
to this standard usage-except in the user interface guidelines.

The Macintosh originally had one menu bar at the top of the screen, with a
pull-down menu for each menu item. By convention, the first item of every
application is a tiny Apple(R) logo, whose pull-down menu is analogous in
spirit if not in function to the Control menu of a Windows or PM
application. Menu bar items are text only, in the standard system font (the
Apple logo is simply a character in the Chicago font). Pull-down menus are
normally text, but an application can supply a menu defproc to vary a pull-
down menu's standard appearance.

Mac applications generally adhere to this basic menu structure, but over
the last few years the Macintosh menu manager has been enhanced in several
ways. Pop-up menus can appear anywhere on the screen, and with a little work
these can simulate PM menus. Mac applications have begun to use these,
especially for multiple-choice items in dialog boxes. Hierarchical menus
let pull-down menu items easily call forth an additional complete pull-down
menu. Menus too long to fit on the screen now automatically scroll as needed
during menu selection. Some new applications, notably HyperCard(R), have
implemented tear-off menus. Especially convenient for tool or pattern
palettes, these are pull-down menus that can easily be dragged to other
screen locations, where they remain visible as separate palette windows.

These features are great, but portability in this case leads to a lot of
work or to the lowest common denominator-something like Windows crossed
with the original Mac menu structure. Just as multiple-document
applications can use either MDI or the Mac's menu bar, a single-document
application in Windows can take its main window's menu bar and make it the
menu bar on the Mac. Windows applications having menus in more than one
window require some reworking. Also, applications can't count on scrolling
menus; any menu that might be too long, such as a Font menu, must be checked
against the screen height. If it's not going to fit, a list box or something
else should be used.

Aside from the menu appearance and location, the actual menu items used
can be very similar between the Mac and both Windows and PM. An application
should have a File menu and an Edit menu, followed by additional
application-specific menus. The standard File and Edit menus are nearly
identical across systems; the main exception is the different use of
Command-key and Alt-key shortcuts, which is easily resolved by maintaining
separate resource files for each system. And in this case that's as easy
as trying to convert resource files back and forth.

There are some differences in standard menus. For example, on the Mac, the
About... box is at the top of the Apple menu, followed by the desk
accessories and the MultiFinder menu. In most Windows and PM applications,
this migrates to the bottom of the File menu, right below the Exit item that
replaces the Mac's Quit item. This Exit item violates the IBM(R) Common
User Access (CUA) standard, which calls for an Exit menu in the menu bar,
with Exit and Resume items in its pull-down menu, and frankly, this is one
violation that is necessary.


Document Windows and Dialog Boxes

Windows and PM have a common style for standard document windows and
dialog boxes, although in each case there are other options. Modal dialog
boxes on the Mac are virtually identical to their Windows and PM
counterparts, but standard document windows are somewhat different. The
title bar is the same, except a close box replaces the Control menu icon,
providing only its Close function, and a zoom box replaces only the Maximize
icon. There is nothing to replace the Minimize icon, since Mac application
windows do not normally have an iconic state. There is no thick frame for
window sizing──just the size box in the lower right corner. Scroll bars, if
present, work identically. Interestingly, none of these differences really
affect portability; they affect visual appearance and the exact mouse (or
keyboard) interface, but provide essentially the same functionality
except for the lack of an iconic state.

As to the contents of a document window or dialog box, dialog boxes are
similar across all three systems. Although recent Mac dialog boxes have
started to use pop-up menus and other new interface features, the typical
Windows or PM dialog box would look at home on the Mac and vice versa. This
includes both modal and modeless dialog boxes. (There is no distinction
between system modal and application or window modal on the Mac; under
MultiFinder, modal dialog boxes are all system modal.)

One feature of Windows and PM dialog boxes not often seen on the Mac is the
Alt-letter shortcuts for navigating around the box. This is one case where
CUA improved on the original Windows and Mac user interfaces; in a large
dialog box the Alt-letters can be very convenient. They are worth
implementing on the Mac, although there isn't any built-in support for
them.

The content of document windows is for the most part application
dependent. Fortunately, the same kinds of user interface techniques are
appropriate on all three systems. Users expect to find virtually the same
keyboard and mouse interface on the Mac as in Windows and PM. Applications
for the latter systems usually have a more substantial keyboard interface
since they must work without a mouse.


Portability Techniques

"Vanilla" applications with relatively standard document windows, dialog
boxes, and menus are the easiest to port. Specialized programs like Tiler,
Spellswell(TM)──or most Mac desk accessories for that matter-are tougher.
But in the context of using windows and menus in fairly standard ways,
there's room to build a competitive, high quality application──vanilla with
all the toppings. Packages like PageMaker(R) show that it's possible to move
lots of code among these systems with a high degree of user interface
refinement and compatibility. It's just a small matter of programming...

But wait, we're supposed to design before we code, right? We had better
figure out what approach to use. Coding in C gives us a reasonable degree
of language portability, but what's the best way to deal with the
different APIs involved? There isn't really one best way; it depends on the
situation.

Besides maintaining separate source code for each system, another kind of
brute force technique is to keep common source code but use a zillion
#ifdef's to isolate machine specific code. This is usually a bad bet; it
ends up harder to read and maintain than separate source.

Macros are a fine tool for isolating system dependencies. It's a pity C's
macro processor is so limited, because it gives just a taste of the kinds of
things that can be done with macros. Even with its limitations, the C
preprocessor lets you define simple text-substitution macros with
parameters, and that alone is plenty. The Presentation Manager Conversion
Guide includes a comprehensive example of using macros to port the Cardfile
program from Windows to PM. C macros don't do the whole job, so the Cardfile
example also has a fair number of #ifdef's and machine-specific helper
functions. The macros do reduce the number of #ifdef's enough that they
don't hurt the code's readability very much. When porting between Windows
and Presentation Manager, this macro approach works very well, because of
the inherent similarity of the two systems. It's also very efficient.

When the Mac comes into the picture, the macro scheme starts to fall apart.
Because of the radically different architectures, it takes a considerable
body of machine-specific code no matter how you approach the problem. The
question becomes how best to divide the application between system-
independent code and system-specific code, and which services the system-
specific code provides to the rest. One way to look at this is to view the
independent code as running in a virtual operating environment that is the
same on all systems, and the specific code as implementing that environment
for each system.

The next question is just what that virtual operating environment is. It
can provide its own unique set of APIs, which are implemented by the system-
specific code on each system; or it can use the APIs of one of the systems
directly, in which case code on the other systems mimics those APIs. For
example, if you were starting with a Mac application and wanted to port it
to PM and Windows, it would be very useful to have the same Mac Toolbox
calls available on all three systems. Barring the existence of some
commercial library providing these functions, there's a lot of coding
involved in this, to say the least. It is true that you wouldn't have to
implement every feature of the base system, only those particular features
that your application actually uses.

Making up new APIs for coding the application is also a valid approach, as
Aldus did in writing a version of PageMaker that runs on the Mac and
Windows (and shortly for Presentation Manager). As described in "Aldus:
Preparing PageMaker for the Move to Windows" MSJ, (Vol. 1, No. 2), Aldus
split the project into "core code" and "edge code." The core code is the
same on both systems, and the edge code interfaces to the target systems,
providing a common set of functions that the core code uses.

There is also a commercial product that uses this method to provide
portability between the Mac and Windows. The Extensible Virtual Toolkit
(XVT) from Advanced Programming Institute, Ltd. provides a common interface
for these two systems and is being extended to support Presentation Manager
and X Windows as well. XVT provides a layer on top of each of the target
environments, with its own API, its own program structure, and its own user
interface. XVT uses a "vanilla with all the toppings" user interface like
the one I've been describing. Windows versions use the Multiple Document
Interface or a single document window, and Macintosh versions use the
standard Mac multiple window interface. Dialog boxes and menus work as
expected on both systems.

XVT does not provide as comprehensive or flexible an environment as any of
the native environments, but it provides enough to write a large variety of
applications. It doesn't support some features, such as modeless dialogs,
but there are plans for it to do so in future releases. It's possible to
implement features not supported by XVT by directly calling the native
environment, but this is nonportable. Nonetheless, it's possible to write
fairly sophisticated programs using XVT alone; XVT-Draw, a drawing program
written purely in XVT code, is a full-featured drawing program that shows
the power available. I'm looking forward to seeing the Presentation
Manager version of XVT.

One benefit of XVT's approach is that it's possible to invent an API that is
simpler and easier to program than any of the target systems. Although XVT
was written to facilitate portability, many people have used it as a
simpler way to program just one of the target systems. It also provides very
high-level support for a few things commonly used in applications. For
example, standard Font and Style menus can be generated simply by setting a
flag, and there is a complete Help system that can be built into XVT
applications.

As an experiment I took the other approach to portability, picking one
environment as the base and writing native code for it, then implementing
portions of that environment on the other systems. Since Presentation
Manager is my favorite of the three in terms of its APIs presenting the
greatest amount of functionality, I used that as the base system. I also
wanted a taste of what is involved in implementing an SAA-compatible
programming environment. Although there are now very few applications
written to SAA standards, many are coming, and it would be a plus to be able
to run them on hardware other than IBM's.

I also expect PM to become the single largest end-user and business
environment, so it makes good sense to focus the primary attention there,
and to port my application to Windows and the Mac as secondary markets. Of
course this also suggests, and I would certainly like to see, that all
those excellent Macintosh applications be ported to Presentation Manager.
(See the accompanying article which discusses porting Macintosh
applications to Windows and PM.──Ed.)

Taking one system as the base environment simplifies the problem in two
areas. It eliminates the need to invent common APIs, and it eases
debugging of at least one version of the application, because it runs as
native code on the base system. It also means that useful features of the
base system become available on the others. In the case of PM as the base,
this means, for example, that child windows──and window functions in
general──become available on the Macintosh. When I was first programming
the Mac a few years ago, I had never heard of child windows so I didn't
miss them. But now that I've had a chance to use them in my Windows and PM
programming, I don't know how I got along without them.


Sleuth

As a test case for this portability method, I used a souped-up version of
a program I wrote for Windows some time ago, which I now call Sleuth. It is
written as a native OS/2 application using Presentation Manager. I wrote
two libraries; WinPM for Windows, and MacPM for the Macintosh that provide
just enough Presentation Manager functionality to let Sleuth run on those
systems with no change to its source code.

Sleuth (see Figure 1 for PM source code) creates a main window that is a
standard document window-movable, resizable, and scrollable horizontally
and vertically. In this window, Sleuth lists all existing windows, visible
and invisible, in an outline showing the hierarchical relationship of
parent and child windows. Each entry in the list shows the basic information
about that window: its handle, ID, class name, rectangle, and text. You can
double-click any entry to show all the information Sleuth can find out about
a particular window, and see the information in a new window that Sleuth
opens for each entry you double-click. You can also use the cursor keys to
select an entry instead and then hit Enter or select the "Show Detail" menu
item.

Figures 2-5☼ show Sleuth running on OS/2 and the Macintosh; note that the
OS/2 version shows information about more windows than the others. That's
because the native version of Sleuth captures information about every
window created by Presentation Manager, regardless of the application
that created them. The other versions only know about the windows created by
the Presentation Manager emulator, either MacPM or WinPM, which
essentially means only the windows created by Sleuth itself. While Sleuth
on its native environment is useful for looking at how other applications
do things, it isn't on the other systems. However, if I were implementing
a "serious" application on MacPM and WinPM, the Sleuth window is the first
thing I would use in debugging the rest of the program. (I would also add a
message trace window as in Microsoft's Spy program; that isn't in this
version of Sleuth.)

Even as simple as it is, Sleuth uses a lot of Presentation Manager's
functionality. Many of its features──menus, text painting, scrolling──are
the same things that would be used by, say, a simple text editor. In fact,
an interesting thing happened as I was implementing MacPM (Sleuth's first
excursion into foreign territory). I had to sweat out a lot of code to get
a small portion of Sleuth running: the event/message manager, window
manager, and all sorts of utility functions such as mapping between PM and
Mac coordinates. Big chunks of the PM version of Sleuth were #ifdef'd out
for a long time while I just tried to get one message through a window
function correctly. Once I got past a certain point, things started coming
together much more smoothly. With the overall framework in place, filling
in the bits and pieces became much easier.


MacPM

MacPM provides enough of the PM functions and messages to let Sleuth, as
written for PM, run on the Macintosh. All that is required is recompiling
the code on the Mac, using MacPM's .H files, and linking it with the MacPM
code. Only MacPM makes any native Macintosh calls; Sleuth doesn't even know
it is running on a Macintosh.

MacPM makes no attempt to completely emulate Presentation Manager. It
provides only those portions of PM that Sleuth actually uses. In many cases,
I did try to code MacPM functions and messages the "right" way, even though
Sleuth only uses some of their options, but I did take shortcuts. The prime
goal was to get a particular application running. With different
applications, there are different areas you have to concentrate on, but
the central features──PM-style message and window management──are similar.
You can build any degree of compatibility you wish into a library like
MacPM, up to virtually 100 percent.

The best way to understand MacPM is to follow how it is used by Sleuth. If
you have the MacPM code on your Macintosh, you can trace through it using
Lightspeed C's source debugger while you read this description. (Full source
code for Sleuth and MacPM is available from any of the bulletin boards
listed on the back page of this issue.──Ed.)


Initialization

As with all good PM programs, Sleuth starts out with calls to WinInitialize
and WinCreateMsgQueue. These functions are in MpmInit.c. In PM,
WinInitialize just initializes the heap- and atom-management functions,
leaving the graphics and windowing initialization for WinCreateMsgQueue.
MacPM follows the same pattern. WinInitialize (see Figure 6) does some
Macintosh memory management initialization and returns a dummy HAB
(anchor block handle) value. (I've cheated already. Not only have I skipped
the heap and atom initialization──since MacPM doesn't yet support those
functions──but by just pretending to have an anchor, I'm risking getting
lost at sea. Since MacPM is linked in as part of a single application, it
can get away with not really allocating anchor blocks and a few other
things.)

WinInitialize also opens up a resource file called MacPM.rsrc. Macintosh
resources are used similarly to PM and Windows resources, but there is more
flexibility about where resources are located. Some resources are found in
the System file or in ROM, others in the application executable file, and
still others are in resource files explicitly opened with OpenResFile. Under
Lightspeed C, it's a common practice to have a separate resource file
during development. This helps provide a quick turnaround; there is no
need to merge the resources into the application file. When the
application is ready to ship, a standalone application file can be built
and the OpenResFile call removed. (Or the call can even be left in; it will
try to open the file but fail, and we can just ignore the error return
code.)

WinCreateMsgQueue (see Figure 7) is where the real work begins. First it
does the rest of the generic Macintosh initialization that you will find
at the beginning of any Mac application-initializing QuickDraw, the Event
Manager, the Window Manager, and assorted friends. Then it calls
MpmInitSysValues to initialize the system values that WinQuerySysValue can
access. These values are derived from appropriate Macintosh functions or
values-hard-coded where the corresponding Mac value is an absolute
number of pixels. For example, the width of a standard Mac vertical scroll
bar is 16 pixels, now and forever. This is hard-coded into nearly every Mac
application.

Next, WinCreateMsgQueue registers all of the predefined window classes,
making a call to WinRegisterClass for each one. This function is pretty
straightforward. First it checks if the class is already defined, and if
not it allocates a block of memory and stores the class information in it.
This block includes a CLASSINFO structure as defined in PM, along with a
few other pieces of information about the class. The class blocks are kept
in a linked list so WinCreateWindow can search through them. In theory,
MacPM should have the PM atom-management functions to search for a class
name, but I took a shortcut using a "quick-and-dirty" hashing function.

WinCreateMsgQueue wraps up its work by creating two predefined windows:
the desktop and object windows. Presentation Manager apparently creates
these windows as special cases; they don't have window classes like other
windows. In MacPM, it seemed simpler to make up an additional window class,
WC_DESKTOP, and then call WinCreateWindow to create these two windows as it
does the others. They are still special cases in that they have no
corresponding Mac windows. The desktop window lets you write to the
entire screen, just as in PM. Object windows aren't really implemented in
MacPM; it creates the top level object window but doesn't do much else with
it.


Creating a Window

With WinCreateWindow (see Figure 8), found in MpmCreate.c, we are getting
into the real inner workings of MacPM. As I mentioned earlier, the
Macintosh has nothing corresponding to PM's hierarchy of windows. There
is only a single window level, corresponding to a main window in PM. There
are no child windows nor a desktop window. Nor does the Mac have the concept
of a window function to which messages can be sent. Macintosh events are a
rough subset of PM messages, and the Mac provides functions to find out
which window a message relates to, but the interface is generally at a
lower level. For example, there is no WinDefWindowProc to handle default
conditions; if you don't include code to check for, say, a mouse click in
the close box, then the user's going to have a tough time closing your
window. Contrast this with the WM_SYSCOMMAND/SC_CLOSE message in PM or
Windows, which you can handle specially if you want or just disregard and
pass through to WinDefWindowProc.

Since all these facilities are so fundamental to PM coding, WinCreateWindow
creates a window structure that can hold the information necessary for a PM
window-desktop, main (top-level), or child. There is a window function
pointer, taken from the window class, along with all the WinCreateWindow
information; style flags, owner and parent window handles (along with
next/previous sibling and first/last child window handles), PM-style window
size and location, etc.

To communicate with the Macintosh world, the structure also holds a pointer
(WindowPeek) to the corresponding Macintosh window structure. All child
windows of a given main window have the same window pointer. For child
windows that implement button and scroll bar controls, there is also a
handle (ControlHandle) to the actual Macintosh control.

There is also a Mac adjustment rectangle, rectAdj, to help translate PM
window coordinates into the proper Macintosh coordinate space. The PM
child windows (and main window) are all defined the same way they are in the
real PM. Coordinates are always relative to the parent window, and the Y
coordinate counts up from the bottom──unlike the Mac and Windows, where Y
counts from the top down. Most Mac Toolbox functions expect coordinates to
be relative to the Mac's window client rectangle. The rectAdj field contains
the displacement (for all four sides) to get from the Mac window rectangle
to the Mac client rectangle. With this adjustment, it's possible to convert
any PM coordinate to the corresponding Mac coordinate, and vice versa. The
file MpmMap.c contains several mapping functions that convert points or
rectangles back and forth between Mac and PM coordinates. As you might
guess, these functions are used extensively throughout MacPM.

One last field in the MacPM window structure is the "window kind." There
are four different kinds of windows as far as MacPM is concerned, so
WinCreateWindow saves one of the following values in the window structure
and then takes care of some special processing for each:

  ■  WK_OBJECT (object window): No special processing; not really
     supported in this version of MacPM.

  ■  WK_DESKTOP (desktop window): Save the screen boundaries and a pointer
     to the Mac's full-screen graphics port (WMgrPort).

  ■  WK_MAIN (top-level Macintosh window): Create a Macintosh window. The
     Mac provides several styles of windows, so we choose the best style
     based on the PM style flags and then call the Mac's NewWindow function
     to create the actual window. Not all PM styles are supported; for
     example, a borderless window will get a border anyway. It's possible to
     create a borderless Mac window, but it takes more code (in Mac terms, a
     window defproc), and Sleuth doesn't need one anyway. NewWindow returns
     a WindowPeek pointer to the Macintosh window structure; save that
     pointer in our window structure.

  ■  WK_CHILD (child window): Set up the appropriate "relative"
     fields──such as hwndNextSibling, hwndTopChild, etc.──for this window
     and its parent and siblings. This wasn't necessary for WK_MAIN windows,
     since we relied on the Macintosh's window list for main windows. But
     for child windows we have to do all this bookkeeping.

At this point, the window structure is completely initialized, and
WinCreateWindow sends a WM_CREATE message to the window. Note that the
window is invisible at first, regardless of the WS_VISIBLE flag. Next,
WinSetWindowPos is called in order to set the window position and send the
WM_ADJUSTWINDOWRECT, WM_MOVE, and WM_SIZE messages. WinSetWindowPos and
WinSetMultWindowPos are interesting functions we'll come back to a little
later.

One final detail and then WinCreateWindow's job is done. Since the window
was created invisible, we now check the WS_VISIBLE flag and call
WinShowWindow if necessary. This makes the window visible and sends a
WM_SHOW message to the window function.


Standard Windows

After WinCreateMsgQueue returns, Sleuth registers its own client window
class and then creates its main window with WinCreateStdWindow (see
Figure 9). This function, found in MpmFrame.c, is responsible for creating a
document window as well as the standard frame control windows and client
window. It sounds like it needs to do a bunch of WinCreateWindow calls, and
that's basically what it does. WinCreateStdWindow assigns a default window
size and position for cases where WS_VISIBLE is specified. (If WS_VISIBLE
isn't set, it creates the window with size and position values zeroed out,
under the assumption that the application will position the window before
making it visible.) Then a WinCreateWindow call creates the frame window
itself.

Now it's time to create the frame control windows according to the FCF_
flags passed to WinCreateStdWindow. Here we call an internal function,
MpmFrameUpdate, in order to create the proper windows. MpmFrameUpdate is
also called if the frame window gets a WM_UPDATEFRAME message. (However,
the early PM documentation I worked with wasn't very clear about how this
should operate, so it's unlikely that I implemented it correctly.)

When all of this is finished, WinCreateStdWindow creates the client window,
and then-if WS_VISIBLE is specified-sends a WM_FORMATFRAME message to the
frame window and calls WinShowWindow in order to make it all visible.
WM_FORMATFRAME tells the frame window function in MacPM to position all its
child windows according to the current frame window size.


Frame Window Details

Some of the frame window initialization is handled inside
WinCreateStdWindow, but as much as possible is parceled out to the
individual window functions. In addition to the WM_UPDATEFRAME and
WM_FORMATFRAME processing in MpmFnwpFrame, several of the frame controls
have special initialization in their window functions. Scroll bars have the
most work to do, so let's take a closer look at how they are created and
positioned.

The code for scroll bars and scrolling is in MpmScroll.c. The scroll bar
window function, MpmFnwpScrollBar (see Figure 10), creates each Macintosh
scroll bar control when it receives the WM_CREATE during WinCreateWindow.
It uses a straightforward call to the Mac's NewControl function, except for
one little trick. NewControl does not have a parameter by which you can
tell it whether you want a vertical or horizontal scroll bar. Instead, it
looks at the scroll bar dimensions, and whichever direction is
larger──vertical or horizontal──is the kind of scroll bar you get. So,
MpmFnwpScrollBar makes up an arbitrary rectangle to insure that NewControl
creates the right kind of scroll bar. (Remember that the window may be
created with zero height and width, and the Mac wouldn't know if it should
be a vertical or horizontal scroll bar.) If the window is later resized, you
might get the wrong kind of scroll bar if it is sized too small in either
direction, but as long as window sizing has a large enough minimum bound,
this isn't a problem.

These "made up" dimensions aren't the correct ones, but that's no problem,
because the scroll bar is still invisible. Before it is displayed, along
come the WM_MOVE and WM_SIZE messages to clean things up. Their processing
is straightforward, simply calling the Mac's MoveControl and SizeControl
functions. With the scroll bar now properly positioned, the WM_SHOW
message that comes along next can call the Mac's ShowControl function to
make the scroll bar visible.

One subtle point here is that a scroll bar that is created via
WinCreateStdWindow actually gets the WM_MOVE and WM_SIZE messages twice. The
scroll bar, along with the other frame controls, is created with a zero size
during the WM_UPDATEFRAME processing. The first WM_MOVE and WM_SIZE
messages are sent at that time. Next the WM_FORMATFRAME processing makes a
call to WinSetMultWindowPos to set the actual positions, sending a second
pair of WM_MOVE and WM_SIZE messages. This is not that uncommon a situation
in PM; windows are often created with zero size then sized before being made
visible, and the PM documentation recommends that WM_SIZE processing be
skipped if the size is zero.

The menu and system menu windows also have their work cut out for them, but
it's handled a little differently. For one thing, they don't occupy any
space in the Macintosh window-they are child windows only for the sake of
sending and receiving messages, not because they are visible as windows. The
menu bar, of course, is visible at the top of the Macintosh screen, and it
certainly doesn't look like a child of the frame window. It really is a
child window, however, since WinQueryWindow (hwndFrame, FID_MENU, FALSE )
will find it, and you can send messages to it and your client window will
receive WM_COMMAND and other messages from it.

The idea of using an invisible child window──or an invisible window of any
kind──may seem strange at first. Of course, in this particular case, I had
no choice; I wanted MacPM to emulate PM, but with the Macintosh menu style
at the top of the screen. The invisible child window is about the only way
to accomplish that. Invisible windows can be very handy in other situations,
though. The mere fact that you can send messages to them and vice versa
gives you an opportunity to do something approaching object-oriented
programming.

On the menu window code, I really cheated. In PM there's a full set of menu
messages that let you manipulate menus dynamically, most of which would be
simple enough to implement on the Mac──with the caveat that the Mac has only
the single menu bar. Either the application can have only one window with a
menu bar (as an ASSERT in MacPM enforces now), or you would have to come up
with a scheme for merging the various windows' menus into the one menu bar.
For that kind of application, the best approach would be to write it as an
MDI window. There's a very convenient parallel between the MDI window with
its menu bar and overlapping child windows, and the Macintosh screen with
its menu bar at the top and overlapping windows below. In fact, as I write
this I am running MindWrite and Lightspeed C on the Mac, and each one has a
"Window" menu at the end, just like MDI. (The Mac applications actually
call it "Windows," but that's close enough for rock 'n' roll.)

Nice as this daydreaming can be, there's none of it in this version of
MacPM. Instead, WinCreateStdWindow calls MpmMenuLoad (see Figure 11)──found
in MpmMenu.c──if the FCF_MENU flag is set. MpmMenuLoad does some very simple,
generic Mac menu initialization. It loads in the menu bar definition from
the resource file, looks for an Apple menu and adds the desk accessory menu
to it, then displays the menu bar.

The last frame control with special initialization is the title bar. If the
FCF_TITLEBAR flag is set, WinCreateStdWindow calls WinSetWindowText to pass
the pszTitle parameter along to the title bar control. Next,
WinSetWindowText sends a WM_SETWINDOWPARAMS message to the window in
question, with the WPM_TEXT and WPM_CCHTEXT flags and the corresponding
fields in the WNDPARAMS structure set. The title bar window function,
MpmFnwpTitleBar, responds to WM_SETWINDOWPARAMS by calling the Mac's
SetWTitle function to set the displayed window title. MpmFnwpTitleBar is
also able to respond to WM_QUERYWINDOWPARAMS, getting the window title with
the Mac's GetWTitle function and passing it back to the caller.

The only complication in these messages is that the text strings must be
converted back and forth between the C and Pascal string formats. Mac
programmers working in C quickly get used to this since the Mac Toolbox
functions all expect (and return) Pascal-style strings, which have no zero
terminator but instead have a length byte at the front. Lightspeed C does
have Pascal-style string constants for this situation, but that doesn't
help in this case since Sleuth is pure PM-style C code and passes C-style
strings into MacPM.

The remaining frame controls have no special initialization. They will have
some work later, when they receive mouse and other messages, but first, we
have to create those messages and get them to their destination. The code
for doing this is in MpmMsg.c.


Events

With MacPM, a message isn't just a message, it's an event. MacPM, just like
Windows or the real PM, has event messages along with a host of other kinds
of messages. I've discussed several initialization and notification
messages, which any PM programmer should be familiar with. These messages go
directly to the destination window function via a WinSendMsg call. This
is one of the simplest functions in MacPM; other than a validity check on
the window handle, all it does is make an indirect function call to the
window function. This function address is a cinch to get to and call with
the MYWNDOF macro:

  return ( *MYWNDOF (hwnd).pfnwp )( hwnd, msg, mp1, mp2 );

That's WinSendMsg at a glance-rather easy. Now for the fine print. After
Sleuth creates its main window and does some other initialization, it falls
into a typical PM message loop:

  while( WinGetMsg( hab, &qmsg, NULL, 0, 0 ) ) WinDispatchMsg( hab, &qmsg );

It happens that Mac applications are constructed around a similar main
event loop:

  while( TRUE )
  {
    fMine = GetNextEvent( everyEvent, &theEvent );
    MyDispatchEvent( &theEvent );
  }

GetNextEvent is a Mac Toolbox function that returns the next event, very
much like WinGetMsg. (The return value from GetNextEvent tells whether the
event occurred in an application window or in a system window, that is, a
desk accessory.) MyDispatchEvent isn't part of the Mac Toolbox, but a
function that would be part of this imaginary application to take care of
the incoming events. Before GetNextEvent picks them up, the Mac events are
held in an event queue much like PM's message queue.

The Mac provides events for the following conditions: mouse down and up, key
down and up (and key repeat), window update and activate/deactivate. These,
of course, correspond closely to PM messages. The Mac has a few other events
that don't map well into any PM messages, so MacPM disregards them.

For the events that have corresponding messages, things are almost
straightforward. For instance, when we click the mouse in Sleuth's client
window we go into the main message loop, inside the WinGetMsg function.
Since WinGetMsg isn't supposed to return until it has a message for us, it
sits in its own idle loop:

  while( ! WinPeekMsg( hab, pqmsg, hwnd, msgMin, msgMax, PM_REMOVE ) )
  WinWaitMsg( hab, msgMin, msgMax );

Here's the tricky part: WinWaitMsg is the function that is finally supposed
to wait for a message; and sure enough, it is indeed waiting in yet another
idle loop:

  while( ! WinPeekMsg( hab, &qmsg, NULL, msgMin, msgMax, PM_NOREMOVE ) );

Note the difference between those two WinPeekMsg calls. The PM_REMOVE option
in the first one tells WinPeekMsg to pull the message from the queue,
because that's what WinGetMsg is supposed to do. The second one has the
PM_NOREMOVE option, because WinWaitMsg is supposed to wait until a message
is available and then return, but with the message still in the queue. So
you can see that WinPeekMsg gets called somewhat redundantly during the
course of a single WinGetMsg call──first, inside WinWaitMsg to find out that
the message is available, then again inside WinGetMsg to actually pull the
message. While this isn't very efficient, it certainly made the coding
easier──once I had both options of WinPeekMsg debugged, the other message
functions fell together with the simple code you see above.

WinPeekMsg is where life becomes interesting. It helps that Sleuth doesn't
use the filtering options of WinPeekMsg, with which you can specify the
minimum and maximum message numbers along with the window handle you're
interested in. The window handle would be no problem, but implementing
the message numbers would be a nuisance. There is one special case
internally in MacPM: WinSetActiveWindow calls WinPeekMsg specifically
looking for WM_ACTIVATE messages only so it can have the WM_ACTIVATE sent
to the window at the proper time. The Mac normally queues this event.
WinPeekMsg handles this as a special case, setting a different event mask
for the Mac Toolbox call. The event mask tells which events you want to
receive.

Before asking for any events, WinPeekMsg calls the Mac's SystemTask
function. This yields control to desk accessories (and under MultiFinder,
to other applications). Then WinPeekMsg calls either GetNextEvent or
EventAvail, depending on whether you specified PM_REMOVE or PM_NOREMOVE.
(EventAvail is the same as GetNextEvent except it leaves the event in the
queue.) If an event is returned, WinPeekMsg then calls one of several
internal functions to convert the event into the equivalent PM message,
which then gets passed back to whatever called WinPeekMsg.


Mousing Around

Mouse movement requires special treatment. The Mac does not have a mouse-
move event; it has events for mouse-down and mouse-up, but mouse movement
doesn't generate an event. However, when no event is pending at all,
GetNextEvent returns a special null event. Since the mouse position is
passed along with every event, as in PM, whether or not the event has to do
with the mouse, it's not too much trouble for WinPeekMsg to check the mouse
position when it gets a null event. If the mouse has moved, a WM_MOUSEMOVE
message is generated. If not, WinPeekMsg just returns FALSE to indicate no
message is available.

Mouse clicks are easier to handle than mouse movement-single clicks are,
anyway. The Mac provides mouse-down and mouse-up events, so it's simply a
matter of passing these through as PM messages. Double clicks are a little
more complicated. Like many things on the Mac, there is no automatic
support for these; the application must determine whether two mouse clicks
are close enough in time and space to be a double-click. MacPM should
compare the time and location of each mouse-down event with the most recent
mouse-up event and, if they are close enough, generate a WM_BUTTON1DBLCLK
message instead of a WM_BUTTON1DOWN. I say "should" because this isn't coded
in this version of MacPM.

Each one of these mouse events is passed through the MpmMsgMouse function
(see Figure 12), which decides which window gets the message. First, it
determines the proper main Mac window with a call to the Mac's FindWindow
function. This not only tells which window the mouse was in, but which area,
such as the size box or the close box. It also distinguishes mouse clicks
in a system window (that is, a desk accessory) from those on the desktop
itself. If the mouse is in one of our windows, the mouse message has to be
passed to the correct child window. Some return codes from FindWindow
indicate this directly, like inGrow, which indicates the mouse is in the
size box. In these cases, the proper child window is determined directly
from the return code (for example, the FID_SIZEBORDER window for inGrow).
The inContent return code takes a little more work. The MpmMsgFindChild
function does the trick here; it scans through all the child windows of the
main window, looking for a match on the child window rectangles. The
appropriate child window then gets the mouse message, or the frame window
gets it, if no child window matches.

In theory, there should be some WM_HITTEST messages flying around at this
point. I didn't bother with these since things were working pretty well
without them and, to be honest, I wasn't sure from the preliminary PM
documentation just how WM_HITTEST was supposed to work. An application
using child windows in a more complex way might need the WM_HITTEST
messages.

Keyboard and mouse messages require some translation to convert them to the
proper PM form, but this is nothing special. Mouse messages do need
translation from the Mac's coordinate system to PM's upside down
system-which is a minor nuisance, but one that crops up several places in
the code. The mapping functions in MpmMap.c take care of this where
necessary.


The Active Life

 The Mac's concept of active and inactive windows is very much the same as
that in PM and Windows; the topmost visible main window is the active
window, and all others are inactive. All windows in each of these systems
are "active" in the sense that the application may display information in
them, but the topmost window has a different visual appearance to
highlight it. The Mac, however, places greater emphasis on the difference
between active and inactive windows; the scroll bars and size box disappear
on inactive windows, and a mouse click in an inactive window brings the
window to the top but is otherwise disregarded. Unlike Windows and PM──in
which you click in any visible portion of any window, and it does what you
expect──the Mac takes a second click to "really" click the mouse in the
window. This is a flaw in the original Macintosh user interface guidelines,
and not all Mac applications follow it; some work like PM and Windows,
making all windows respond immediately to clicks. The Finder(TM), for
example, works partially this way. But I followed the majority and made
mouse clicks in MacPM work according to the guidelines, even though it's
less convenient. This is certainly something to consider changing; I don't
think Mac users would object if their windows were more responsive.

In any case, MpmMsgActivate processes the Mac's activate event, turning it
into a WM_ACTIVATE message with the appropriate activate/deactivate flag.
There is a little trick in this function which shows up in several others as
well. Don't forget that WinPeekMsg can be called with either the PM_REMOVE
or PM_NOREMOVE option. If called with PM_NOREMOVE, then later it will be
called again to pick up the same message with the PM_REMOVE option. So the
little trick is that, in the PM_NOREMOVE case, MpmMsgActivate does nothing
more than return the PM message. When called again with the PM_REMOVE
option, it goes ahead and updates the grow icon and scroll bars to reflect
the new active/inactive state.


Painting

Update events are the trickiest ones of all. These generate WM_PAINT
messages, of course, but an update event pertains to an entire Macintosh
window; the Mac doesn't know we have child windows inside it. Therefore
MacPM must parcel out the WM_PAINT messages to the child windows, making
sure each one has the right coordinate system and clipping region. This is
done in MpmPaint.c, and it's where I really started to wonder if child
windows are worth it. To be honest, I could have gotten by with less work on
the child window painting for this particular application. But I wanted to
find out what's involved in emulating the individual WM_PAINT messages that
get sent to child windows in PM.

MpmMsgPaint is where it all starts, and it's simple enough; it just calls
WinUpdateWindow, once it determines that the PM_REMOVE flag was set on the
original WinPeekMsg call. WinUpdateWindow is the beginning of some serious
cheating. In the real PM, you can call WinUpdateWindow individually on
any child window, and that window will be painted immediately, while any
other pending updates remain deferred. The problem here is that the Mac
has BeginUpdate and EndUpdate functions that perform the same tasks as the
functions WinBeginPaint and WinEndPaint in PM. However, these functions
apply to the entire Macintosh window; they know nothing of child windows.
I could have mimicked the PM operation exactly, but it was simpler to say
that any WinUpdateWindow call updates the entire Macintosh window,
including all child windows.

Given this restriction, WinUpdateWindow does the BeginUpdate, paints the
grow icon and all controls (for example, the scroll bars) with Mac Toolbox
calls, then calls MpmPaintWindow to send WM_PAINT messages to the frame
window and all child windows. Note that these are sent, not posted, since
they must all be mimicked before the EndUpdate call found at the
end of WinUpdateWindow. MpmPaintWindow is a simple recursive function that
sends a WM_PAINT message to a window and then calls itself for each first-
level child of that window, thus sending WM_PAINT to all child windows at
any level of nesting.

Now the fun begins. The WM_PAINT we're most interested in is the one sent
to Sleuth's client window function. Like all good WM_PAINT handlers (at
least ones using the cached micro-PS), this begins with a WinBeginPaint
call, which returns the presentation space handle for use in painting. MacPM
has to do a bit of work to create this PS, even though a MacPM presentation
space barely qualifies as a nano-PS, much less a micro-PS. For that matter,
MacPM's "cache" of PS's is awfully tiny; only a single presentation space is
reused whenever WinGetPS (see Figure 13) is called.

WinBeginPaint itself is easy enough. It just calls WinGetPS and then
converts the Mac's visible region bounding rectangle to PM coordinates.
(During update processing, the visible region is clipped down to the former
update region.) Then WinGetPS copies the entire GrafPort structure from the
specified Mac window (or the Mac desktop) into the new PS. A GrafPort is
the Mac's structure that corresponds roughly to a device context in PM or
Windows. In addition to copying the structure itself, a couple of CopyRgn
calls are required to duplicate the visRgn and the clipRgn from the original
GrafPort. Once that's done, the visRgn in this new GrafPort must be clipped
down further to take the child window coordinates into account. (I could
have left the visRgn alone and modified the clipRgn instead, and perhaps
avoided the need to copy the GrafPort, but it seemed simpler and safer this
way.)

That's not hard in this version of MacPM, since it doesn't support the
WS_CLIPCHILDREN and WS_CLIPSIBLINGS styles. However, it does support
WS_PARENTCLIP, or more significantly, the absence of WS_PARENTCLIP. If
WS_PARENTCLIP is set on a child window, the window lacks its own clipping
region, therefore using that of the parent, and it's up to the child window
to avoid drawing outside its boundaries. When WS_PARENTCLIP is missing, the
child window needs its own clipping region, so WinGetPS takes the
intersection of the parent's clipping region and the child window's
rectangle and makes that the new clipping region for the child.

If WS_CLIPCHILDREN and WS_CLIPSIBLINGS were supported, they would be
handled next. Each would basically be a loop over the window's children or
siblings, subtracting the appropriate rectangles from the window's
clipping region. Since Sleuth doesn't use either of these window styles, the
easiest way to implement this was to just say "no".

Now you see why WinGetPS makes a copy of the GrafPort it needs. With all of
this modification to the clipping region, as well as other possible
modifications we would need to make if we wanted to implement additional
PM features, the safest thing was to make a private copy of the whole
structure. There are probably plenty of optimizations that could be made
but, more importantly, it works.

That's our presentation space such as it is; so now what does Sleuth do with
it? Just two things: a GpiErase and a series of GpiCharStringAt calls. In
MpmGPI.c (see Figure 14) these functions were remarkably simple to
implement, because all the dirty work was already done. GpiErase is nothing
more than a call to the Mac function EraseRgn, passing it the visRgn from
our modified GrafPort in the pico-PS. GpiCharStringAt breaks down into
GpiMove──which calls the Mac's MoveTo function──and GpiCharString, which is
a simple call to the Mac's DrawText function.

Although they took a lot of work, the simplicity of these GPI functions
gives me hope that other simple GPI drawing functions would also be easy to
implement on the Mac. Some of the fancier GPI stuff would, I suspect, be a
lot of work since the Mac doesn't have exact equivalents.

When Sleuth is done painting its client window, it calls WinEndPaint, which
does nothing more than make a call to WinReleasePS, which in turn just frees
up the pico-PS by discarding its regions and marking it as no longer in
use.

One other topic in window painting is invalidation. Sleuth does call the
WinInvalidateRect function whenever it wants to force its client window to
be repainted, and this function is very straightforward. It just converts
the rectangle to a Mac rectangle and then calls the Mac's InvalRect
function. It's then up to the Macintosh event manager to generate an update
event.

After all of this discussion about WM_PAINT, one strange fact is that none
of the window functions in MacPM itself provide much in the way of WM_PAINT
processing. Since the frame control windows all happen to be Macintosh
"controls" as well (or invisible), they were all drawn back at the
beginning of WinUpdateWindow, when it called the Mac's UpdtControl
function. They don't have to do anything else.

The only window function that handles WM_PAINT is the frame window
function, MpmFnwpFrame, which does not really paint anything; the Mac
Toolbox takes care of the drawing of the window frame. It does erase the
client area background, by sending a WM_ERASEBACKGROUND message to the
client window. The client window can either erase the background itself on
this message and return TRUE, or else return FALSE and let the frame window
do the erasing. This is done with a GpiErase call, bracketed by WinGetPS and
WinReleasePS. This WM_ERASEBACKGROUND processing doesn't really achieve the
purpose that it does in PM, which is to cause synchronous background
erasing in an asynchronously painted client window──because I neatly
sidestepped the whole issue of synchronous vs. asynchronous painting.
MacPM uses its own strange brew when it comes to window painting.


Scrolling

Because Sleuth's windows are scrollable, MacPM must provide the two pieces
of PM window scrolling: scroll bar tracking and messages, and the
WinScrollWindow function. Scroll bars on the Mac are a pain to deal with.
Not only must you position them yourself──there's no asking for and getting
a standard scroll bar──but your code must get much more involved in tracking
them. Unlike PM and Windows, in which a scroll bar can track itself and send
messages to its owner, application code must call the proper tracking
function (depending on which part of the scroll bar was clicked) and deal
with more of the busywork itself. Sleuth, being a PM application, doesn't
have to worry about this, but MacPM does. MpmScroll.c has all the details.

When the mouse is clicked in a scroll bar, this sends a WM_BUTTON1DOWN
message to MpmFnwpScrollBar. After converting the PM mouse coordinate to a
Mac point, this calls the Mac's FindControl function, which identifies the
Mac control containing that point, and which part of the control it is. We
already know which Mac control should be returned here, since the
WM_BUTTON1DOWN was here initially because it hit this particular child
window, and MacPM's window structure for a control includes the Mac control
handle. So this is a good place for an ASSERT macro to trap out, if for some
reason FindControl returned a different control from what we expected. (You
will find a lot of ASSERTs in MacPM; they are an invaluable debugging
tool.)

The piece of information we are really after from the FindControl call is
the "part code," which tells whether the mouse was in the thumb, page
up/down area, or one of the arrows. We need to know whether the mouse is in
the thumb or not because we're about to call the Mac's TrackControl
function, one of the more bizarre functions in the Mac Toolbox. It does
the actual mouse tracking, either dragging a gray rectangle for the thumb or
monitoring mouse movement and release in the arrows or page areas.

TrackControl looks simple enough; you pass it the control handle, mouse
position, and a pointer to a callback function that is called repeatedly
until the mouse is released. The weird thing is that you have to use two
different callback functions, one for thumb tracking and one for everything
else, and they take different sets of parameters. The arrow and page
callback is reasonably straightforward; it receives as parameters the
control handle and part code, and it only gets called if the mouse is
inside the proper tracking limit rectangle (that is, the arrow or the page
area). In MacPM this function, MpmTrackScrollBar, just sends a WM_HSCROLL
or WM_VSCROLL message to its owner, with the SB_ command code determined by
the Mac's part code. Then as in PM it's up to the owner to set the new
scroll bar position and do any needed window scrolling.

The thumb tracking callback is where things get difficult. This function has
no parameters at all; it must figure everything out by reading the mouse
position each time it's called. The function has to calculate the new
scroll bar value itself, based on how far the mouse has moved and on the
original scroll bar value and range. This isn't a difficult calculation, but
it's not trivial, and it's silly to require the application to calculate it,
given that TrackControl is capable of calculating the final scroll bar
position when the mouse button is released. It should have made this same
calculation during tracking and passed it to the thumb tracking function as
a parameter.

But that's not the worst of it. You've probably noticed that when you drag a
scroll bar thumb──on PM, Windows, or the Mac──it will continue to track if
the mouse is within a certain distance of the scroll bar, but it jumps back
to its original position if you move too far. Move it closer and it resumes
tracking. TrackControl takes care of this as far as the visual aspect. But
does the thumb tracking callback know whether tracking is suspended because
the mouse is too far away? Well, it can just compare the new mouse position
with...what? Dumb question. I forgot that the thumb tracking callback doesn't
get told anything.

Although I haven't disassembled TrackControl, I suspect it's calling
another Toolbox function called DragGrayRgn, which tracks the mouse and
drags a gray outline of a rectangle or other region. One of the parameters
to this function is a "slop rectangle," which determines how far the mouse
may stray before tracking is suspended. This slop rectangle would be just
the thing the thumb callback needs, but it's stored somewhere on the stack,
and is inaccessible to the thumb callback.

Need I say that after much experimentation I finally gave up on including a
thumb tracking function in MacPM? The only harm done is that the
SB_SLIDERTRACK notification isn't supported, so Sleuth does not scroll
its window during thumb tracking, only afterward. If you've ever wondered
why nearly all Mac applications wait until thumb tracking is complete before
actually scrolling their windows, this is why. At least 99 percent of the
Mac programmers in the world came to the same conclusion I did: thumbs
down.

Even without worrying about thumb tracking, there's one more minor
complication with the thumb. When you release the mouse button, TrackControl
calculates the new scroll bar position and sets the scroll bar to that
position. However in PM, when the SB_SLIDERPOSITION notification is sent,
the new position has not yet been set. An SBM_QUERYPOS message returns the
old position, but it's the responsibility of the application to set the new
position. MpmFnwpScrollBar handles this by saving the old thumb position in
a local static variable at the beginning of thumb tracking, at which time
the local static flag fTrackThumb is also set, including the moment the
SB_SLIDERPOSITION is sent. Then it's a simple matter for the SBM_QUERYPOS
code to check fTrackThumb and return either the old value or the current
value (in the normal case). When Sleuth follows up by sending an SBM_SETPOS
message, the position has already been set, but the redundant call does no
harm.

Enough complaining about scroll bars. The other part of scrolling,
WinScrollWindow, is a lot simpler. Sleuth calls this function when it is
ready to actually scroll its client window. WinScrollWindow first checks for
child windows and adjusts their positions if the SW_SCROLLCHILDREN flag is
set. (There aren't any child windows inside Sleuth's client window, so it
doesn't use SW_SCROLLCHILDREN.) Then it picks up the window rectangle for
the window to be scrolled, converts it to Mac coordinates, and passes the
resulting rectangle to the Mac's ScrollRect function. This function scrolls
the bits in the window and then calculates the invalidated region. It
doesn't actually invalidate the region and cause it to be repainted, it
merely calculates it. So, WinScrollWindow then calls the Mac's InvalRgn
function to accumulate this region into the window's update region.


Movement and Sizing

Even though the child windows for FID_TITLEBAR and FID_SIZEBORDER aren't
actually visible in MacPM, their window functions (in MpmFrame.c) provide
the same services as in PM itself. In each case, the WM_BUTTON1DOWN message
will cause the appropriate mouse tracking and window movement or sizing.
MpmMsgMouse directs this message to the FID_TITLEBAR or FID_SIZEBORDER
window when the Mac's FindWindow function returns the inDrag or inGrow
window area code. For FID_TITLEBAR, the MpmFnwpTitleBar function takes the
message and converts the mouse location back to Mac coordinates, then calls
the Mac's DragWindow function. This function performs the mouse tracking and
actually moves the window when the button is released, if the mouse was
inside a bounding rectangle that was passed to DragWindow. MpmFnwpTitleBar
calculates this bounding rectangle somewhat arbitrarily, four pixels inside
the actual screen size (not including the menu bar). After the button is
released, MpmFnwpTitleBar checks to see if the window actually did move, and
if so, sends a WM_MOVE message to its owner (the frame window).

Window sizing is similar, but a little more complicated. MpmFnwpSizeBorder
takes the WM_BUTTON1DOWN message in this case, and calls the Mac's
GrowWindow function to do the mouse tracking. Unlike DragWindow, GrowWindow
doesn't actually resize the window──it just returns the new mouse position.
Then MpmFnwpSizeBorder picks up the old frame window size and position by
using WinQueryWindowPos and sets the new size and position with
WinSetMultWindowPos. WinSetMultWindowPos is more convenient than
WinSetWindowPos in this particular case, since it takes an SWP structure
that is exactly like the one filled in by WinQueryWindowPos.
WinSetMultWindowPos takes care of sending the WM_MOVE and WM_SIZE messages
to the frame window, which in turn sends itself a WM_FORMATFRAME message to
reposition the frame controls and client window. Before calling
GrowWindow, MpmFnwpSizeBorder should send the frame window a
WM_QUERYMINMAXINFO message, but instead it merely sets arbitrary tracking
limits: 100-pixel minimum height and width, no maximum limit.


Menu Selection

The menu window function MpmFnwpMenu, located in MpmMenu.c, does triple duty
because there are three different "menus" to consider: the menu bar, the
system menu (close box on the Mac), and the minimize/maximize icons (zoom
box on the Mac). In PM each of these is a true menu and they are all treated
the same. In MacPM the close box and zoom box are special cases but, to be
consistent with PM, they are all handled in MpmFnwpMenu. MpmMsgMouse
directs the WM_BUTTON1DOWN message to one of these three frame controls and,
since they are all of class WC_MENU, MpmFnwpMenu gets the message in each
case.

For FID_SYSMENU (the close box), it's straightforward. MpmFnwpMenu simply
calls the Mac's TrackGoAway function and, if that function returns TRUE,
then it sends a WM_SYSCOMMAND message with the SC_CLOSE command to its
owner, the frame window. MpmFnwpFrame passes this command message through to
the client window, and Sleuth's client window function merely passes it on
to WinDefWindowProc, which posts a WM_QUIT message to the application
queue.

FID_MINMAX (the zoom box) is almost as simple, but I cheated a little with
this one. Back in MpmMsgMouse, the FindWindow function returned two
different area codes for the zoom box, inZoomIn or inZoomOut, depending on
whether the window is currently zoomed or not. The Mac provides a simple
ZoomWindow function to do the actual zooming or un-zooming, but that
function expects to receive the inZoomIn or inZoomOut code to tell it which
to do. So, I simply passed that code along in the high-order word of mp2 in
the WM_BUTTON1DOWN message. That word happens to be unused in PM, so I was
able to get away with this nonstandard use. Given that little trick,
MpmFnwpMenu calls the Mac's TrackBox function and if the function returns
TRUE, sends either an SC_RESTORE or SC_MAXIMIZE message to its owner, the
frame window. MpmFnwpFrame takes over from there, calling the Mac's
ZoomWindow function and sending the appropriate WM_MOVE and WM_SIZE
messages.

There's one more thing that MpmFnwpMenu has to deal with and that is the
menu bar itself. When the mouse button is pressed in the menu bar,
MpmMsgMouse directs the WM_BUTTON1DOWN message to the FID_MENU control
window. MpmFnwpMenu sends a WM_INITMENU message to its owner, then calls
the Mac's MenuSelect function to do the mouse tracking. This function
returns the Mac menu ID; the high-order word is the resource ID for the
pull-down menu, and the low-order word tells which item was selected on the
menu. Unfortunately, while resource IDs may be chosen as desired, the menu
item number in the low-order word is simply a sequential index into the
menu. This is different from PM or Windows, where you can assign your own
menu IDs to each menu item. It means that if you want to move a menu item to
a new position, you must update your definitions in the C code and
recompile. For MacPM I could have set up a way to allow arbitrary menu
ID's──the way that Windows and PM allow them──but it was simpler to just use
the Mac's numbering convention, assigning menu IDs in the PM version that
would match the Mac's menu IDs.

The code in MpmFnwpMenu has to check one more thing. The Apple menu has a
list of desk accessories as well as menu items for the application itself.
If the menu selected was a desk accessory, it starts up that desk accessory
with an OpenDeskAcc call. Otherwise, it sends a WM_COMMAND message to its
owner, the frame window. The frame window function passes this message along
to the client window, where Sleuth's window function handles it.


Header Files

In MacPM I followed the principle that the Presentation Manager C code
should run unchanged. I would have liked to apply this to the .H files as
well as the .C files, because the OS/2 .H files contain all the necessary
declarations. However, because of the compiler differences I mentioned
earlier, it didn't quite work out this way. I was able to use the .H files
from OS/2 with Lightspeed C, but it took some manual editing to get them to
compile. Here's what I had to change:

Conditional compilation. ANSI C has a number of new preprocessor features
that aren't present in Lightspeed C. The only one used much in the OS/2 .H
files is the #if defined(name) construct. This serves the same purpose as
the older #ifdef but allows checking for more than one symbol at a time. The
OS/2 .H files use this in many places, in a form like the following:

  #if ( defined (INCL_WINFRAMEMGR) | !defined (INCL_NOCOMMON) )

Since I didn't care about the check for INCL_NOCOMMON, I edited them to work
with Lightspeed C:

  #ifdef INCL_WINFRAMEMGR

Fortunately most of the conditional compilations were already in the #ifdef
form (they only test for a single symbol), so there weren't too many places
that needed editing.

Prototypes for function pointer declarations. Although Lightspeed C accepts
function prototypes, it does not accept them for pointer types, such as:

  typedef VOID (PASCAL FAR *PFNEXITLIST) ( USHORT );

I took care of these with the simple expedient of commenting out the
parameter declarations:

  typedef VOID (PASCAL FAR *PFNEXITLIST) ( /*USHORT*/ );

NEAR, FAR, and PASCAL keywords. Since Lightspeed C and Microsoft C both
support the "pascal" keyword differently, I decided to forego the use of
the Pascal calling sequence. Most uses of the "pascal" keyword in the .H
files are coded as PASCAL (which is #define'd as pascal), so I just changed
the #define to make PASCAL an empty string. The same thing applies for NEAR
and FAR, since they aren't relevant in the Mac environment:

  #define PASCAL
  #define NEAR
  #define FAR

One slightly odd thing is that OS2DEF.H fails to use these #define'd symbols
in several places, falling back to the lowercase names. This doesn't seem
intentional and will hopefully be corrected in a future version of this
file. In the meantime, I added two more #define's to effectively disable
"near" and "far":

  #define near
  #define far

I couldn't do this with "pascal", since Lightspeed C needs this keyword for
its own .H files, which define all the Mac Toolbox entry points. More
manual editing was called for: changing "pascal" to "PASCAL" where it
occurred in OS2DEF.H. The other OS/2 header files didn't have this problem.

Different calling sequence (order of parameters). Deciding not to use the
Pascal calling sequence led to a complication. There are several data
structures and macros in PMWIN.H that assume the Pascal calling sequence.
The SWP data structure, for example, is defined so that the parameters
passed in a call to SetWindowPos can be directly referenced as an SWP
structure. To allow this, I reversed the order of the fields in these
structures, because the C calling sequence pushes the parameters in the
opposite order from the Pascal sequence. Similarly, I had to change the
COMMANDMSG, CHARMSG, and MOUSEMSG macros and their associated structures
to work with the C calling sequence. Using the C calling sequence was a
mixed blessing; it led to extra work but avoided the need to edit every
single function declaration to allow for the different positioning of the
"pascal" keyword.


WinPM

As you can see, getting Sleuth running under MacPM was a lot of work. So
much, in fact, that I must confess I didn't get a working WinPM written in
time for this article. In lieu of actual code, let's take a look at some of
the issues involved in putting together a WinPM, starting with window
classes.

Because Windows resembles PM more than the Mac does, it would be handy to
use existing Windows features. I would be tempted to use the window class
mechanism in Windows instead of coding it from scratch, as in MacPM. I
wonder, however, if this would be a good strategy. Window classes under PM
are much simpler than under Windows, with many of the items in the Windows
WNDCLASS structure eliminated.

Instead, the frame window class and the FCF_ and FS_
options in WinCreateStdWindow handle these items. For instance, under
Windows all windows of a given class have the same icon, whose handle is
stored in the WNDCLASS structure. Under PM, the window class does not
determine the icon; each frame window can have its own icon, and it's
normally loaded in from the resource file according to the frame window ID.

Depending on the application these differences could be glossed over, but
Sleuth is a tough cookie with issues like this. It calls functions like
WinQueryClassInfo, which have no direct counterpart in Windows. This
function is a particular problem. Under Windows, there is no direct way to
get at the class information given a class name. You can do it, but you must
create an invisible window of the desired class, just to call GetClassWord
and GetClassLong to pick up the class information.

With problems like this, it may be worthwhile to keep the class support code
from MacPM. You would still need to register the window classes with
Windows, so there is some redundancy.


Messages

Message passing is another place where it's tempting to directly use Windows
facilities, since window functions are so similar in the two systems. That's
what the macro binding system in the Presentation Manager Conversion
Guide does.

The goal of attempting to maintain "pure" PM code seems to lead to too many
problems. For starters, a window function in Windows takes a 16-bit window
handle, and its two parameters are different lengths: 16-bit and 32-bit. In
PM, all these items are 32-bit. You could fudge the declarations in PMWIN.H
to make everything the same length as in Windows, but this still wouldn't
handle the differing contents of the message parameters.

I'd probably leave the PM window functions alone, with 32-bit window handles
and two 32-bit parameters. Unlike MacPM, where all the message processing
had to be coded from scratch, this could be handled by having WinPeekMsg
call the Windows function PeekMessage and then do the proper conversions.
WinDispatchMsg and WinSendMsg would not talk to Windows at all; they would
directly call the PM window function, avoiding the problem of different
function formats.

For messages that are sent from Windows directly to the window function
instead of being posted to the queue, the problem is a little different.
Instead of giving Windows the address of the PM window function, there
would have to be a common window function that Windows would call in all
cases. This function would convert the parameters appropriately, find the
address of the correct PM window function (probably using space in the
"window extra" data), and call the PM window function.


Frame Controls

Window function differences notwithstanding, it makes sense to create child
windows for all the frame controls, just as PM does. I'd take the same
approach as in MacPM, where scroll bars are visible child windows in their
expected positions, but the title bar, menu bar, and other frame controls
are invisible existing only for the purpose of message passing. Windows, of
course, already supports child window scroll bars as well as "standard"
scroll bars. I would forget about using standard scroll bars; the only extra
burden for using child window scroll bars is that their size and position
must be explicitly calculated, which is not difficult. For the other frame
controls, invisible child windows would make it easy to mimic their
operation under PM.


Painting/Scrolling

With the limited use Sleuth makes of GPI, it's not hard to provide the same
functionality by converting the GPI calls into the equivalent GDI calls, the
way MacPM does. Also, the window painting logic is much more straightforward
than under MacPM. The various shortcuts I mentioned earlier won't be
necessary; Windows generates WM_PAINT messages for child windows and top-
level windows the same way PM does. A thin layer is needed to take care of
the differences in WinBeginPaint vs. BeginPaint, and then I expect the rest
would work pretty smoothly.


Application or DLL?

One simplifying assumption in MacPM was that it had to support only a single
application, since the MacPM code is linked in with the application. In
Windows this same approach could be used, but the right way to code a WinPM
would be as a dynamic-link library (DLL), so it could support more than one
application. The mechanics of linking to a DLL are no problem, but a
complicating factor is that WinPM would then have to handle multiple
applications calling it. This would rule out using some static variables the
way MacPM does. Instead, the proper data structures would have to be
allocated as needed.

One thing that causes problems in Windows DLLs is that they have no way to
automatically find out when they are being unloaded. There's a library
initialization entry point, but no library termination function.
Fortunately the PM API takes care of this with its WinInitialize and
WinTerminate functions. On the WinTerminate call-or on
WinDestroyMsgQueue-the WinPM DLL can release any resources it has allocated
for that application.


A Reader Exercise

Due to publication deadlines, there are several things I left unimplemented
in Sleuth and in WinPM and MacPM. If you would like to try spiffing it up a
bit, here are some areas you could improve.

The detail windows in Sleuth (see Figure 15☼) don't exist. To make them work,
you'd have to add some code, but this should require little or no change to
WinPM and MacPM as far as creating and destroying these windows; the code's
already there for that. It would take a little work to get double-click
messages from MacPM. This isn't hard, but MpmMsgMouse would have to keep
track of the time and position of the last mouse-up, and check each mouse-
down to see if it looks like a double click. Then it would simply generate a
WM_BUTTON1DBLCLK and not a WM_BUTTON1DOWN. The "Show Detail" menu item
should do the same thing as a double-click. This means that Sleuth should
have some code for selecting a line with a single click or with the cursor
keys. MacPM has to be able to generate keystroke messages for this.

There's an "About Sleuth..." menu item, but it doesn't do anything. Dialog
boxes are an area that I omitted completely from MacPM and WinPM, so it
would take some work (and careful thought) to implement them.

One little thing in MacPM that you could fix is the group of items on the
Edit menu, which should be grayed out when Sleuth's own windows are active.
Those items are really there for desk accessories and should be enabled only
when a desk accessory is active. Also, they need to call the SystemEdit
function to make them actually work. There's some debugging to do with desk
accessories; there are really three different environments they can run in:
non-MultiFinder, MultiFinder using the DA Handler layer, and MultiFinder
using the application layer. (The last choice is what happens if you hold
the Option key down while starting a desk accessory under MultiFinder.) I
got desk accessories working in some cases but not in all these situations.

I had mentioned earlier a couple of places where the WM_QUERYMINMAXINFO
message should be sent, but MacPM and WinPM just choose arbitrary rectangles
instead. There's a bug in MacPM where, if a scroll bar is disabled and you
click in it, the system seems to freeze until you click the mouse somewhere
else ten (yes, ten) times. Then it suddenly catches up with all those
clicks. Finally, if you're feeling really ambitious, you could try
implementing the SB_SLIDERTRACK message. You'll find some commented-out
code in MacPM where I tried to get this one working. Have fun!


No Free Lunch

As we've seen, there is no easy solution to the problem of portability
across windowing environments as disparate as Presentation Manager,
Windows, and the Macintosh. Is it worth the work? Perhaps I should ask
instead if you can afford not to do it. Each of these systems represents a
large (or in the case of Presentation Manager, soon to be very large)
market, and people really appreciate having the same or similar software
available on both the Mac and PC. The approach I've taken with MacPM and
WinPM seems like a fruitful one, but I sure would have liked it if someone
else had done the dirty work for me! Depending on your application, a
toolkit like XVT might do a good job for you. My personal hope is that C++
with some good class libraries will make porting much easier. But I suspect
that regardless of how good the tools get, the old saying will still be
true: there's no free lunch.


Figure 1:  Complete Code Listing for the PM Version of Sleuth

SLEUTH.C - Source code listing for Sleuth

/* Sleuth.c  */

#define INCL_DOSPROCESS
#define INCL_GPI
#define INCL_WIN
#undef NULL

#include <os2.h>
#include <malloc.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "Sleuth.h"

/*-----------------------------------------------------------------*/
/*  The display for a window looks like this in collapsed mode:    */
/*                                                                 */
/*  Window HHHH:HHHH [id] {class} (L,B;R,T) "text"                 */
/*                                                                 */
/*  or like this in expanded mode:                                 */
/*                                                                 */
/*      Window handle: HHHH:HHHH   Owner window: HHHH:HHHH         */
/*        Class name: {class name}                                 */
/*        Window text: "text"                                      */
/*        Class style:  HHHHHHHH                                   */
/*        Window style: HHHHHHHH                                   */
/*        Class function: HHHH:HHHH   Window function: HHHH:HHHH   */
/*        Window ID: DDDDD   Process ID: HHHH   Thread ID: HHHH    */
/*        Message queue handle: HHHH   Window lock count: DDDDD    */
/*        Window extra alloc: DDDDD                                */
/*        Window rectangle: Left=D, Bottom=D, Right=D, Top=D       */
/*      {blank line}                                               */
/*                                                                 */
/*  Total number of lines for one window display: 11               */
/*-----------------------------------------------------------------*/

#define LINES_PER_WINDOW    11
#define WINDOW_WIDTH        160

/* Structure of information for each window. */

#define CLASSMAX    40
#define TEXTMAX     40

typedef struct {
    HWND    hwnd;                   /* Window handle */
    CLASSINFO class;                /* Class info */
    CHAR    szClass[CLASSMAX];      /* Class name */
    CHAR    szText[TEXTMAX];        /* Window title or contents */
    ULONG   flStyle;                /* WS_ window style */
    HWND    hwndOwner;              /* Owner window handle */
    PFNWP   pfnwp;                  /* Window function */
    USHORT  usWindowID;             /* Window ID */
    PID     ProcessID;              /* Process ID */
    TID     ThreadID;               /* Thread ID */
    HMQ     hmq;                    /* Message queue handle */
    RECTL   rclWindow;              /* Window rect, screen coord. */
    SHORT   sLockCount;             /* Window lock count */
    SHORT   sLevel;                 /* Child window nesting level */
    FLAG    fSelect:1;              /* Is this window selected? */
    FLAG    fHasText:1;             /* Does the window have text? */
} INFO;

typedef INFO * PINFO;               /* Pointer to INFO array */

/* Static variables. */

PIDINFO     pidi;

HAB         hab;
HMQ         hmq;
HWND        hwndDesktop, hwndObject;
HWND        hwndSleuth, hwndFrame, hwndHorzScroll, hwndVertScroll;

PINFO       pInfoBase;
SHORT       sWinCount;              /* # of windows in system */
BOOL        fExpand = FALSE;        /* Expanded display mode? */
SHORT       sLinesEach = 1;         /* 1 or LINES_PER_WINDOW */
SHORT       sCharX;                 /* Character width in pixels */
SHORT       sCharY;                 /* Character height in pixels */
SHORT       sDescenderY;            /* Descender height */
SHORT       sExtLeading;            /* Vert. space between chars */
HPS         hpsPaint;               /* PS for SleuthPaint */
POINTL      ptlPaint;               /* Current pos for SleuthPaint */
CHAR        szClass[10];            /* Our window class name */
CHAR        szTitle[40];            /* Our window title */

/* Function prototypes. */

VOID                main( VOID );
PSZ                 SleuthClassName( PSZ );
BOOL                SleuthGetAllWindows( VOID );
VOID                SleuthGetWindowTree( HWND hwnd, SHORT sLevel );
BOOL                SleuthInit( VOID );
PSZ                 SleuthMsgName( USHORT );
VOID                SleuthPaint( HWND hwndPaint );
VOID      CDECL     SleuthPaint1( CHAR* szFormat, ... );
VOID                SleuthQueryScrollBar( HWND hwndBar, SHORT* psPos,
                                          SHORT* psMin,
                                          SHORT* psMax );
SHORT               SleuthScroll( HWND hwnd, USHORT msg,
                                  USHORT idBar,
                                  USHORT cmd, SHORT sPos );
VOID                SleuthSetOneBar( HWND hwndBar, SHORT sMax );
VOID                SleuthSetScrollBars( VOID );
VOID                SleuthTerminate( VOID );
MRESULT   EXPENTRY  SleuthWndProc( HWND, USHORT, MPARAM, MPARAM );

/* Application main program. */

VOID main()
{
    QMSG        qmsg;

    /* Initialize application, quit if any errors */

    if( ! SleuthInit() )
      return;

    /* Main message processing loop */

    while( WinGetMsg( hab, &qmsg, NULL, 0, 0 ) )
      WinDispatchMsg( hab, &qmsg );

    /* Application termination */

    SleuthTerminate();
}

/* Convert a class name into its printable form.  Normal class     */
/* names are returned unchanged; the special WC_ "names" are       */
/* converted into text.                                            */

typedef struct _CLASSNAMES {
    NPSZ        szNum;
    NPSZ        szName;
} CLASSNAMES;

typedef CLASSNAMES NEAR * NPCLASSNAMES;

CLASSNAMES aClassNames[] = {
    { "#1",  "WC_FRAME"      },
    { "#2",  "WC_DIALOG"     },
    { "#3",  "WC_BUTTON"     },
    { "#4",  "WC_MENU"       },
    { "#5",  "WC_STATIC"     },
    { "#6",  "WC_ENTRYFIELD" },
    { "#7",  "WC_LISTBOX"    },
    { "#8",  "WC_SCROLLBAR"  },
    { "#9",  "WC_TITLEBAR"   },
    { "#10", "WC_SIZEBORDER" },
    { NULL,  NULL            }
};

PSZ SleuthClassName( pszClass )
    PSZ         pszClass;
{
    NPCLASSNAMES npNames;

    if( pszClass[0] != '#' )
      return pszClass;

    for( npNames = &aClassNames[0];  npNames->szNum;  npNames++ )
      if( strcmp( pszClass, npNames->szNum ) == 0 )
        return npNames->szName;

    return pszClass;
}

/* Gather up information on all windows in PM and fill in the      */
/* INFO structure for them.                                        */

BOOL SleuthGetAllWindows()
{
    sWinCount = 0;

    /* Pick up both trees, from hwndDesktop and hwndObject */

    SleuthGetWindowTree( hwndDesktop, 0 );
    SleuthGetWindowTree( hwndObject,  0 );

    /* Set scroll bars based on new window count */

    SleuthSetScrollBars();

    /* Force our window to be repainted */

    WinInvalidateRect( hwndSleuth, NULL, TRUE );

    return TRUE;
}

/* Gather information on all windows in the tree starting at hwnd, */
/* and add an entry to the INFO array for each one.                */

VOID SleuthGetWindowTree( hwnd, sLevel )
    HWND        hwnd;
    SHORT       sLevel;
{
    PINFO       pInfo;
    HWND        hwndChild;

    /* Count the window and allocate an INFO entry */

    sWinCount++;

    if( ! pInfoBase )
      pInfoBase = malloc( sizeof(INFO) );
    else
      pInfoBase = realloc( pInfoBase, sWinCount*sizeof(INFO) );

    if( ! pInfoBase )
      exit( 9 );

    pInfo = pInfoBase + sWinCount - 1;  /* -> INFO for this window */

    /* Gather up this window's information */

    pInfo->hwnd = hwnd;
    pInfo->fSelect = FALSE;
    pInfo->fHasText = FALSE;
    pInfo->class.flClassStyle  = 0L;
    pInfo->class.pfnWindowProc = 0L;
    pInfo->class.cbWindowData  = 0;

    pInfo->flStyle = WinQueryWindowULong( hwnd, QWL_STYLE );
    pInfo->hwndOwner = WinQueryWindow( hwnd, QW_OWNER, FALSE );
    pInfo->pfnwp = WinQueryWindowPtr( hwnd, QWP_PFNWP );
    pInfo->usWindowID = WinQueryWindowUShort( hwnd, QWS_ID );
    WinQueryWindowProcess( hwnd, &pInfo->ProcessID,
                                 &pInfo->ThreadID );
    pInfo->hmq = WinQueryWindowPtr( hwnd, QWL_HMQ );
    WinQueryWindowRect( hwnd, &pInfo->rclWindow );
    pInfo->sLockCount = WinQueryWindowLockCount( hwnd );
    pInfo->sLevel = sLevel;

    if( hwnd == hwndDesktop )
      strcpy( pInfo->szClass, "WC_DESKTOP" );
    else if( hwnd == hwndObject )
      strcpy( pInfo->szClass, "WC_OBJECT" );
    else
    {
      WinQueryClassName( hwnd, sizeof(pInfo->szClass),
                         pInfo->szClass );
      WinQueryClassInfo( hab, pInfo->szClass, &pInfo->class );
      if( ! WinIsRectEmpty( hab, &pInfo->rclWindow ) )
        WinMapWindowRect( hwnd, WinQueryWindow(hwnd,QW_PARENT,FALSE),
                          &pInfo->rclWindow );
    }

    pInfo->szText[0] = '\0';
    if( pInfo->ProcessID == pidi.pid )
      pInfo->fHasText =  /* wrong... */
        !! WinQueryWindowText( hwnd, sizeof(pInfo->szText),
                               pInfo->szText );
/* Recurse through all child windows */
    for( hwndChild = WinQueryWindow( hwnd, QW_TOP, FALSE );
         hwndChild;
         hwndChild = WinQueryWindow( hwnd, QW_NEXT, FALSE ) )
       SleuthGetWindowTree( hwndChild, sLevel+1 );
}

/* Initialize the application. */

BOOL SleuthInit()
{
    HDC         hps;
    FONTMETRICS fm;
    SHORT       sScreenX;
    SHORT       sScreenY;
    ULONG       flFrameFlags;

    /* Pick up the basic information we need */

    DosGetPID( &pidi );

    hab = WinInitialize( 0 );
    hmq = WinCreateMsgQueue( hab, 0 );

    hwndDesktop = WinQueryDesktopWindow( hab, NULL );
    hwndObject  = WinQueryObjectWindow( hwndDesktop );

    sScreenX = (SHORT)WinQuerySysValue( hwndDesktop, SV_CXSCREEN );
    sScreenY = (SHORT)WinQuerySysValue( hwndDesktop, SV_CYSCREEN );

    /* Calculate character size for system font */

    hps = WinGetPS( hwndDesktop );

    GpiQueryFontMetrics( hps, (LONG)sizeof(fm), &fm );

    sCharX = (SHORT)fm.lAveCharWidth;
    sCharY = (SHORT)fm.lMaxBaselineExt;
    sDescenderY = (SHORT)fm.lMaxDescender;

    WinReleasePS( hps );

    WinLoadString( hab, NULL, IDS_CLASS, sizeof(szClass), szClass );
    WinLoadString( hab, NULL, IDS_TITLE, sizeof(szTitle), szTitle );

    WinRegisterClass( hab, szClass, SleuthWndProc, 0L, 0 );

        flFrameFlags = FCF_SIZEBORDER | FCF_TITLEBAR |
                        FCF_MINMAX | FCF_SYSMENU | FCF_MENU | FCF_ICON |
                        FCF_VERTSCROLL | FCF_HORZSCROLL |
                        FCF_SHELLPOSITION;
        hwndFrame =
        WinCreateStdWindow (hwndDesktop, WS_VISIBLE, &flFrameFlags,
                                szClass, szTitle, 0L, NULL,
                                ID_SLEUTH, &hwndSleuth) ;

    hwndHorzScroll = WinWindowFromID( hwndFrame, FID_HORZSCROLL );
    hwndVertScroll = WinWindowFromID( hwndFrame, FID_VERTSCROLL );

    WinSetWindowPos( hwndFrame, NULL,
                     sScreenX *  1 / 20,     /* X: 5% from left */
                     sScreenY *  2 / 10,     /* Y  20% from bottom */
                     sScreenX *  9 / 10,     /* nWidth: 90% */
                     sScreenY *  7 / 10,     /* nHeight: 70% */
                     SWP_MOVE | SWP_SIZE );

    /* Make our window visible now, so it's included in the list */

    WinShowWindow( hwndFrame, TRUE );

    /* Post a message to ourself to trigger the first  display */

    WinPostMsg( hwndSleuth, WM_COMMAND, MPFROMSHORT(CMD_LOOK), 0L );

    return TRUE;
}

/* Paint our window. */

VOID SleuthPaint( hwnd )
    HWND        hwnd;
{
    SHORT       sWin;
    SHORT       X;
    SHORT       sScrollY, sScrollX;
    RECTL       rclPaint, rclClient;
    PINFO       pInfo;
    CHAR        szQuote[2];

    /* Get the PS and erase it */

    hpsPaint = WinBeginPaint( hwnd, NULL, &rclPaint );

    GpiErase( hpsPaint );

    /* Find out how big the window is and how it's scrolled */

    WinQueryWindowRect( hwnd, &rclClient );
    sScrollX =
      (SHORT)WinSendMsg( hwndHorzScroll, SBM_QUERYPOS, 0, 0 );
    sScrollY =
      (SHORT)WinSendMsg( hwndVertScroll, SBM_QUERYPOS, 0, 0 );

    /* Calculate horizontal paint pos from scroll bar pos */

    X = /* ( 1 */ - sScrollX /* ) */ * sCharX;

    /* Calculate index into INFO array and vertical paint pos,
       from scroll bar pos and top of painting rectangle */

    sWin =
      ( ( (SHORT)rclClient.yTop - (SHORT)rclPaint.yTop ) / sCharY
        + sScrollY )
      / sLinesEach;

    ptlPaint.y =
      (SHORT)rclClient.yTop + sDescenderY
        - ( sWin * sLinesEach - sScrollY + 1 ) * sCharY;

    pInfo = pInfoBase + sWin;

    /* Loop through and paint each entry */

    while( sWin < sWinCount  &&
           (SHORT)ptlPaint.y + sCharY >= (SHORT)rclPaint.yBottom )
    {
      /* Set X position and indent child windows */

      ptlPaint.x = X + pInfo->sLevel * sCharX * (fExpand ? 4 : 2);

      szQuote[0] = szQuote[1] = '\0';
      if( pInfo->fHasText )
        szQuote[0] = '"';

      if( ! fExpand )
      {
        /* Paint the one-liner */

        SleuthPaint1(
          "%08lX [%04X] {%s} (%d,%d;%d,%d) %s%s%s",
          pInfo->hwnd,
          pInfo->usWindowID,
          SleuthClassName( pInfo->szClass ),
          (INT)pInfo->rclWindow.xLeft,
          (INT)pInfo->rclWindow.yBottom,
          (INT)pInfo->rclWindow.xRight,
          (INT)pInfo->rclWindow.yTop,
          szQuote, pInfo->szText, szQuote
        );
      }

      /* Increment to next INFO entry */
      sWin++;
      pInfo++;
    }

    WinEndPaint( hpsPaint );
}

/* Paint one line of text, using the global variables hpsPaint and */
/* ptlPaint.  The #ifdef PM_MACINTOSH is because Lightspeed C      */
/* doesn't like the ... notation, and Microsoft C doesn't like to  */
/* do without it!                                                  */

#ifdef PM_MACINTOSH
VOID CDECL SleuthPaint1( szFormat )
#else
VOID CDECL SleuthPaint1( szFormat, ... )
#endif
    CHAR *      szFormat;
{
    va_list     pArgs;
    CHAR        szBuf[160];

    va_start( pArgs, szFormat );

    GpiCharStringAt(
      hpsPaint, &ptlPaint,
      (LONG)vsprintf( szBuf, szFormat, pArgs ),
      szBuf
    );

    va_end( pArgs );

    ptlPaint.y -= sCharY;
}

/* Get a scroll bar's range and position.  More convenient than    */
/* sending the messages every time.                                */

VOID SleuthQueryScrollBar( hwndBar, psPos, psMin, psMax )
    HWND        hwndBar;
    SHORT*      psPos;
    SHORT*      psMin;
    SHORT*      psMax;
{
    MRESULT     mrRange;

    *psPos  = (SHORT)WinSendMsg( hwndBar, SBM_QUERYPOS, 0, 0 );

    mrRange = WinSendMsg( hwndBar, SBM_QUERYRANGE, 0, 0 );
    *psMin = SHORT1FROMMR(mrRange);
    *psMax = SHORT2FROMMR(mrRange);
}

/* Scroll hwnd and adjust idBar according to cmd and sPos. */

SHORT SleuthScroll( hwnd, msg, idBar, cmd, sPos )
    HWND        hwnd;
    USHORT      msg;
    USHORT      idBar;
    USHORT      cmd;
    SHORT       sPos;
{
    HWND        hwndBar;
    SHORT       sOldPos;
    SHORT       sDiff;
    SHORT       sMin;
    SHORT       sMax;
    SHORT       sPageSize;
    RECTL       rcl;

    /* Get old scroll position and scroll range */

    hwndBar =
      WinWindowFromID( WinQueryWindow(hwnd,QW_PARENT,FALSE), idBar );

    SleuthQueryScrollBar( hwndBar, &sOldPos, &sMin, &sMax );

    /* Calculate page size, horizontal or vertical as needed */

    WinQueryWindowRect( hwnd, &rcl );

    if( msg == WM_HSCROLL )
      sPageSize = ( (SHORT)rcl.xRight - (SHORT)rcl.xLeft) / sCharX;
    else
      sPageSize = ( (SHORT)rcl.yTop - (SHORT)rcl.yBottom) / sCharY;

    /* Select the amount to scroll by, based on the scroll message */

    switch( cmd )
    {
      case SB_LINEUP:
        sDiff = -1;
        break;

      case SB_LINEDOWN:
        sDiff = 1;
        break;

      case SB_PAGEUP:
        sDiff = -sPageSize;
        break;

      case SB_PAGEDOWN:
        sDiff = sPageSize;
        break;

      case SB_SLIDERPOSITION:
        sDiff = sPos - sOldPos;
        break;

      case SBX_TOP:
        sDiff = -30000;  /* Kind of a kludge but it works... */
        break;

      case SBX_BOTTOM:
        sDiff = 30000;
        break;

      default:
        return 0;
    }

    /* Limit scroll destination to sMin..sMax */

    if( sDiff < sMin - sOldPos )
      sDiff = sMin - sOldPos;

    if( sDiff > sMax - sOldPos )
      sDiff = sMax - sOldPos;

    /* Return if net effect is nothing */

    if( sDiff == 0 )
      return 0;

    /* Set the new scroll bar position and scroll the window */

    WinSendMsg( hwndBar, SBM_SETPOS, MRFROMSHORT(sOldPos+sDiff), 0 );

    WinScrollWindow(
      hwnd,
      msg == WM_HSCROLL ?  -sDiff*sCharX : 0,
      msg == WM_VSCROLL ?   sDiff*sCharY : 0,
      NULL, NULL, NULL, NULL,
      SW_INVALIDATERGN
    );

    /* Force an immediate update for cleaner appearance */

    WinUpdateWindow( hwnd );

    return sDiff;
}

/* Set one scroll bar's position and range. */

VOID SleuthSetOneBar( hwndBar, sMax )
    HWND        hwndBar;
    SHORT       sMax;
{
    SHORT       sPos, sOldMin, sOldMax;

    SleuthQueryScrollBar( hwndBar, &sPos, &sOldMin, &sOldMax );

    if( sMax <= 0 )
      sMax = sPos = 0;

    if( sMax != sOldMax )
    {
      WinSendMsg( hwndBar, SBM_SETSCROLLBAR,
                  MPFROMSHORT(sPos), MPFROM2SHORT(0,sMax) );

      WinEnableWindow( hwndBar, !!(sMax) );
    }
}

/* Set both scroll bars according to the window size and the       */
/* number of INFO entries.                                         */

VOID SleuthSetScrollBars()
{
    RECTL       rcl;

    WinQueryWindowRect( hwndSleuth, &rcl );

    SleuthSetOneBar( hwndHorzScroll,
                     WINDOW_WIDTH - (SHORT)rcl.xRight / sCharX );

    SleuthSetOneBar( hwndVertScroll,
                     sWinCount*sLinesEach - (SHORT)rcl.yTop/sCharY );
}

/* Terminate the application. */

VOID SleuthTerminate()
{
    WinDestroyWindow( hwndFrame );
    WinDestroyMsgQueue( hmq );
    WinTerminate( hab );

    exit( 0 );
}

/* Window function for Sleuth's main window. */

MRESULT EXPENTRY SleuthWndProc( hwnd, msg, mp1, mp2 )
    HWND          hwnd;
    USHORT        msg;
    MPARAM        mp1;
    MPARAM        mp2;
{
    switch( msg )
    {
      /* Tell PM that we don't need no stinkin' upside down coordinates! */

      case WM_CALCVALIDRECTS:
        return MRFROMSHORT( CVR_ALIGNLEFT | CVR_ALIGNTOP );

      /* Menu command message - process the command */

      case WM_COMMAND:
        switch( COMMANDMSG(&msg)->cmd )
        {
          case CMD_ABOUT:
            return 0L;

          case CMD_EXIT:
            WinPostMsg( hwnd, WM_QUIT, 0L, 0L );
            return 0L;

          case CMD_LOOK:
            SleuthGetAllWindows();
            return 0L;
        }
        return 0L;

        /* Scroll messages - scroll the window */

        case WM_HSCROLL:
        case WM_VSCROLL:
          SleuthScroll( hwnd, msg, SHORT1FROMMP(mp1),
                        SHORT2FROMMP(mp2), SHORT1FROMMP(mp2) );
          return 0L;

        /* Key-down message - handle cursor keys, ignore others */

        case WM_CHAR:
          switch( CHARMSG(&msg)->vkey )
          {
            case VK_LEFT:
            case VK_RIGHT:
              return WinSendMsg( hwndHorzScroll, msg, mp1, mp2 );
            case VK_UP:
            case VK_DOWN:
            case VK_PAGEUP:
            case VK_PAGEDOWN:
              return WinSendMsg( hwndVertScroll, msg, mp1, mp2 );
          }
          return 0L;

        /* Paint message - repaint all or part of our window */

        case WM_PAINT:
          SleuthPaint( hwnd );
          return 0L;

        /* Size message - recalculate our scroll bars */

        case WM_SIZE:
          SleuthSetScrollBars();
          return 0L;
    }

    /* All other messages go to DefWindowProc */

    return WinDefWindowProc( hwnd, msg, mp1, mp2 );
}


Figure 6:  MacPM WinInitialize

HAB APIENTRY WinInitialize( fOptions )
    USHORT      fOptions;
{
    /* Static initialization - less hassle this way? */

    _pps1 = &_ps1;
    _hps1 = (HPS)&_pps1;

    /* Macintosh memory management initialization */

    MaxApplZone();
    MoreMasters();
    MoreMasters();
    MoreMasters();

    /* Open resource file - TEMP */

    OpenResFile( "\pMacPM.rsrc" );

    return (HAB)1;
}


Figure 7:  MacPM WinCreateMsgQueue

HMQ APIENTRY WinCreateMsgQueue( hab, sSize )
    HAB         hab;
    SHORT       sSize;
{
    PMYWND      pwnd;

  /* Generic Macintosh initialization (already did memory stuff) */

    InitGraf( &thePort );
    InitFonts();
    FlushEvents( everyEvent, 0 );
    SetEventMask( everyEvent );
    InitWindows();
    InitMenus();
    TEInit();
    InitDialogs( 0L );
    InitCursor();

  /* Initialize the SV_ values and register the predefined window classes */

    MpmInitSysValues();

    if( ! WinRegisterClass( hab, WC_BUTTON, MpmFnwpButton,
                            CS_MOVENOTIFY | CS_PUBLIC, 0 ) )
      return NULL;

    if( ! WinRegisterClass( hab, WC_DESKTOP, MpmFnwpDesktop,
                            CS_PUBLIC, 0 ) )
      return NULL;

#ifdef FUTURE
    if( ! WinRegisterClass( hab, WC_DIALOG, MpmFnwpDialog,
                            CS_MOVENOTIFY | CS_PUBLIC, 0 ) )
      return NULL;
#endif

    if( ! WinRegisterClass( hab, WC_ENTRYFIELD, MpmFnwpEntryField,
                            CS_MOVENOTIFY | CS_PUBLIC, 0 ) )
      return NULL;

    if( ! WinRegisterClass( hab, WC_FRAME, MpmFnwpFrame,
                            CS_MOVENOTIFY | CS_PUBLIC, 0x20 /*??*/ ) )
      return NULL;

    if( ! WinRegisterClass( hab, WC_LISTBOX, MpmFnwpListBox,
                            CS_MOVENOTIFY | CS_PUBLIC, 0 ) )
      return NULL;

    if( ! WinRegisterClass( hab, WC_MENU, MpmFnwpMenu,
                            CS_MOVENOTIFY | CS_PUBLIC, 0 ) )
      return NULL;

    if( ! WinRegisterClass( hab, WC_SCROLLBAR, MpmFnwpScrollBar,
                            CS_MOVENOTIFY | CS_PUBLIC, 0 ) )
      return NULL;

    if( ! WinRegisterClass( hab, WC_SIZEBORDER, MpmFnwpSizeBorder,
                            CS_MOVENOTIFY | CS_PUBLIC, 0 ) )
      return NULL;

    if( ! WinRegisterClass( hab, WC_STATIC, MpmFnwpStatic,
                            CS_MOVENOTIFY | CS_PUBLIC, 0 ) )
      return NULL;

    if( ! WinRegisterClass( hab, WC_TITLEBAR, MpmFnwpTitleBar,
                            CS_MOVENOTIFY | CS_PUBLIC, 0 ) )
      return NULL;

    /* Create the object and desktop windows */

    _hwndObject =
      WinCreateWindow(
        NULL, WC_DESKTOP, _szNull,
        0, 0, 0, 0, 0,
        NULL, NULL, 0, NULL, NULL
      );
    if( ! _hwndObject )
      return NULL;

    pwnd = PMYWNDOF(_hwndObject);
    pwnd->ucKind = WK_OBJECT;
    pwnd->pfnwp  = MpmFnwpObject;

    _hwndDesktop =
      WinCreateWindow(
        NULL, WC_DESKTOP, _szNull,
        WS_DISABLED,
        0, 0, 0, 0,
        NULL, NULL, 0, NULL, NULL
      );
    if( ! _hwndDesktop )
      return NULL;

    pwnd = PMYWNDOF(_hwndDesktop);
    pwnd->cx = _alSysVal[SV_CXSCREEN];
    pwnd->cy = _alSysVal[SV_CYSCREEN];
    pwnd->flStyle |= WS_VISIBLE;

    return (HMQ)1;
}


Figure 8:  MacPM WinCreateWindow

HWND APIENTRY
WinCreateWindow( hwndParent, pszClass,  pszName, flStyle,
                 x, y, cx, cy, hwndOwner, hwndPrevSibling, id,
                 pCtlData, pPresParams )
    HWND        hwndParent, hwndOwner, hwndPrevSibling;
    PSZ         pszClass, pszName;
    ULONG       flStyle;
    SHORT       x, y, cx, cy;
    USHORT      id;
    PVOID       pCtlData, pPresParams;
{
    HWND        hwnd, hwndNextSibling, hwndMac;
    PMYWND      pwnd;
    HMYCLASS    hClass;
    PMYCLASS    pClass;
    USHORT      usLen;
    SHORT       sProcID;
    Rect        rect, rectAdj;
    WindowPeek  pwin, pwinBehind;
    CHAR        szTitle[256];
    BOOL        fGoAway, fFrameWindow;
    UCHAR       ucKind;
    USHORT      usHash;
    CHAR        szClassBuf[10];
    ULONG       flFrameFlags;

    /* Figure out which WK_ code to use */

    if( pszClass == WC_DESKTOP )
      ucKind = WK_DESKTOP;
    else
    {
      if( ! hwndParent  ||  hwndParent == HWND_DESKTOP )
        hwndParent = _hwndDesktop;
      if( hwndParent == _hwndDesktop )
        ucKind = WK_MAIN;
      else
        ucKind = WK_CHILD;
    }

    /* Fix up WC_ names, calculate hash, and find window class */

    MpmFixName( &pszClass, szClassBuf, &usHash );

    hClass = MpmFindClass( pszClass, usHash );
    if( ! hClass )
      return NULL;

    /* Grab frame flags if it's the window frame class */

    fFrameWindow = MpmIsFrameClass( pszClass );
    flFrameFlags =
      ( fFrameWindow && pCtlData ? *(PULONG)pCtlData : 0 );

    /* Allocate window structure */

    usLen = sizeof(MYWND) + (**hClass).class.cbWindowData;
    hwnd = (HWND)MpmNewHandleZ( usLen );
    if( ! hwnd )
      return NULL;

    /* Handle WK_ specific stuff */

    switch( ucKind )
    {
      /* Desktop window: just set window position */

      case WK_DESKTOP:
        SetRect( &rect, 0, 0, cx, cy );
        SetRect( &rectAdj, 0, 0, 0, 0 );
        break;

      /* Main Mac window: figure out which style of Mac window to use and
         create it */

      case WK_MAIN:
        fGoAway = FALSE;
        if( ! ( flStyle & FS_BORDER )  &&
            ! ( flFrameFlags & FCF_DOCBITS ) )
        {
          sProcID = plainDBox;  /* shouldn't have border! */
          rectAdj = _rectAdjPlainDBox;
        }
        else if( flFrameFlags & FCF_DOCBITS )
        {
          sProcID = documentProc;
          rectAdj = _rectAdjDocumentProc;
          if( flFrameFlags & FCF_MAXBUTTON )
            sProcID += 8;
          if( flFrameFlags & FCF_SYSMENU )
            fGoAway = TRUE;
        }
        else if( flStyle & FS_DLGBORDER )
        {
          sProcID = dBoxProc;
          rectAdj = _rectAdjDBoxProc;
        }
        else
        {
          sProcID = altDBoxProc;
          rectAdj = _rectAdjAltDBoxProc;
        }

        rect.left   = rectAdj.left   + x;
        rect.right  = rectAdj.right  + x + cx;
        rect.top    = rectAdj.top    + rect.bottom - cy;
        rect.bottom = rectAdj.bottom + screenBits.bounds.bottom - y;

        /* Should check rect for out of screen boundaries! */

        strncpy( szTitle, pszName, 255 );
        szTitle[255] = '\0';

        if( hwndPrevSibling == HWND_TOP )
          pwinBehind = (WindowPeek)(-1);
        else if( hwndPrevSibling == HWND_BOTTOM )
          pwinBehind = (WindowPeek)NULL;
        else if( ISMAINWINDOW(hwndPrevSibling) )
          pwinBehind = PWINOFHWND(hwndPrevSibling);
        else
          ERROR( "WinCreateWindow: Invalid hwndPrevSibling" );

        pwin =
          (WindowPeek)NewWindow( NULL, &rect, CtoPstr(szTitle),
                                 FALSE, sProcID, pwinBehind,
                                 fGoAway, (LONG)hwnd );
        if( ! pwin )
        {
          DisposHandle( (Handle)hwnd );
          return NULL;
        }
        PWINOFHWND(hwnd) = pwin;

        break;

      /* Child window: set up all the "kin" windows */

      case WK_CHILD:
        if( hwndPrevSibling == HWND_TOP )
        {
          MYWNDOF(hwnd).hwndNextSibling = hwndNextSibling =
            MYWNDOF(hwndParent).hwndTopChild;
          MYWNDOF(hwndNextSibling).hwndPrevSibling = hwnd;
          MYWNDOF(hwndParent).hwndTopChild = hwnd;
        }
        else if( hwndPrevSibling == HWND_BOTTOM )
        {
          MYWNDOF(hwnd).hwndPrevSibling = hwndPrevSibling =
            MYWNDOF(hwndParent).hwndBottomChild;
          MYWNDOF(hwndPrevSibling).hwndNextSibling = hwnd;
          MYWNDOF(hwndParent).hwndBottomChild = hwnd;
        }
        else
        {
          if( ! MpmValidateWindow(hwndPrevSibling) )
            return NULL;
          if( MYWNDOF(hwndPrevSibling).hwndParent != hwndParent )
            return NULL;
          MYWNDOF(hwnd).hwndNextSibling = hwndNextSibling =
            MYWNDOF(hwndPrevSibling).hwndNextSibling;
          MYWNDOF(hwnd).hwndPrevSibling = hwndPrevSibling;
          MYWNDOF(hwndPrevSibling).hwndNextSibling = hwnd;
          if( hwndNextSibling )
            MYWNDOF(hwndNextSibling).hwndPrevSibling = hwnd;
          else
            MYWNDOF(hwndParent).hwndBottomChild = hwnd;
        }
        if( ! MYWNDOF(hwndParent).hwndTopChild )
          MYWNDOF(hwndParent).hwndTopChild = hwnd;
        if( ! MYWNDOF(hwndParent).hwndBottomChild )
          MYWNDOF(hwndParent).hwndBottomChild = hwnd;
        for( hwndMac = hwndParent;
             ISCHILDWINDOW(hwndMac);
             hwndMac = MYWNDOF(hwndMac).hwndParent );
        PWINOFHWND(hwnd) = PWINOFHWND(hwndMac);
        rectAdj = MYWNDOF(hwndMac).rectAdj;
        break;
    }

    /* Fill in the window structure fields */

    pClass = *hClass;
    pwnd = PMYWNDOF(hwnd);

    pwnd->signature = WND_SIGNATURE;
    pwnd->ucKind = ucKind;
    pwnd->pfnwp = pClass->class.pfnWindowProc;
    pwnd->hclass = hClass;
    pwnd->flStyle =
      ( flStyle & ~WS_VISIBLE ) |
      ( pClass->class.flClassStyle & CLASSWINDOWBITS );
    pwnd->hwndOwner = hwndOwner;
    pwnd->hwndParent = hwndParent;
    pwnd->id = id;
    pwnd->rectAdj = rectAdj;
    pwnd->flFrameFlags = flFrameFlags;

    /* Now the window is here for real, so send the WM_CREATE */

    if( WinSendMsg( hwnd, WM_CREATE,
                    MPFROMP(pCtlData), MPFROMP(&hwndParent) ) )
    {
      WinDestroyWindow( hwnd );
      return NULL;
    }

    /* Send the WM_ADJUSTWINDOWPOS if it's not the desktop window
       and it has a nonzero size */

    if( cx  &&  cy  &&  ucKind != WK_DESKTOP )
      WinSetWindowPos( hwnd, NULL, x, y, cx, cy, SWP_MOVE | SWP_SIZE );

    /* Make the window visible if it's supposed to be visible */

    if( flStyle & WS_VISIBLE )
      WinShowWindow( hwnd, TRUE );

    return hwnd;
}


Figure 9:  MacPM WinCreateStdWindow

HWND APIENTRY WinCreateStdWindow( hwndParent, flStyle, pCtlData,
                                  pszClassClient, pszTitle,
                                  flStyleClient, hmod, idResources,
                                  phwndClient )
    HWND        hwndParent;
    ULONG       flStyle, flStyleClient;
    PVOID       pCtlData;
    PSZ         pszClassClient, pszTitle;
    HMODULE     hmod;
    USHORT      idResources;
    PHWND       phwndClient;
{
    HWND        hwndFrame, hwndClient;
    SHORT       x, y, cx, cy;
    ULONG       flFrameFlags;

    /* Pick up frame flags, take care of special hwndParent values */

    flFrameFlags = ( pCtlData ? *(PULONG)pCtlData : 0 );

    if( ! hwndParent || hwndParent == HWND_DESKTOP )
      hwndParent = _hwndDesktop;

    ASSERT( hwndParent == _hwndDesktop,
            "WinCreateStdWindow: Must be a top level window" );
    ASSERT( ! hmod,
            "WinCreateStdWindow: hmod must be NULL" );
    ASSERT( ! (flStyle & WS_CLIPCHILDREN),
            "WinCreateStdWindow: WS_CLIPCHILDREN not allowed" );

    /* Assign default position if visible */

    if( flStyle & WS_VISIBLE )
    {
      x = 10;       /* TEMP HACK */
      y = screenBits.bounds.bottom - 200;
      cx = 250;
      cy = 150;
    }
    else
      x = y = cx = cy = 0;

    /* Create the frame window itself */

    hwndFrame =
      WinCreateWindow( hwndParent, WC_FRAME, _szNull,
                       flStyle & ~WS_VISIBLE, x, y, cx, cy,
                       NULL, HWND_TOP, idResources, pCtlData, NULL );

    if( ! hwndFrame )
      return NULL;

    /* Create frame controls according to flFrameFlags */

    MpmFrameUpdate( hwndFrame, flFrameFlags );

    /* Create client window if requested */

    if( pszClassClient && *pszClassClient )
    {
      *phwndClient = hwndClient =
        WinCreateWindow(
          hwndFrame, pszClassClient, _szNull, flStyleClient,
          0, 0, 0, 0, hwndFrame, HWND_BOTTOM, FID_CLIENT, NULL, NULL
        );
      if( ! hwndClient )
        goto exitDestroy;
    }

    /* Create menu and initialize title */

    if( flFrameFlags & FCF_MENU )
      if( ! MpmMenuLoad( hwndFrame, idResources ) )
        goto exitDestroy;

    if( flFrameFlags & FCF_TITLEBAR )
      WinSetWindowText( hwndFrame, pszTitle );

    /* Make window visible if requested */

    if( flStyle & WS_VISIBLE )
    {
      WinSendMsg( hwndFrame, WM_FORMATFRAME, 0L, 0L );
      WinShowWindow( hwndFrame, TRUE );
    }

    return hwndFrame;

exitDestroy:
    if( pszClassClient && *pszClassClient )
      *phwndClient = NULL;
    WinDestroyWindow( hwndFrame );
    return NULL;
}


Figure 10:  MacPM Windowfunction for Scroll Bar Controls

MRESULT EXPENTRY MpmFnwpScrollBar( hwnd, msg, mp1, mp2 )
    HWND        hwnd;
    USHORT      msg;
    MPARAM      mp1;
    MPARAM      mp2;
{
    USHORT      id;
    ControlHandle hctl, hctl1;
    Rect        rect;
    Rect*       prect;
    SHORT       sPart;
    POINTL      ptl;
    SHORT       cmd;
    Handle      hcdef;
    LONG        lPoint;
    Point       point;
    static BOOL fTrackThumb = FALSE;
    static SHORT sInitValue;

    if( ! MpmValidateWindow(hwnd) )
      return FALSE;

    id   = MYWNDOF(hwnd).id;
    hctl = MYWNDOF(hwnd).hctl;

    switch( msg )
    {
      /* Return the current position */

      case SBM_QUERYPOS:
        return MRFROMSHORT( fTrackThumb ? sInitValue
                                        : (**hctl).contrlValue );

      /* Return the scroll bar range */

      case SBM_QUERYRANGE:
        return MRFROM2SHORT( (**hctl).contrlMin, (**hctl).contrlMax );

      /* Set the scroll bar position and range */

      case SBM_SETSCROLLBAR:
        (**hctl).contrlMin = SHORT1FROMMP(mp2);
        (**hctl).contrlMax = SHORT2FROMMP(mp2);
        /* fall through */

      /* Set the scroll bar position only */

      case SBM_SETPOS:
        if( MYWNDOF(hwnd).flStyle & WS_VISIBLE )
          SetCtlValue( hctl, SHORT1FROMMP(mp1) );
        else
          (**hctl).contrlValue = SHORT1FROMMP(mp1);
        fTrackThumb = FALSE;
        return MRFROMSHORT(1);

      /* Handle mouse button down: call TrackControl to track it */

      case WM_BUTTON1DOWN:
        ptl.x = SHORT1FROMMP(mp1);
        ptl.y = SHORT2FROMMP(mp1);
        MpmMapMacOfPtl( hwnd, &point, &ptl );
        sPart = FindControl( point, PWINOFHWND(hwnd), &hctl1 );
        ASSERT( sPart  &&  hctl1 == hctl,
                "MpmFnwpScrollBar: FindControl failed" );
        _hwndTrack = hwnd;
        if( sPart == inThumb )
        {
          fTrackThumb = TRUE;
          sInitValue = (**hctl).contrlValue;
          if( TrackControl( hctl, point, NULL ) )
            MpmTrackScrollBarNotify( SB_SLIDERPOSITION );
          fTrackThumb = FALSE;
        }
        else
        {
          TrackControl( hctl, point, (ProcPtr)MpmTrackScrollBar );
          MpmTrackScrollBarNotify( SB_ENDSCROLL );
        }
        return 0L;

      /* Handle scroll bar creation: call NewControl to create it */

      case WM_CREATE:
        rect.left = rect.top = 0;
        if( MYWNDOF(hwnd).flStyle & SBS_VERT )
          rect.right = _alSysVal[SV_CXVSCROLL], rect.bottom = 100;
        else
          rect.bottom = _alSysVal[SV_CYHSCROLL], rect.right = 100;
        hctl = NewControl( PWINOFHWND(hwnd), &rect, _szNull,
                           MYWNDOF(hwnd).flStyle & WS_VISIBLE,
                           0, 0, 1, scrollBarProc, (long)hwnd );
        if( ! hctl )
          return MRFROMSHORT(1);
        MYWNDOF(hwnd).hctl = hctl;
        return 0L;

      /* Destroy scroll bar */

      case WM_DESTROY:
        MYWNDOF(hwnd).hctl = NULL;
        DisposeControl( hctl );
        return 0L;

      /* Handle window movement: use MoveControl to move the bar */

      case WM_MOVE:
        MpmQueryMacRect( hwnd, &rect );
        if( MYWNDOF(hwnd).flStyle & WS_VISIBLE )
          MoveControl( hctl, rect.left, rect.top );
        else
        {
          prect = &(**hctl).contrlRect;
          prect->right  += rect.left - prect->left;
          prect->left    = rect.left;
          prect->bottom += rect.top  - prect->top;
          prect->top     = rect.top;
        }
        return 0L;

      /* Show or hide the scroll bar */

      case WM_SHOW:
        if( mp1 )
          ShowControl( hctl );
        else
          HideControl( hctl );
        return 0L;

      /* Handle window resizing: use SizeControl to resize it */

      case WM_SIZE:
        if( SHORT1FROMMP(mp2) && SHORT2FROMMP(mp2) )
        {
          if( MYWNDOF(hwnd).flStyle & WS_VISIBLE )
            SizeControl( hctl,
                         SHORT1FROMMP(mp2),
                         SHORT2FROMMP(mp2) );
          else
          {
            prect = &(**hctl).contrlRect;
            prect->right  = prect->left + SHORT1FROMMP(mp2);
            prect->bottom = prect->top  + SHORT2FROMMP(mp2);
          }
        }
        return 0L;
    }

    return WinDefWindowProc( hwnd, msg, mp1, mp2 );
}


Figure 11:  Function to Load the Menu Bar from the Resource File

LOCAL BOOL MpmMenuLoad( hwndFrame, idResources )
    HWND        hwndFrame;
    USHORT      idResources;
{
    MbarHandle  hmbar;
    MbarPtr     pmbar;
    SHORT       sMenuCount;
    MenuHandle  hmenu;

    hmbar = (MbarHandle)GetNewMBar( idResources );
    if( ! hmbar )
      return FALSE;

    sMenuCount = MENUCOUNTOFHMBAR( hmbar );
    ASSERT( sMenuCount > 0,
            "MpmMenuLoad: null menu bar?" );

    SetMenuBar( hmbar );
    DisposHandle( (Handle)hmbar );

    hmbar = (MbarHandle)MenuList;

    hmenu = (**hmbar).mlist[0].hmenu;

    ASSERT( hmenu,
            "MpmMenuLoad: no apple menu?" );

    _sAppleCount = CountMItems( hmenu );

    AddResMenu( hmenu, 'DRVR' );

    DrawMenuBar();

    return TRUE;
}


Figure 12:  Function to Handle Mouse Events

LOCAL BOOL MpmMsgMouse( pqmsg, pEvent, msg )
    PQMSG       pqmsg;
    EventRecord *pEvent;
    USHORT      msg;
{
    SHORT       sArea;
    POINTL      ptl;
    WindowPeek  pwin;
    USHORT      fid, usHitHi;
    HWND        hwnd;

    sArea = FindWindow( pEvent->where, &pwin );

    fid = usHitHi = 0;
    ptl = pqmsg->ptl;
    pqmsg->hwnd = hwnd = ( pwin ? HWNDOFPWIN(pwin) : _hwndDesktop );

    switch( sArea )
    {
      case inContent:
        WinMapWindowPoints( _hwndDesktop, hwnd, &ptl, 1 );
        MpmMsgFindChild( pqmsg, &ptl );
        hwnd = pqmsg->hwnd;
        break;

      case inDesk:
        break;

      case inDrag:
        fid = FID_TITLEBAR;
        break;

      case inGoAway:
        fid = FID_SYSMENU;
        break;

      case inGrow:
        fid = FID_SIZEBORDER;
        break;

      case inMenuBar:
        hwnd = _hwndMenu;
        if( ! hwnd )
          return FALSE;
        break;

      case inSysWindow:
        SystemClick( pEvent, pwin );
        return FALSE;

      case inZoomIn:
      case inZoomOut:
        usHitHi = sArea;
        fid = FID_MINMAX;
        break;

      default:
        return FALSE;
    }

    if( fid )
    {
      hwnd = WinWindowFromID( hwnd, fid );
      ASSERT( hwnd,
              "MpmMsgMouse: missing frame control" );
    }

    if( MYWNDOF(hwnd).flStyle & WS_DISABLED )
      return FALSE;

    pqmsg->hwnd = hwnd;
    pqmsg->msg  = msg;
    pqmsg->mp1  = MPFROM2SHORT( ptl.x, ptl.y );
    pqmsg->mp2  = MPFROM2SHORT( HT_NORMAL, usHitHi );

    return TRUE;
}


Figure 13:  Function to Get a PS Handle for hwnd

HPS APIENTRY WinGetPS( hwnd )
    HWND        hwnd;
{
    GrafPtr     pgraf;
    RgnHandle   hrgn;
    Rect        rect;

    ASSERT( ! ( _ps1.flags & PSF_INUSE ),
            "WinGetPS: PS already in use" );

    if( ! hwnd  ||  hwnd == HWND_DESKTOP )
      hwnd = _hwndDesktop;

    if( ! MpmValidateWindow(hwnd) )
      return NULL;

    /* Clear the cache PS and mark it as in use */

    memzero( &_ps1 );
    _ps1.hwnd = hwnd;
    _ps1.flags |= PSF_INUSE;

    /* Copy the Mac window's GrafPort */

    if( hwnd == _hwndDesktop )
      GetWMgrPort( &pgraf );
    else
      pgraf = &PWINOFHWND(hwnd)->port;

    _ps1.port = *pgraf;

    _ps1.port.visRgn = NewRgn();
    CopyRgn( pgraf->visRgn, _ps1.port.visRgn );

    _ps1.port.clipRgn = NewRgn();
    CopyRgn( pgraf->clipRgn, _ps1.port.clipRgn );

    /* Clip the visRgn down to this window's rectangle in case it's
       a child window */

    if( ! ( MYWNDOF(hwnd).flStyle & WS_PARENTCLIP ) )
    {
      hrgn = NewRgn();
      MpmQueryMacRect( hwnd, &rect );
      RectRgn( hrgn, &rect );
      SectRgn( _ps1.port.visRgn, hrgn, _ps1.port.visRgn );
      DisposeRgn( hrgn );
    }

    /* Handle WS_CLIPCHILDREN and WS_CLIPSIBLINGS here? */

    return _hps1;
}


Figure 14:  Module Containing Mac Equivalents of PM GPI Functions

/*-----------------------------------------------------------------*/
/* MpmGPI.c                                                        */
/* GPI functions                                                   */
/*-----------------------------------------------------------------*/

#include "MacPM.h"

/*-----------------------------------------------------------------*/
/* Draw a character string at the current position                 */
/*-----------------------------------------------------------------*/

LONG APIENTRY GpiCharString( hps, lLen, pch )
    HPS         hps;
    LONG        lLen;
    PCH         pch;
{
    if( ! MpmValidatePS(hps) )
      return GPI_ERROR;

    thePort = PGRAFOFHPS(hps);

    DrawText( pch, 0, (SHORT)lLen );

    return GPI_OK;
}

/*-----------------------------------------------------------------*/
/* Draw a character string at the specified position               */
/*-----------------------------------------------------------------*/

LONG APIENTRY GpiCharStringAt( hps, pptl, lLen, pch )
    HPS         hps;
    PPOINTL     pptl;
    LONG        lLen;
    PCH         pch;
{
    if( ! GpiMove( hps, pptl ) )
      return GPI_ERROR;

    return GpiCharString( hps, lLen, pch );
}

/*-----------------------------------------------------------------*/
/* Erase a PS on the screen.                                       */
/*-----------------------------------------------------------------*/

BOOL APIENTRY GpiErase( hps )
    HPS         hps;
{
    if( ! MpmValidatePS(hps) )
      return FALSE;

    thePort = PGRAFOFHPS(hps);

    EraseRgn( thePort->visRgn );

    return TRUE;
}

/*-----------------------------------------------------------------*/
/* Set the drawing position to *pptl.                              */
/*-----------------------------------------------------------------*/

BOOL APIENTRY GpiMove( hps, pptl )
    HPS         hps;
    PPOINTL     pptl;
{
    PMYWND      pwnd;

    if( ! MpmValidatePS(hps) )
      return FALSE;

    thePort = PGRAFOFHPS(hps);
    pwnd = PMYWNDOF(HWNDOFHPS(hps));

    MoveTo( (SHORT)pptl->x, (SHORT)( pwnd->cy - pptl->y ) );

    return TRUE;
}

/*-----------------------------------------------------------------*/
/* Pick up the current font metrics and return it in *pfm.         */
/* This is kind of hokey and only initializes some of the fields   */
/* properly!                                                       */
/*-----------------------------------------------------------------*/

BOOL APIENTRY GpiQueryFontMetrics( hps, lLen, pfm )
    HPS         hps;
    LONG        lLen;
    PFONTMETRICS pfm;
{
    GrafPtr     pgraf;
    FMInput     fmi;
    FMOutPtr    pfmo;

    if( ! MpmValidatePS(hps) )
      return FALSE;

    pgraf = PGRAFOFHPS( hps );

    fmi.family    = pgraf->txFont;
    fmi.size      = pgraf->txSize;
    fmi.face      = pgraf->txFace;
    fmi.needBits  = FALSE;
    fmi.device    = 0;
    fmi.numer.h   = 1;
    fmi.numer.v   = 1;
    fmi.denom.h   = 1;
    fmi.denom.v   = 1;

    pfmo = FMSwapFont( &fmi );
    ASSERT( pfmo,
            "GpiQueryFontMetrics: FMSwapFont failed?" );

    memzero( pfm );

    pfm->lAveCharWidth      =  /* wrong, but make it same as max */
    pfm->lMaxCharInc        = pfmo->widMax;
    pfm->lMaxBaselineExt    = pfmo->ascent + pfmo->descent
                              + pfmo->leading;
    pfm->lMaxDescender      = pfmo->descent;

    /* more later... */

    return TRUE;
}

/*-----------------------------------------------------------------*/
/* Make sure hps is a valid PS handle.                             */
/*-----------------------------------------------------------------*/

LOCAL BOOL MpmValidatePS( hps )
    HPS         hps;
{
    if( hps != _hps1 )
    {
      ERROR( "Invalid PS handle!" );
      return FALSE;
    }

    return TRUE;
}

/*-----------------------------------------------------------------*/


───────────────────────────────────────────────────────────────────────────
Development Tools for the Macintosh
───────────────────────────────────────────────────────────────────────────

For OS/2 and Windows, there's not much question of which compilers and
libraries to use: Microsoft C and Macro Assembler (MASM), along with the
Windows and OS/2 Software Development Kits (SDKs). This situation may change
as other vendors provide OS/2 and Windows tools. On the Mac, the choice is
less clear. There are a variety of good development environments for C and
assembler, but the best are Lightspeed C and the Macintosh Programmer's
Workshop (MPW). MPW provides the utmost in programmability and
customizability; its main window is a command shell, where you can type in
simple commands or edit and run complex scripts. You can open additional
windows to edit C or assembler code, or to save and edit script files.
Besides using MPW to code standalone Mac applications, it's also very easy
to write "tools," specialized applications that run only under the MPW shell
and extend its functionality.

As great as MPW is, my personal choice is Lightspeed C. Though it lacks the
programmable shell of MPW, Lightspeed C lives up to its name in speeding up
the development process. Turnaround from a source change to running the
code is blazingly fast, and the editor and compiler are closely integrated
so the editor knows things like which source file defines a particular
function. You can write inline assembly right in the middle of C code,
making utility routines easy to code.

Best of all is the source code debugger in the new 3.0 version. The hot item
is its data window. Like the CodeView(R) Watch window, it's basically a
list of expressions and their values, and it's updated each time the program
stops (for example, at a breakpoint). Double-click on a structure or array
in this window, and it opens up a structure or array window. This is almost
identical to the new "??" command in the CodeView debugger, but you can open
up as many of these windows as you like and keep them open while you run
your program, watching your data as you go. In any of the data windows, you
can type in new values for any of the watched expressions. Data windows and
the source code window share screen space with the application fairly
cleanly; on a Mac with two monitors, the data windows can appear on the
second monitor. The expression evaluator, both for new values and for the
expressions themselves, knows every bit of compiler information, and you
can use preprocessor macros and symbols freely in your expressions. Using
Lightspeed C with the source debugger requires at least two megabytes of
RAM and MultiFinder.

There are a few Mac utilities I wouldn't leave home without, whether using
MPW or Lightspeed C. TMON is a machine-level debugger that is the perfect
complement to Lightspeed's source debugger. It's easy to use, with a nice
windowed interface, but lacks nothing in debugging power──a must for any
serious Mac programmer. QuicKeys is a keyboard/mouse macro program.
Although Apple now includes a keyboard macro program with the Mac System
software, QuicKeys is still my favorite. None of my Mac software could do
much with the extra keys on Apple's Extended Keyboard, but a few minutes
with QuicKeys and I had them all working the way I wanted. I even have all
the same Edit menu shortcuts (Ctrl-Ins and friends) as in Windows and PM,
making life less confusing when I switch back and forth. QuicKeys is worth
buying just to get a lesson in how to design a superb utility package. One
last tidbit is The Programmer's Online Companion, a handy on-line reference
to all the Mac Toolbox and operating system calls. If you ever use Inside
Macintosh, you need this. Be sure to get the new version with the Mac II
calls.

Of course, one remaining problem is transferring files between the PC and
the Mac. The best bet for this would be either Lap-Link Mac or MacLink(R).
I've been using Lap-Link Mac, and it does a good job. It comes with a serial
cable that will connect any Mac to any PC. The cable has both 9-pin and 25-
pin connectors on the PC end, and 9-pin and mini-DIN connectors on the Mac
end; it makes for a funny looking cable, but hooking the computers up is
painless this way. Transfers are quick and reliable, and the program can
convert between the Mac and PC text file formats. The only thing I don't
like about Lap-Link Mac is the user interface. Everything is controlled
from the PC side──the Mac program is a slave──and, while it's reasonably
powerful and convenient, the user interface takes some getting used to.
MacLink is probably worth looking into, though I haven't tried it out
myself.

Product Information

XVT-the Extensible Virtual Toolkit
Advanced Programming Institute Ltd.
P.O. Box 17665
Boulder, CO 80308
(303) 443-4223

Currently available for Macintosh and Windows. OS/2, X Windows, and other
versions planned. The "Technical Overview of XVT," by Marc Rochkind is
available free from API and is itself an education in portability issues.

Lightspeed C
Symantec, Inc., Think Technologies Division
135 South Road
Bedford, MA 01730
(617) 275-4800

Macintosh Programmer's Workshop
Apple Programmer's and Developer's Association (APDA)
290 S.W. 43rd St.
Renton, WA 98055
(206) 251-6548

TMON
ICOM Simulations, Inc.
648 S. Wheeling Road
Suite 10
Wheeling, IL 60090
(312) 520-4440

QuicKeys
CE Software, Inc.
P.O. Box  65580
West Des Moines, IA 50265
(515) 224-1995

The Programmer's Online Companion
Addison-Wesley Publishing Company, Inc.
Jacob Way
Reading, MA 01867
(617) 944-3700


Using the OS/2 Environment to Develop DOS and OS/2 Applications

───────────────────────────────────────────────────────────────────────────
Also see the following related articles:
  An OS/2 Command Reference
  Configuring OS/2
  Exploring the OS/2 SDK
  Exploring the OS/2 Programmer's Toolkit
───────────────────────────────────────────────────────────────────────────

Richard Hale Shaw☼

You've been thinking of switching to an OS/2 system for applications
development but don't know where to start. You've bought an OS/2 system and
aren't sure how to install and configure it for programming. Its
multitasking, multithreaded capabilities intrigue you but seem formidable.

Well, they are a bit formidable. OS/2 offers significant productivity
gains whether you're developing software for either DOS or OS/2, but it
demands that you rethink many of the programming assumptions you made when
writing DOS applications.

This article will help you get started. First, we'll investigate OS/2 as a
program development environment: its hardware and software requirements,
installing and configuring OS/2, and utilizing multiple sessions for
program development. In subsequent articles we'll study the OS/2
application program interface (API), in particular the Video Input/Output
(VIO), Keyboard (KBD), and Mouse (MOU) subsystems by writing useful programs
that rely on them. We'll learn to write bound applications for both real and
protected mode, and wrap up by putting together the key ideas into a real
and useful OS/2 application.

This series of articles assumes a working knowledge of C and DOS, but not
OS/2. Our goal is to develop several programs and tools that will help to
instill a working knowledge of the OS/2 API and some of the issues
critical to writing OS/2 applications. You will need a C compiler, the
Microsoft(R) OS/2 Programmer's Toolkit, and an OS/2 system. By the time we
finish you'll be ready to consider the next level of programming──the
Presentation Manager.


What are the Benefits?

It's well known that OS/2 operates in the protected mode of the Intel(R)
80286 processor──the 80386 also supports 286 protected mode──and, of course,
you can use OS/2 to develop protected-mode programs. But there are also
advantages to using OS/2 as a host development environment for
applications that run in MS-DOS(R) 8086/8088 real mode.

Probably the most immediate benefit of OS/2 is the ability to use multiple
program development sessions. Each program can run as an independent
process under OS/2, which gives the program its own screen group or
session, and also endows each session with a logical keyboard and console
interface. OS/2 ties these to the real keyboard and video screen when the
process is running in the foreground. Processes running in the background
continue uninterrupted unless awaiting keyboard input (which they can get
when switched back into the foreground).

As a result, most of the mundane tasks done while writing and developing a
program, including tasks that tie up specific machine resources, may be
carried out more efficiently in multiple program sessions. Compiling a
program or grepping for program references, for instance, can run in a
session that you switched into the background. This leaves you free to
pursue other tasks, such as editing another part of the program or
compiling a different program altogether.

With the OS/2 multisession environment, you can dedicate a session to each
facet of program development. It's possible to create separate sessions for
the program editor, the compiler or assembler, for utilities, the debugger,
and the program test environment. Or, you might create a session whose sole
purpose is to act as a convenient, readily-available command prompt, for
running utilities and copying disks (you can even relegate disk formatting
to the background). You need not exit or terminate your program editor to
start the compilation of a source code module. Nor must you terminate a
debugging session in order to look up a source code reference with your
editor.

Since each OS/2 session contains its own independent environment, prompt,
path, and screen configuration, you can customize a session and tailor it to
the tasks which are to run in it. For example, you can set a compiler
session's path and environment variables for library and source file
searches, and optimize them to most quickly navigate the appropriate
directories. You can create a program editor session which only allows the
editor to know (via environment variables) about a specific set of files and
directories, and which would differ from a session used for testing a new
program.

Creating several sessions to perform different tasks is easy under OS/2; in
fact, it's addictive. Using the same tricks that were available under MS-DOS
(a few simple tools and ANSI environment strings), you can configure the
command prompt, screen colors, and screen modes for each session
independently. Thus, you can readily identify each, even when rapidly
switching from one session to another.

For example, a compilation session might be set to a 43-line screen mode
with a dark background and highlighted prompt. This would allow a quick
perusal of compiler warning and error messages when switching between
sessions, and it can make it easier to discern the progress of a compiler
session when processing a large number of files. Or, the command-prompt
session described above might be a stark black-on-white.

Under OS/2 you can dedicate a separate session to each program that needs a
unique environment (the concept of the "environment" takes on a meaning
more akin to that in UNIX(R) operating environments). In fact, it's not
unusual to run two different compilers at once.

Running multiple editing sessions can further enhance your productivity.
Most contemporary program editors boast the ability to display and edit
multiple files in multiple windows. By creating more than one editing
session, you can literally double or triple the number of files that can be
displayed or edited at a time, and you can do so without cluttering the
screen.

To take this a step further, I've found it useful to actually edit, compile,
and debug two entirely different programs simultaneously. Although it
might seem counterproductive, this is helpful when one of the programs is
exceptionally large (over 100Kb) and takes an inordinate amount of time to
compile and link, in which case there is plenty of time to switch one
compilation session into the background and edit or test a completely
different program in the foreground. While foreground sessions get priority
attention from the CPU under OS/2, you'll be amazed how quickly a well-
behaved OS/2 application will run in the background. (The definition of a
"well-behaved" OS/2 application will become evident over the course of the
next few articles in this series.──Ed.)


The DOS Session

OS/2 can be configured to create a special real-mode session that supports
a hybrid version of MS-DOS 3.x, known alternately as the Compatibility Box
or the DOS session. This session is only active as a foreground process
(that is, when displayed on the screen). It's frozen in the background
whenever you switch to another session and OS/2 returns the processor to
protected mode.

To programs running in the DOS session, the operating environment appears
to be DOS, although OS/2 keeps some control over the machine. Since the
processor runs the DOS session in real mode, the "fast and loose" DOS
operating environment is still available, making it possible for a DOS
program running in the DOS session to abuse it and cause problems──with even
greater ramifications than under normal DOS. Further, some tools behave
acceptably under DOS but may not under OS/2. A number of program editors,
for instance, poll the keyboard continuously in order to speedily process
keyboard input. This is fine in a single-tasking environment like DOS.
Under OS/2 these applications can bring the system to its knees; by
directly polling the hardware from the DOS session, they force the CPU to
assign them too many time slices. Thus, while they remain speedy, the lack
of CPU attention given elsewhere can slow background processes (such as a
program compile) to a crawl.

A well-written OS/2 editor will create a thread to get keyboard input.
Since OS/2 knows which screen group "owns" the thread, it will "block" the
thread until keyboard input is available for it. Thus other processes will
proceed normally (whether in the background or the foreground), getting
their appropriate share of CPU time.

While the DOS session facilitates the move to OS/2, product development is
far easier when you have tools that run in, and take advantage of, OS/2's
multitasking environment. For instance, if you have a UNIX-like grep
command that only runs under DOS, you will only be able to run it in the DOS
session, and it will be frozen whenever the DOS session is pushed into the
background.

If such a command were written for OS/2, it would not only continue to run
in a background OS/2 session, but you could have it take advantage of
threads──multitasking within the process itself──by assigning, for example,
one thread to open and search a file, and another to write the results to
the output. It will not only cooperate with other OS/2 processes competing
for CPU attention and machine resources, but it will also be significantly
more efficient than its single thread DOS counterpart.

While the DOS session can provide almost a full 640Kb DOS environment, the
limitations of trying to debug and run large programs under it are the
same as those found under MS-DOS 3.x. It may be impossible to debug a
program with Microsoft CodeView(R) debugger in the DOS session if the
program is too large, or if too many modules are compiled for debugging.
This is never an issue, however, when debugging large programs with the
protected-mode version of CodeView, due to OS/2's virtual memory.


Startup Requirements

Now that you've decided to develop programs under OS/2, what do you really
need in order to get started? And what fits in that special "it would sure
be great──if I could afford it" category?

Our goal is to become as proficient as possible at writing OS/2
applications. However, there really are two ways to get there, one
sufficient and one preferred. It's not unlike the difference between flying
coach and first class. The sufficient method includes everything necessary
to successfully write OS/2 programs. Preferred (or first class) is
definitely more expensive but it can make the difference between getting up
and running, and getting up and running very quickly. Either way, to run and
utilize OS/2 you need a machine with at least an Intel 80286 or 80386 CPU;
2.5Mb of RAM; 20Mb of disk space; a high-density disk drive (either a 1.44
megabyte 3.5" or a 1.2 megabyte 5.25"); and, of course, a video monitor and
keyboard.

The following hardware is preferred (and will immediately prove valuable):
3+ megabytes of RAM; 40+ megabytes of disk space; IBM(R) EGA, VGA, or 100
percent compatible display adapter (the EGA adapter should have 128Kb of
RAM); and a mouse.

When it comes to software, the choices are far more varied. To adequately
develop programs under OS/2, you'll need the following tools: a compiler
(C, Pascal, etc.) that provides OS/2 support; an assembler; a protected-
mode linker; a protected-mode program editor; a protected-mode debugger;
other utilities such as make, grep, etc.; OS/2 Version 1.0 (Version 1.1,
which includes the Presentation Manager, will do, but it's not necessary to
get started).

As you're probably aware, most of the Microsoft languages already offer
versions that will produce protected-mode executables. This includes
Microsoft C Optimizing Compiler Version 5.1, a "bound" application capable
of producing programs that run under both operating environments (that
is, it runs in either real or protected mode). If you are an experienced C
programmer, you'll have a number of assembly language subroutines.
Microsoft Macro Assembler Version 5.1 (MASM) will fill the bill for an
assembler (like C 5.1, it is a bound application). The new Microsoft
incremental linker, LINK4, which can incrementally (and therefore more
quickly) link object files to produce an executable for either OS/2 or DOS,
is included with both Microsoft C 5.1 and MASM 5.1.

The Microsoft Editor, a powerful protected-mode program editor, is also
included with both MASM and Microsoft C. I should mention, however, that
other excellent protected-mode editors, such as Lugaru's Epsilon(TM) and
the ME Editor from Magma Systems are now available or beginning to appear.


Toolkit or SDK?

Microsoft offers two ways of getting the necessary tools for OS/2 program
development. The Microsoft OS/2 Software Development Kit, known as SDK (see
"Exploring the OS/2 SDK"), offers a comprehensive collection of virtually
every tool needed, from OS/2 (Versions 1.0 and 1.1──the latter including
Presentation Manager and LAN Manager) to the C compiler and assembler, to
utilities, etc. This is the preferred package──but the cost of the SDK
($3,000) can be prohibitive to the individual programmer or developer.

The alternative is to purchase each individual tool you need. MSJ readers
who already own the most recent versions of Microsoft C and/or MASM have a
head start, since many of the other required tools come with the products.
These, plus OS/2 itself, are adequate for developing OS/2 programs. You will
also need to purchase the Microsoft OS/2 Programmer's Toolkit (PTK). The
PTK (see "Exploring the OS/2 Programmer's Toolkit"), which is also included
in the SDK, contains many of the essential tools and example programs
found in the SDK──most importantly the complete Programmer's Reference guide
to all of the OS/2 API calls.

If you don't expect to develop Presentation Manager or LAN Manager
applications right away (a reasonable expectation if you are just getting
started), you might consider purchasing the PTK as a less expensive (about
$300) alternative to the SDK, in addition to the other required tools. Note
that the PTK requires a high-level language compiler (such as C 5.1), an
editor, and OS/2 itself.


Installing OS/2

Once you're ready to install OS/2, you'll encounter a number of options,
primarily hinging on whether you already have MS-DOS on the machine on which
you're installing OS/2. These options let you:

  ■  Default to booting OS/2 (boot DOS from a floppy).

  ■  Default to booting DOS (boot OS/2 from a floppy).

  ■  Have the system prompt you for which operating system to run at boot
     time. (Dual-boot mode, available with early SDK versions of OS/2 and
     some other manufacturers' versions──for example, Zenith's; the
     operating system can be booted from the hard disk.)

If you expect to rarely boot DOS again, the first option might be a viable
one. The last option is usually the most convenient. Keep in mind that the
DOS session will still be available to you when OS/2 is running; the option
only refers to booting either MS-DOS 3.x or later or OS/2 from the hard
disk. Not all versions of OS/2 provide a dual-boot mode option of
installation, and dual-boot will not be supported in retail versions of
OS/2 sold by IBM.

For the sake of simplicity, I will assume you want to install OS/2 on your
MS-DOS development machine, and still be able to run MS-DOS free and clear
of OS/2-in other words, the dual-boot installation option. In actuality, it
doesn't matter which installation method is chosen-whichever is handiest
should be the guiding line. While there may be other steps involved,
depending on the number and size of the hard disks in your system,
installing OS/2 basically involves booting the OS/2 Installation Disk and
answering a few questions. (It's infinitely less complex than installing C
5.1, with its plethora of options.) The installation program will prompt
you for other disks as needed. And that's it.

If you install OS/2 to include the dual-boot option, then the installation
program reconfigures the boot sector of your drive. Once you reboot after
successfully installing OS/2, the following message will appear on your
screen:

  Boot: Enter=OS/2, ESC=DOS

That's all that it takes to choose between OS/2 and DOS. If you chose to
install OS/2 from either the hard disk or a floppy, then obviously OS/2 will
come up directly.


Exploring the OS/2 Files

As you start exploring your system after installing OS/2, you'll find a
number of changes. First of all, you'll find some additions to the
subdirectory structure in the root of your boot drive, as shown in the
diagram in Figure 1. Each directory has a specific use and meaning as
indicated. It is possible that this directory structure may change somewhat
in the future, but as it stands, it serves our purposes for discussion in
this article.

The installation program places a number of files in the root directory of
your boot drive, or in one of the subdirectories mentioned above. While
quite a few of these are OS/2 utilities (see "An OS/2 Command Reference"),
a number are worth noting. First you'll find two hidden files, as in DOS
environments: OS2BIO.COM and OS2DOS.COM. (Note the DOS hidden files,
IBMBIO.COM and IBMDOS.COM, are untouched.) These will always be in the root
directory if dual-boot mode was selected. Retail releases of OS/2, however,
may have different names for these hidden system files.

The next few changes are especially meaningful in light of what I said
earlier about peaceful coexistence between DOS and OS/2.

CONFIG.OS2 is similar to the CONFIG.SYS found under MS-DOS (see "Configuring
OS/2"). OS/2 processes it at system startup. If you are not using the dual-
boot mode, this file would be called CONFIG.SYS under OS/2.

STARTUP.CMD is OS/2's counterpart to the MS-DOS AUTOEXEC.BAT. It is
processed after CONFIG.SYS and lets you start other programs and processes
and further modify the OS/2 environment.

OS2INIT.CMD is the batch file that is run whenever you initiate a new
protected-mode session, and its commands execute at the start of each
session.

AUTOEXEC.OS2, like AUTOEXEC.BAT its MS-DOS counterpart, runs upon initiation
of the real-mode session and is used to configure the environment of the
DOS session. This file retains its AUTOEXEC.BAT filename under OS/2 when
the dual-boot mode is not used.

Two of these files are OS/2 batch files. OS/2 requires that batch files have
a CMD extension to distinguish them from their DOS counterparts. The
following are among the remaining files added by the installation program,
and require special note:

CMD.EXE is the OS/2 command interpreter (akin to COMMAND.COM under
MS-DOS).

HARDERR.EXE is the OS/2 critical error handler.

PMSHELL.EXE is the Presentation Manager Task Manager (for OS/2 Version
1.1).

SHELL.EXE is the OS/2 Program Selector (for OS/2 Version 1.0).

SWAPPER.DAT is the OS/2 virtual memory swap file.

SWAPPER.EXE is the OS/2 virtual memory manager.


Configuring OS/2

There are several ways to configure OS/2 via CONFIG.OS2 (see
"Configuring OS/2"). You'll want to have loaded the appropriate device
drivers (via DEVICE=), and you'll want paths to both the real-mode command
processor (SHELL= as under MS-DOS) and to the protected-mode command
processor. The latter usually appears as shown in Figure 2A. SHELL.EXE is
the OS/2 Program Selector (provided with OS/2 Version 1.0), and CMD.EXE is
the OS/2 command processor. Since one of SHELL.EXE's arguments is CMD.EXE,
the former will use the latter for loading and running programs.

In order to cover all bases, I should mention that SDKs shipped at the time
of this writing (October, 1988) include installation programs for a beta
version of Presentation Manager, which is included in OS/2 Version 1.1. The
prerelease of the PM Shell is relatively functional, and if you install its
Program Starter and Task Manager (both are PM's alternative to the Program
Selector), your PROTSHELL entry will probably look like that shown in
Figure 2B.

In addition, the following are "musts" if you're going to use OS/2 for
program development:

  ■  Use the OS/2 disk cache to gain a performance advantage. This is done
     by placing DISKCACHE=x into CONFIG.OS2, where x is the size in
     kilobytes of the disk cache. I recommend at least 256 (on a
     development machine with 2.5 meg of RAM).

  ■  To run the protected-mode version of CodeView, you will need IOPL=YES
     in CONFIG.OS2, which gives additional IO data privileges to programs
     like CodeView.

  ■  If you plan to use or create dynamic-link libraries, you
     should have LIBPATH=path, where path points to the location of DLLs
     (the installation program will take care of this for you in most
     versions).

  ■  If you want to conserve memory when the DOS session is unnecessary,
     set PROTECTONLY=YES in CONFIG.OS2. This prevents the DOS session from
     being allocated or run, even though you might have a SHELL= statement
     designating the real-mode command processor. As an alternative, you
     can keep the DOS session available but limit the memory allocated to it
     via RMSIZE=x, where x is the total memory up to 640Kb that can be
     allocated to it.

  ■  If you're running processes with a lot of threads, you can increase the
     number of threads available from OS/2 with THREADS=x, where x is the
     total number of threads available. Keep in mind that there is a slight
     memory overhead relative to the total number of threads allocated.


RUN and START

OS/2 lets you prestart programs via the RUN or START commands. RUN is a
directive placed in CONFIG.OS2 that lets OS/2 initiate a program or
process. For example, the line shown in Figure 2C will run the OS/2 print
spooler when OS/2 is done processing CONFIG.OS2. Since CONFIG.OS2 lets you
have more than one RUN statement in it, you can initiate a number of
processes directly from CONFIG.OS2.

One limitation to RUN, however, is that it is not capable of running CMD
files-so that while you can run a program directly, you can't configure its
environment. You get around this by running the CMD file as an argument
to the OS/2 command processor, CMD.EXE. Thus, you could use a line like
that in Figure 2D to start a process that needs a CMD file to initialize
its environment.

Alternatively, you could use the START command. START lets you initiate
another session from the OS/2 command line. If you wanted to create a CMD
file that initiated several processes (without having to initiate each one
from the Program Selector), the CMD file might contain the lines shown in
Figure 2E. If you are going to regularly start a series of programs, the
preferred method is to place the START commands in STARTUP.CMD.


Switching Programs

Getting around in OS/2 is easy. You can press Ctrl-Esc at any time to bring
up the Program Selector (the Task Manager under PM), which displays a
configurable list of programs to initiate, and a list of sessions already
active. You then select the session you wish to switch to, or the process
you wish to start. Personally, I find it's easiest to switch between active
sessions by pressing Alt-Esc. This lets you rapidly "hot-key" from session
to session until you reach the one you want to work with. There is very
little delay in this method even if you are hot-keying across the DOS
session, regardless of the hardware you're using. This is fairly rapid on a
10 MHz 286 machine with 2.5Mb of memory and over a half-dozen sessions
running, and it's especially fast when you consider the overhead that OS/2
imposes on the processor when switching from protected mode to real mode and
back. Yet this is exactly what happens when you hot-key from a protected-
mode session to the DOS session to another protected-mode session with Alt-
Esc.


Using OS/2

Once you have OS/2 up and running you can start exploring some of its
advantages over DOS. As shown in "An OS/2 Command Reference," a number of
carry-over commands from DOS have been enhanced to accept multiple
arguments. In addition, some of the substitution facilities that were
available only in BAT files under DOS are available on the command line
under OS/2. If the environment has this entry:

  LIB=c:\MSC\OS2\LIB

followed by the command

  CD %LIB%

then it would change to the \MSC\OS2\LIB directory on the C: drive. Under
DOS, this would only have worked in a BAT file.

Another convenience is the DETACH command. DETACH lets you initiate a
background process from the command line. For instance,

  DIR | SORT > dsort.txt

initiates a process that runs DIR, pipes the result to SORT, and redirects
the output to a file. So,

  DETACH DIR | SORT > dsort.txt

places the entire process in a background session. DETACH always assumes
that the process it runs does not require any keyboard input. After
initiating the process, DETACH prints the process' ID for the task and
returns to the process that started it──in this case, the OS/2 command line.
Thus, it's easy to relegate complex tasks to the background and let the user
proceed as usual.

Another advantage becomes apparent whenever you need to perform an OS/2
command line operation while running a program. You can always Ctrl-Esc
to the Program Selector to bring up another OS/2 command line. This is
especially convenient when you need to format floppy disks or delete some
files without leaving a program. The formatting, once started, will proceed
smoothly in the background after you Alt-Esc back to the session you left
earlier. If you find this convenient, you may want to make a habit of
initiating an OS/2 command session when you begin work (or RUN one from
CONFIG.OS2 before the Program Selector appears).

Another OS/2 plus is that the command-line facilities have been enhanced in
order to allow more complex expressions, as mentioned in "An OS/2 Command
Reference." For instance, the command line in Figure 3 would conditionally
build a sorted list of protected-mode utilities-conditionally, because the
DIR command would not be issued if the drive and directory change were not
successful. Users can run the entire process as a background task by
placing a DETACH command in front of the entire command. Additionally, the
foreground session would return on the original drive in the original
directory since the drive and directory change would take place in the
background session; remember, such changes are local to a session and do
not affect other sessions.

Finally, you'll find that OS/2 CMD files have also been enhanced over MS-DOS
BAT files, especially with the new SETLOCAL and ENDLOCAL commands. SETLOCAL
saves the current state of the logged-in disk drive and current working
directory; ENDLOCAL restores these to the state saved by SETLOCAL. Thus you
could write a CMD file like this:

  @echo off
  setlocal
  c: && cd\ha && ha
  endlocal

This would log in drive C, change to the \HA directory, and run the program
HA.EXE. When the program has completed execution, the original drive and
directory will be restored to the session. These commands are similar to the
PUSHDIR/POPDIR commands available in some UNIX shell environments.


No Caffeine

The best reason for not using OS/2 as a development environment is that
it'll eliminate coffee breaks during long program compilations; you can
always do something else! While its multitasking capabilities are extensive,
OS/2 is an extremely productive, in fact fun, work environment. Coupled with
the applications that are taking advantage of it, this fact will draw large
numbers of users to OS/2, and it's a good reason for you to begin using OS/2
to develop programs now, if you haven't already started.

In the next article of this series, we'll discuss setting up the C compiler
and explore the issues surrounding the writing of your first protected-mode
application: a multithreaded version of HELLO.C.

(As this article was going to press, IBM and Microsoft announced the
official release of OS/2 Version 1.1. The new installation of OS/2 Version
1.1 and Presentation Manager changes some of the setup features of earlier
releases. For example, dual-boot mode is no longer an available option, the
system files IBMBIO.COM and IBMDOS.COM have been renamed to OS2LDR and
OS2KRNL respectivley, and the initial directory structure may be somewhat
different than that described here.──Ed.)


Figure 1:  Standard OS/2 Directory Structure

                          ┌───────────────────\(Root)────────┬──────────┐
                          │                                  │          │
                                                           SPOOL     (other)
   ┌──────────┬──────────OS/2─────────┬──────────┐
   │          │           │           │          │
  BIN        DEV         LIB        PBIN       RBIN

\OS2         for OS\2 and Presentation Manager files
\OS2\BIN     for bound utilities
\OS2\DEV     for storing device drivers
\OS2\LIB     for dynamic-link libraries
\OS2\PBIN    for protected-mode only utilities
\OS2\RBIN    for real-mode only utilities
\SPOOL       used by the OS/2 print spooler


Figure 2:  Examples of OS/2 CONFIG.SYS Entries

 2A
 PROTSHELL=C:\SHELL.EXE C:\CMD.EXE /K C:\OS2INIT.CMD

 2B
 PROTSHELL=C:\OS2\PMSHELL.EXE C:\OS2\PBIN\CMD.EXE /K C:\OS2INIT.CMD

 2C
 RUN=c:\os2\pbin\spool.exe

 2D
 RUN=c:\cmd.exe /c c:\os2\pbin\mestart.cmd

 2E
 start "HyperACCESS" /c ha
 start "Paradox" /c pdoxos2 -multi
 start "OS/2 Command Line"


Figure 3:  Under OS/2 multiple commands can be executed from the command
           line.

c: && cd \os2\pbin && dir *.exe *.cmd | sort > tools.lst


───────────────────────────────────────────────────────────────────────────
An OS/2 Command Reference
───────────────────────────────────────────────────────────────────────────

Most of the traditional MS-DOS commands have been retained in OS/2's
protected-mode environment. However, several commands have been enhanced and
a number of new ones have been added.

The following commands have been enhanced over their DOS and real-mode
counterparts to allow multiple arguments:

  ■ DEL         ■ MD/MKDIR
  ■ DIR         ■ RD/RMDIR
  ■ TYPE        ■ VOL

For instance you can delete all of the .C, .H, and .ASM files in a directory
with:

  DEL *.c *.h *.asm

(Obviously, I'm not recommending that you try out this particular sequence.)
In addition, the MODE command now allows for the setting of time-out values
and hardware handshaking details.

The following are new commands, available in OS/2's protected mode:

  &&

The && is used to conditionally chain commands:

  dir x && del x

This will delete x only if the first command was successful.

  &

The & can unconditionally chain commands:

  dir x & del x

Here, OS/2 will attempt both commands. The outcome of one does not depend on
the other.

|| can conditionally execute commands, so that in:

  copy x y || copy a y

The second command will only execute if the first command is unsuccessful.

  ()

Parentheses can group commands for chaining and execution:

  del x || (attrib -r x & del x)

In this case, if the deletion is unsuccessful (because x's read/write
attribute is on), OS/2 will attempt to clear the read/write attribute of x
and, if successful, delete it.

  ^

The ^ will normalize special characters. Thus:

  ^> file.txt

will be seen as "> file.txt". OS/2 will not see the > as a special character
and will not attempt redirection.

  ANSI [ON | OFF]

This command toggles support for ANSI escape sequences. It is provided in
lieu of ANSI.SYS, the real-mode device driver.

  CMD [options] [arguments]

Used to run the protected-mode command processor. Options include /c to
ensure return to the primary command processor, and /k to retain the new
command processor in memory after executing the specified commands.

  CREATEDD

Creates a memory dump diskette.

  DETACH [program [arguments]]

This command runs protected-mode programs and processes in the background.
However, detached programs will not be able to get keyboard input and all
screen output from them is lost. A process ID is returned when successfully
initiated.

  DPATH path;...

Similar to the APPEND command in real mode, DPATH creates a path
applications can search for opening data files, via the environment
variable, DPATH. Applications that use DPATH must search the environment to
obtain its values.

  ENDLOCAL

For batch file usage, this command restores the current working directory
and environment to the one saved by a previous SETLOCAL command.

  EXTPROC

This command defines an alternative command processor for use in processing
a batch file.

  KEYB

Specifies an alternative keyboard layout for users outside of the United
States.

  SETLOCAL

For batch file usage, this command preserves the current drive, directory,
and environment for later restoration by ENDLOCAL during a batch file run.

  SPOOL

Initiates the background print spooler.

  START "session name" [/c] [command [options]]

Initiate a new protected-mode session. The session's name (specified as an
argument) will appear in the Program Selector's menu.

  TRACE <on | off> [code[,...]]

Toggles system tracing on or off.

  TRACEFMT

Displays the contents of the system trace buffer in LIFO order.

───────────────────────────────────────────────────────────────────────────
Configuring OS/2
───────────────────────────────────────────────────────────────────────────

As with CONFIG.SYS, under MS-DOS users can extensively configure OS/2 by
placing commands in CONFIG.OS2. Several of these, such as break and fcbs,
are virtually identical to their DOS counterparts and are still available in
OS/2's real mode. Others──such as buffers, device, and shell──are also
available in protected mode. Device is used to install OS/2 device drivers.
In the context of OS/2's CONFIG.OS2 file, shell designates the real-mode
command processor, as it did for MS-DOS' COMMAND.COM.

In addition to these, however, there are a number of new commands available
for configuring OS/2.

DISKCACHE This command enables the diskcache and sets its size. By default
the cache is not enabled.

IOPL Gives protected-mode applications requesting them additional data IO
privileges. IOPL=YES must be in your CONFIG.OS2 file to run the protected-
mode version of CodeView, CVP. The default is YES.

LIBPATH Similar in form to PATH and DPATH, LIBPATH is a CONFIG.OS2 command
that specifies the location of dynamic-link libraries. The default LIBPATH
is the root of the boot drive.

MAXWAIT This command sets the maximum wait time, in seconds; this is the
time that must elapse before the scheduler increases a process' priority
when it hasn't been recognized. The default is 3 and can range from 1-255
seconds.

MEMMAN This command toggles virtual-memory swapping. Turning swapping on
will let OS/2 run more processes than will fit into memory, and
automatically turn move on. Having move on will let OS/2 temporarily move
data segments. However, performance tends to improve with swapping off, up
to a point. The default is SWAP, MOVE if the boot drive is a hard disk, and
noswap/nomove if the boot drive is a floppy.

PAUSEONERROR Lets OS/2 display error messages while processing CONFIG.OS2.
The default is YES.

PRIORITY This command influences the manner in which a process gets time
priority. OS/2 classifies priorities into three classes: time-critical,
normal, and idle-time. There are 32 priority levels in each class, and
priorities in the normal class can be adjusted.

Setting PRIORITY=ABSOLUTE in CONFIG.OS2 stops the system from dynamically
changing the priority of processes within the normal class.

If PRIORITY=DYNAMIC, OS/2 will try to determine which process most needs CPU
resources at any given time slice. If a process seems to have a lower
priority than it should, OS/2 will increase the priority level of that
process. The default is PRIORITY=DYNAMIC.

PROTECTONLY Lets you run both protected-mode and real-mode applications if
PROTECTONLY=NO. Although you can't run real-mode applications if PROTECTONLY
is set to NO, this will free some memory used by the real-mode compatibility
box (up to 640Kb). The default is NO.

PROTSHELL This command designates the protected-mode command processor and
program selector. The default is:

  PROTSHELL=SHELL.EXE CMD.EXE /K OS2INIT.CMD

REM Used to place comments in CONFIG.OS2.

RMSIZE Configures the amount of memory reserved for the real-mode
environment. The default is based on the total memory available, usually the
amount of memory installed below 1024Kb (either 512Kb or 640Kb).

RUN This command lets protected-mode programs or processes start during
system initialization. Thus with several RUN= commands in CONFIG.OS2, a
number of programs can be started before the OS/2 Program Selector appears.
Note that OS/2 processes all DEVICE= directives before any programs are run,
and that batch files (.CMD) are not runnable from CONFIG.OS2.

SWAPPATH The swap file is the temporary file OS/2 uses to implement its
virtual memory scheme. This command specifies the location of the swap file
(which should be placed on a hard disk). Note that the minimum swap file
size is 640Kb, and that this command is ignored if swapping is disabled (see
MEMMAN).

THREADS A thread is the portion of an application or process that OS/2 can
schedule. A process always contains at least one thread of execution, and
can contain multiple threads. These act like small programs that perform
particular tasks in each process.

The command sets the maximum number of threads available at one time. Note
that OS/2 uses about 14 threads. The system sets the default and each
additional thread uses a small amount of memory.

TIMESLICE A time slice is the interval of time OS/2 uses to schedule the
threads of a process. The TIMESLICE command specifies the minimum and
maximum amounts of time in milliseconds (thousands of a second) that OS/2
can dedicate to one process before checking on other processes. If only one
value is specified, both values will be set to it. OS/2 sets the default.
The minimum value must be greater than 31 and the maximum must be greater
than or equal to the minimum. Setting TIMESLICE=500 will make the scheduler
wait a half-second before checking other threads for a priority higher than
the threads currently executing.

TRACE OS/2 keeps a record of the actions it takes processing hardware
interrupts and functions, which can be helpful during program development.
System tracing, as this is called, can be toggled via TRACE=ON or TRACE=OFF
in CONFIG.OS2. You can trace individual types of events by specifying their
related event codes after the ON. You can issue TRACE=ON followed by
TRACE=OFF X,Y where X and Y are event codes corresponding to events that you
do not want traced. Tracing is OFF by default. The event codes must fall
into the range 0-255.

TRACEBUF This command sets, in kilobytes, the size of the trace buffer to be
allocated for the TRACE command.

In addition to the commands mentioned above, the following commands are also
available, but are really only applicable outside of the U.S.

CODEPAGE Selects a code page.

COUNTRY Selects time, date, and currency conventions.

DEVINFO Prepares a device for use with code page.

Note that OS/2 ignores the FILES and LASTDRIVE commands found in DOS
CONFIG.SYS files.


Sample CONFIG.OS2 Files

The following are two examples of CONFIG.OS2. The first is for OS/2 Version
1.0, while the second, for OS/2 Version 1.1 including the PM, is nearly
identical to that produced by the installation program.

Sample 1:

  REM CONFIG.OS2 FOR OS/2 v.1.0
  buffers=50
  shell=c:\command.com /P /E:500
  protshell=c:\shell.exe cmd.exe /k c:\os2init.cmd
  libpath=c:\os2\dll
  device=c:\os2\dev\mousea03.sys
  device=c:\os2\dev\pointdd.sys
  device=c:\os2\dev\com01.sys
  run=c:\os2\pbin\spool.exe c:\os2\spool /o:lpt1
  break=on
  files=25
  IOPL=YES
  TRACE=ON
  TRACEBUF=8
  run=c:\cmd.exe /c c:\cset
  diskcache=512

Sample 2:

  REM CONFIG.OS2 FOR OS/2 v.1.1 and PM
  buffers=30
  shell=c:\os2\rbin\command.com /P /e:1024 c:\os2\rbin
  rem the following entry must be all on one line!
  protshell=c:\os2\pmshell.exe c:\os2\pbin\cmd.exe /k c:\os2init.cmd
  rmsize=640
  protectonly=NO
  break=OFF
  fcbs=16,8
  threads=64
  iopl=YES
  libpath=c:\os2\dll
  set path=c:\os2;c:\os2\bin;c:\os2\pbin
  memman=SWAP,MOVE
  diskcache=64
  maxwait=3
  swappath=c:
  device=c:\os2\dev\mousea03.sys
  device=c:\os2\dev\pointdd.sys
  device=c:\os2\dev\pmdd.sys
  device=c:\os2\dev\com01.sys
  device=c:\os2\dev\ega.sys

───────────────────────────────────────────────────────────────────────────
Exploring the OS/2 SDK (Software Development Kit)
───────────────────────────────────────────────────────────────────────────

The OS/2 Software Development Kit (SDK) is the single most comprehensive
resource for OS/2 development tools and materials. While it costs $3,000,
it's so complete that you may seriously want to consider it, depending on
what types of OS/2 applications you (or your company) intend to develop.

In essence, the SDK puts all the tools together in one place. If you expect
to develop Presentation Manager or LAN Manager applications rather quickly,
the SDK contains everything you need in one purchase. On the other hand, if
you won't be developing Presentation Manager or LAN Manager programs right
away, you may want to consider purchasing the necessary components
separately.

Below is a list of the components included in the SDK. Note that in addition
to the Presentation Manager, the LAN Manager, and the OS/2 Programmer's
Toolkit, both MS Windows and the Windows SDK are included. The SDK includes
these since the Windows programming interface is very similar to that
provided by the Presentation Manager. The rule of thumb is: get to know the
Windows programming interface. Once you've done so, programming Presentation
Manager will be considerably easier.

  ■  MS OS/2 Version 1.0; MS OS/2 Version 1.1 (including Presentation
     Manager); MS Windows 2.03; MS OnLine; MS OS/2 Version 1.1 Toolkit.

  ■  Tools: Microsoft C Version 5.1; Microsoft Assembler Version 5.1;
     CodeView (real and protected mode); Utilities: Editor, Linker,
     Librarian, Bind (for creating dual-mode applications), Make, Grep,
     Incremental Linker.

  ■  QuickHelp for OS/2

  ■  OS/2 Programmer's Toolkit (PTK) Version 1.0.

  ■  MS OS/2 LAN Manager: User's Guide; Programmer's Reference.

  ■  MS OS/2 Presentation Manager: Conversion Guide; Reference, Volume I;
     Reference, Volume II; Reference, Addendum; "Programming the OS/2
     Presentation Manager" (and companion disk), by Charles Petzold.

  ■  MS Windows Version 2.03 Software Development Kit (SDK): Learning Guide;
     Tools, Style Guide, Extensions; Programmer's Reference Guide; Windows
     Applications Tools.

  ■  Inside OS/2 by Gordon Letwin.

  ■  A subscription to Microsoft System Journal.

───────────────────────────────────────────────────────────────────────────
Exploring the OS/2 Programmer's Toolkit
───────────────────────────────────────────────────────────────────────────

The Microsoft OS/2 Programmer's Toolkit contains many of the tools required
to build complex protected-mode applications under OS/2. The Toolkit
complements and completes the minimal program development tools necessary to
writing programs under OS/2. Included are:

  ■  Setup Guide and User's Guide

  ■  Programmer's Learning Guide and Programming Tools introduces writing C
     programs under OS/2. It presupposes experience in both. There are also
     discussions of how to create and build dynamic-link libraries.

  ■  Programmer's Reference includes all of the OS/2 system calls, etc.

  ■  QuickHelp

  ■  Sample Program examples demonstrate: memory allocation; multithreaded
     programming: thread examples in assembler and C, passing arguments to
     threads, using critical sections in threads, using DosKill and DosExit,
     suspending threads; controlling the speaker; accessing machine
     configuration and machine mode; accessing country information; aliasing
     code/data segments; accessing the machine date and time; Dynamic-Link
     Libraries in C and assembler; use of Exitlist; accessing volume
     information; accessing environment information; hello.c for OS/2;
     accessing process information; keyboard input; using monitors, pipes,
     and queues; moving files; accessing file handle information; handling
     sessions; shared memory; signal handling; register VIO subsystem;
     simple terminal emulation; simple screen editing; multithreaded text
     searching; directory navigation; listing files; LIFE game; EGA
     graphics; mouse interface.

  ■  Application Development Utilities includes, in addition to the
     appropriate header files and libraries, utilities for: creating dual-
     mode applications; searching libraries; searching files; creating
     import libraries; dumping .EXE headers; running the system trace;
     binding messages; dumping shared memory and files; searching for files;
     setting the keyboard delay and repeat rates.

  ■  A Subscription to MS(R) OnLine and two hours of usage.


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

Volume 4 - Number 2

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

Exploring Vector Fonts with the OS/2 Graphics Programming Interface

 Charles Petzold

Let's begin with a question: What graphics programming language stores fonts
as lines and curves (rather than bitmaps) and thus allows fonts to be
arbitrarily stretched, rotated, outlined, filled with different patterns, or
even used as clipping areas? One answer is obviously PostScript(R), Adobe
Systems' page composition language implemented on many high-end laser
printers (beginning with the Apple(R) LaserWriter(R)) and Allied
Corporation's Linotronic(R) phototypesetters. Over the past few years,
PostScript has become the language of choice for computer manipulation of
fonts and text.

An equally valid answer is GPI--the Graphics Programming Interface
(referred to herein as GPI) component of the OS/2 Presentation Manager. This
article shows you how to work with vector fonts in GPI and demonstrates many
PostScript-like techniques. As we'll see, GPI has facilities to do virtually
everything with fonts that you can do with PostScript. However, GPI does
have a deficiency that I will discuss at the end of this article.

The Trouble with Text

The display of text is always the most problematic part of a graphics
programming system. Unlike lines and polygons (which are merely mathematical
constructs), text is rooted in a long tradition of aesthetic typography. In
any computer graphics system, the goal must always be to display text that
is as pleasing and as easy to read as a well-printed book. Yet, most
computer output devices (such as video displays and printers) are digital
media. The subtly shaped and rounded characters that comprise traditional
fonts must be broken down into discrete pixels for storage and then
reassembled on the output device. This often causes distortions in the
appearance of the text.

One major advantage of using a computer for this job is versatility. We can
use a wide variety of fonts in various sizes and characteristics and modify
these fonts for display. The extent to which we can modify fonts depends on
the way in which the fonts are stored in memory.

Images and Vectors

A font is generally stored in computer (or printer) memory in one of two
very different ways. First, a font can be stored as an image or bitmap. Each
character of the font is simply a rectangular array of bits. The 0 bits
generally correspond to the background around the character and the 1 bits
correspond to the character itself. Second, a font can be stored in a vector
or outline format in which each character is defined as a series of lines
and curves that enclose areas. The character is displayed by drawing the
outline on the output device and filling in the enclosed areas.

Image and vector fonts have distinct advantages and disadvantages. Image
fonts are always created for specific font sizes and specific device
resolutions. The size of a particular image font cannot easily be changed.
(For example, enlarging an image font by doubling the rows and columns of
pixels often emphasizes the jaggedness of the characters.) Also, image fonts
cannot be rotated except possibly by 90 degree increments.

Vector fonts are much more malleable. Because they are defined as a series
of lines and curves, vector fonts can be stretched or compressed to any size
and can be rotated to any angle. Vector fonts are not tied to a particular
output device resolution.

In general, however, image fonts are more legible than vector fonts. Various
techniques are used to design image fonts so they fool the eye into thinking
the characters are smoother than they actually are. Vector
fonts--particularly when displayed on low-resolution devices and scaled
to small font sizes--can be adjusted only by mathematical algorithms,
which currently are less capable than human font designers. Another
advantage of image fonts is performance since vector fonts usually require
much more processing time to draw each character.

Most conventional laser printers store fonts as images, either within the
printer or in font cartridges. The printer is restricted to specific font
sizes and the characters cannot be arbitrarily rotated. Much more versatile
are the fonts stored in PostScript-based printers. These fonts are stored as
vectors. PostScript fonts can be stretched or compressed to any size, they
can be arbitrarily rotated, filled with various patterns, and used for
clipping.

The GPI Fonts

GPI can, of course, take advantage of fonts that are stored in and supported
by output devices such as laser printers. But it also includes its own
support of both image and vector fonts. The image fonts are expected because
they are particularly suited for low-resolution video displays and dot
matrix printers. Image fonts are an important part of most graphics
programming systems (such as Microsoft Windows GDI).

The addition of vector fonts in GPI is a real treat. GPI can use these
vector fonts with any output device. Thus, various font techniques that
previously have been restricted to PostScript printers are now possible with
other laser printers and even the video display.

OS/2 Version 1.1 is shipped with three resource-only dynamic-link libraries
with FON extensions. fIn addition, the video display (DISPLAY.DLL) and
printer device drivers may also contain fonts designed specifically for the
device. For example, the default System Proportional font is stored in
DISPLAY.DLL.

If you want to use any of the fonts in the FON files, you must install the
fonts from the Presentation Manager Control Panel program. It is only
necessary to install one font from each of the three files, and you only
need do this once.

Each font has a face name, which is shown in quotation marks in Figure 1.
Each of the image fonts is available in several point sizes and for several
output devices: the CGA, EGA, VGA (and 8514/A), and IBM(R) Proprinter. GPI
can synthesize variations of these fonts, such as italic or boldfaced
versions. Vector fonts, however, need not be designed for a particular
output device and point size because they can be arbitrary scaled. You'll
note that italic and boldface versions of the vector fonts are also
included.

The vector fonts in GPI are similar in style to the Courier, Helvetica(R),
and Times(R) fonts included in most PostScript printers.

A little exploration of the font files will reveal that vector fonts are
encoded as a series of GPI drawing orders. When drawing text with these
fonts, GPI translates the drawing orders into GPI functions, usually
GpiPolyLine to draw straight lines and GpiPolyFilletSharp to draw curves.



The VECTFONT Program

The VECTFONT program demonstrates the use of GPI vector fonts. For purposes
of clarity, I've divided the program into several modules. The files that
comprise the basic shell of the program are VECTFONT, VECTFONT.LNK,
VECTFONT.DEF, VECTFONT.H, and VECTFONT.C. (These files are not shown here,
but can be downloaded from any of our bulletin boards--Ed.) Figure 2
shows the code fragment from VECTFONT.C that is responsible for creating a
PS, and for sizing client windows.

The VECTFONT Display menu lists 16 options. The first option (to display
nothing) is the default. The other 15 options correspond to routines in the
VF01.C through VF15.C files (described below). The VF00.C file (not shown
here) contains some helper functions used by the routines in VF01.C through
VF15.C.

VECTFONT creates a micro presentation space during the WM_CREATE message
using page units of PU_TWIPS. (Twips is a fabricated word standing for 20th
of a point. A printer's point size is 1/72 inch, so page units correspond to
1/1440 inch.) VECTFONT then modifies the page viewport rectangle so that a
page unit corresponds to 1 point, which is the default coordinate system in
PostScript.

Although VECTFONT displays output to the screen, vector fonts are obviously
better suited for laser printers. As you will see, the appearance of the
fonts--even at 24 point sizes--is not nearly as good as the image
fonts.

You will also notice that several of the demonstration routines in VECTFONT
require a few seconds to run. For anything other than a demonstration
program, you would probably want to use a second thread of execution to
avoid holding up message processing.

Figure 2:

case WM_CREATE:\|  hdc = WinOpenWindowDC (hwnd) ;

                         // Create PS use Twips page units
     sizl.cx = 0 ;
     sizl.cy = 0 ;
     hps = GpiCreatePS (hab, hdc, &sizl,
                             PU_TWIPS     | GPIF_DEFAULT |
                             GPIT_MICRO   | GPIA_ASSOC) ;

                         // Adjust Page Viewport for points

     GpiQueryPageViewport (hps, &rcl) ;
     rcl.xRight *= 20 ;
     rcl.yTop   *= 20 ;
     GpiSetPageViewport (hps, &rcl) ;

     hwndMenu = WinWindowFromID (
                     WinQueryWindow (hwnd, QW_PARENT,
                                     FALSE), FID_MENU) ;
     return 0 ;

case WM_SIZE:
     ptlClient.x = SHORT1FROMMP (mp2) ;      // client width
     ptlClient.y = SHORT2FROMMP (mp2) ;      // client height

     GpiConvert (hps, CVTC_DEVICE, CVTC_PAGE, 1L, &ptlClient);
     return 0 ;



Selecting an Outline Font

To use an outline font in a Presentation Manager program, you must first
create a logical font and then select the font into your presentation space.
The GpiCreateLogFont function creates a logical font and associates the font
with a local ID (a number between 1L and 254L that you select). This
function requires a pointer to a structure of type FATTRS (font attributes)
that specifies the attributes of the font you want.

To create a vector font, most of the fields in this structure can be set to
zero. The most important fields are szFacename (which is set to the face
name of the font, one of the names in the last column of Figure 1) and
fsFontUse, which is set to the constant identifiers FATTR_FONTUSE_OUTLINE
and FATTR_FONTUSE_TRANSFORMABLE, combined with the C bitwise OR operator.

You may prefer using the CreateVectorFont function in VF00.C to create a
vector font. This function requires only the presentation space handle, the
local ID, and the face name:

CreateVectorFont(hps, lcid, szFacename);

After you create a logical font (using either GpiCreateLogFont or
CreateVectorFont), you can select the font into the presentation space:

GpiSetCharSet(hps, lcid);

The lcid parameter is the local ID for the font. After the font is selected
in the presentation space, you can alter the attributes of the font with
various functions described below, obtain information about the font by
calling GpiQueryFontMetrics, GpiQueryWidthTable, and GpiQueryTextBox; and
use the font for text output with one of the text functions such as
GpiCharStringAt.

When you no longer need the font, you reselect the default font into the
presentation space:

GpiSetCharSet(hps, LCID_DEFAULT);

and then delete the local ID associated with the font:

GpiDeleteSetId(hps, lcid);

In VECTFONT I always use the identifier LCID_MYFONT for the local ID. This
is defined in VECTFONT.H as 1L.

Scaling to a Point Size

When you call GpiSetCharSet to select a vector font into the presentation
space, the initial width and height of the font are based on the GPI
character box, which defines a character width and height in page units. The
default character box is based on the size of the default system font. You
can change the character box size by calling GpiSetCharBox.

An important point to note is that to get a correctly proportioned vector
font, you must change the character box size. Do not use the default.
Generally you set the height of the character box to the desired height of
the font. If you want a vector font to have a normal width, set the width of
the character box equal to the height. For a skinnier font, set the width of
the character box less than the height; for a fatter font, set the width
greater than the height.

If you're working in page units of PU_PELS, you must also adjust the
character box dimensions to account for differences in horizontal and
vertical resolution of the output device. For this reason, it's much easier
to work with vector fonts if you use one of the metric page units
(PU_LOENGLISH, PU_HIENGLISH, PU_LOMETRIC, PU_HIMETRIC, or PU_TWIPS). With
these page units, horizontal and vertical page units are the same. For
example, suppose you're using page units of PU_TWIPS. This means that one
page unit is equal to 1/20 of a point or 1/1440 inch. After selecting a
vector font into the presentation space, you want to scale the font to 24
points. You first define a structure of type SIZEF:

SIZEF sizfx;

The two fields of this structure, named cx and cy, are interpreted as 32-bit
FIXED numbers; that is, the high 16 bits are interpreted as an integer, and
the low 16 bits are interpreted as a fraction.

If you want to scale the vector font to a 24-point height, you can use the
MAKEFIXED macro to set to the fields of the structure like this:

sizfx.cx = MAKEFIXED(24 * 20, 0);
sizfx.cy = MAKEFIXED(24 * 20, 0);

Multiplying by 20 is necessary to convert the point size you want to twips.
Then call GpiSetCharBox:

GpiSetCharBox(hps, &sizfx);

After setting the character box, any character or text dimensions you obtain
from GpiQueryFontMetrics, GpiQueryTextBox, and GpiQueryWidthTable will
reflect the new font size.

The ScaleVectorFont routine in VF00.C can help in scaling a vector font to a
desired point size. The function will work with any page units, even
PU_PELS. The second and third parameters to ScaleVectorFont specify a point
size in units of 0.1 points. (For example, use 240 for a 24-point size.) If
you want a normally proportioned vector font, set the third parameter to
ScaleVectorFont equal to the second parameter.

The VF01.C file in Figure 3 shows how the functions discussed so far can be
used to display all the available vector fonts in 24-point size. You can run
the function in VF01.C by selecting the 24 Point Fonts option from the
VECTFONT Display menu. The source code and its results are shown in Figure
3.

Figure 3:

/*----------------------------------------
   VF01.C -- Display 24-point vector fonts
   ----------------------------------------*/
#define INCL_GPI
#include <os2.h>
#include <string.h>
#include "vectfont.h"

VOID Display_24Point (HPS hps, LONG cxClient, LONG cyClient)
     {
     static CHAR *szFacename[] = {
                                 "Courier",      "Courier Italic",
                                 "Courier Bold", "Courier Bold Italic",
                                 "Tms Rmn",      "Tms Rmn Italic",
                                 "Tms Rmn Bold", "Tms Rmn Bold Italic",
                                 "Helv",         "Helv Italic",
                                 "Helv Bold",    "Helv Bold Italic"
                                 } ;
     static INT  iNumFonts = sizeof szFacename / sizeof szFacename[0] ;
     FONTMETRICS fm ;
     INT         iFont ;
     POINTL      ptl ;

     ptl.x = cxClient / 8 ;
     ptl.y = cyClient ;

     for (iFont = 0 ; iFont < iNumFonts ; iFont++)
          {
                                   // Create font, select it and scale

          CreateVectorFont (hps, LCID_MYFONT, szFacename[iFont]) ;
          GpiSetCharSet (hps, LCID_MYFONT) ;
          ScaleVectorFont (hps, 240, 240) ;

                                   // Get font metrics for scaled font

          GpiQueryFontMetrics (hps, (LONG) sizeof (FONTMETRICS), &fm) ;
          ptl.y -= fm.lMaxBaselineExt ;

                                   // Display the font facename

          GpiCharStringAt (hps, &ptl, (LONG) strlen (szFacename[iFont]),
                           szFacename[iFont]) ;

          GpiCharString (hps, 10L, " - abcdefg") ;

          GpiSetCharSet (hps, LCID_DEFAULT) ;     // Clean up
          GpiDeleteSetId (hps, LCID_MYFONT) ;
          }
     }

Arbitrary Stretching

Besides scaling the vector font to a specific point size, you can also scale
vector fonts to fit within an arbitrary rectangle. For example, you may want
to scale a short text string to fill the client window.

The function shown in VF02.C (see Figure 4) shows how this is done. You can
run this function by selecting Stretched Font from VECTFONT's menu. The
function in VF02.C displays the word "Hello!" in the Tms Rmn Italic font
stretched to the size of the client window.

The ScaleFontToBox function in VF00.C helps out with this job. This function
first calls GpiQueryTextBox to obtain the coordinates of a parallelogram
that encompasses the text string. The character box is then scaled based on
the size of this text box and the rectangle to which the font must be
stretched. The QueryStartPointInTextBox function in VF00.C determines the
starting point of the text string (that is, the point at the baseline of the
left side of the first character) within this rectangle. This point is used
in the GpiCharStringAt function to display the text.

Figure 4:

/*----------------------------------------------------------
   VF02.C -- Display vector font stretched to client window
  ----------------------------------------------------------*/

#define INCL_GPI
#include <os2.h>
#include "vectfont.h"

VOID Display_Stretch (HPS hps, LONG cxClient, LONG cyClient)
     {
     static CHAR szText[] = "Hello!" ;
     static LONG cbText = sizeof szText - 1 ;
     POINTL      ptl ;

                              // Create font, select, and scale

     CreateVectorFont (hps, LCID_MYFONT, "Tms Rmn Italic") ;
     GpiSetCharSet (hps, LCID_MYFONT) ;
     ScaleFontToBox (hps, cbText, szText, cxClient, cyClient) ;
     QueryStartPointInTextBox (hps, cbText, szText, &ptl) ;

     GpiCharStringAt (hps, &ptl, cbText, szText) ;     // Display text

     GpiSetCharSet (hps, LCID_DEFAULT) ;               // Clean up
     GpiDeleteSetId (hps, LCID_MYFONT) ;
     }

Mirror Images

The character box, besides scaling the font to a particular size, can also
flip the characters around the horizontal or vertical axis.

If the character box height (the cy field of the SIZEF structure) is
negative, the characters will be flipped around the baseline and displayed
upside down. If the character box width (the cx field) is negative, the
individual characters will be flipped around the vertical axis. Moreover,
GpiCharStringAt will draw a character string from right to left. It's as
though the whole character string is flipped around the vertical line at the
left side of the first character.

The function in VF03.C (see Figure 5) displays the same string four times,
using all possible combinations of negative and positive character box
dimensions. You can run this function by selecting Mirrored Font from the
VECTFONT menu.

Figure 5:

/*------------------------------------------------------
   VF03.C -- Display four strings in mirror reflections
  ------------------------------------------------------*/

#define INCL_GPI
#include <os2.h>
#include "vectfont.h"

VOID Display_Mirror (HPS hps, LONG cxClient, LONG cyClient)
     {
     static CHAR szText[] = "Mirror" ;
     static LONG cbText = sizeof szText - 1 ;
     INT         i ;
     POINTL      ptl ;
     SIZEF       sizfx ;
                                   // Create font, select and scale

     CreateVectorFont (hps, LCID_MYFONT, "Tms Rmn Italic") ;
     GpiSetCharSet (hps, LCID_MYFONT) ;
     ScaleFontToBox (hps, cbText, szText, cxClient / 2, cyClient / 2) ;

     ptl.x = cxClient / 2 ;        // Center of client window
     ptl.y = cyClient / 2 ;

     for (i = 0 ; i < 4 ; i++)
          {
          GpiQueryCharBox (hps, &sizfx) ;

          if (i == 1 || i == 3)
               sizfx.cx *= -1 ;
                                   // Negate char box dimensions
          if (i == 2)
               sizfx.cy *= -1 ;

          GpiSetCharBox (hps, &sizfx) ;

          GpiCharStringAt (hps, &ptl, cbText, szText) ;
          }
     GpiSetCharSet (hps, LCID_DEFAULT) ;          // Clean up
     GpiDeleteSetId (hps, LCID_MYFONT) ;

     }

Transformations

Unlike image fonts, vector fonts can be scaled to any size and sheared and
rotated to any angle. The matrix transforms described in my article "OS/2
Graphics Programming Interface: An Introduction to Coordinate Spaces," MSJ
(Vol. 3, No. 4), affect vector fonts in the same way they affect the display
of other graphics primitives.

In addition, GPI also supports several functions specifically for performing
transforms on vector fonts. We've already seen how the GpiSetCharBox
function allows font characters to be scaled. The GpiSetCharAngle rotates
the font characters and the GpiSetCharShear performs x shear.

Character Angle and Rotation

By default, the baseline of the vector font characters is parallel to the x
axis in world coordinates. You can change this by calling GpiSetCharAngle.
This rotates the vector font's characters.

The character angle is specified using a GRADIENTL structure, which has two
fields named x and y of type LONG. Imagine a line connecting the point (0,0)
to the point (x,y) in world coordinates. The baseline of each character is
parallel to this line. The direction of the text is the same as the
direction from (0,0) to (x,y).

You can also think of this in trigonometric terms. The baseline of the text
is parallel to a line at angle a measured counterclockwise from the x axis,
where:

a = arctan (y/x)

and y and x are the two fields of the GRADIENTL structure.

The absolute magnitudes of y and x are not important. What's important are
the relative magnitudes and signs. The signs of x and y determine the
direction of the text string as indicated in the following table:

x    y    Direction

+    +    To upper right
-     +    To upper left
-    -    To lower left
    +    -    To lower right

The function in VF04.C (see Figure 6) uses the GpiSetCharAngle function to
display eight text strings at 45 degree increments around the center of the
client window. Each string displays the fields of the GRADIENTL structure
that is used in the GpiSetCharAngle call.

In this example, the text strings begin with a blank character so as not to
make a mess of overlapping characters in the center of the client window.
The character angle does not affect the interpretation of the starting
position of the string specified in the GpiCharStringAt function. If you
move your head so that a particular string is seen as running left to right,
the starting position still refers to the point at the baseline of the left
side of the first character.

You can make the characters in a text string follow a curved path by
individually calculating the starting position angle of each character and
displaying the characters one at a time. This is what is done in VF05.C (see
Figure 7) to display "Hello, World!" around the perimeter of a circle.

The text string is scaled based on the circumference of a circle that is
positioned in the center of the window and has a diameter half the width or
height (whichever is less) of the window. The GpiQueryWidthTable function is
used to obtain the width of individual characters and then space them around
the circle.

Figure 6:

/*------------------------------------------
   VF04.C -- Display eight character angles
  ------------------------------------------*/

#define INCL_GPI
#include <os2.h>
#include <stdio.h>
#include "vectfont.h"

VOID Display_CharAngle (HPS hps, LONG cxClient, LONG cyClient)
     {
     static GRADIENTL agradl[8] = { 100,    0,  100,  100,
                                      0,  100, -100,  100,
                                   -100,    0, -100, -100,
                                      0, -100,  100, -100 } ;
     CHAR             szBuffer[40] ;
     INT              iIndex ;
     POINTL           ptl ;

                              // Create Helvetica font

     CreateVectorFont (hps, LCID_MYFONT, "Helv") ;
     GpiSetCharSet (hps, LCID_MYFONT) ;
     ScaleVectorFont (hps, 200, 200) ;

     ptl.x = cxClient / 2 ;   // Center of client window
     ptl.y = cyClient / 2 ;

     for (iIndex = 0 ; iIndex < 8 ; iIndex++)
          {
          GpiSetCharAngle (hps, agradl + iIndex) ;     // Char angle

          GpiCharStringAt (hps, &ptl,
               (LONG) sprintf (szBuffer, " Character Angle (%ld,%ld)",
                               agradl[iIndex].x, agradl[iIndex].y),
               szBuffer) ;
          }
     GpiSetCharSet (hps, LCID_DEFAULT) ;               // Clean up
     GpiDeleteSetId (hps, LCID_MYFONT) ;
     }

Character Shear

It is easy to confuse the character angle with the character shear. Let's
look at the difference. The character angle refers to the orientation of the
baseline. As you can see in Figures 6 and 7, text displayed with various
character angles is rotated but otherwise not distorted in any way.

The character shear affects the appearance of the characters themselves
apart from any rotation. Character shear by itself bends characters to the
left or right, but the bottom of each character remains parallel to the x
axis. You can use character shear to create oblique (sometimes mistakenly
called italic) versions of a font.

To set character shear you call the GpiSetCharShear function. This function
requires a pointer to a structure of type POINTL, which has two fields named
x and y. Imagine a line drawn from (0,0) to the point (x,y) in world
coordinates. The left and right sides of each character are parallel to this
line.

The function shown in in VF06.C (see Figure 8) displays seven text strings
using different character shears. You can run this function by selecting
Character Shear from the VECTFONT menu. Each string displays the x and y
values used in the POINTL structure to set the character shear.

The character shear is governed by the relative magnitudes and signs of the
x and y fields of the POINTL structure. If the signs of both fields are the
same, then the characters tilt to the right; if they are different, the
characters tilt to the left. The character shear does not flip the
characters upside down. For example, character shear using the point
(100,100) has the same effect as (-100,-100).

The angle of the left and right sides of the characters from the y axis is
sometimes called the shear angle. In theory, the shear angle can range to
just above -180 degrees (infinite left shear) to just under +180 degrees
(infinite right shear) and is equal to

a = arctan (x/y)

where x and y are the two fields of the POINTL structure.

When you set a nondefault character shear, the GpiQueryTextBox function
returns an array of points that define a parallelogram rather than a
rectangle. However, the top and bottom sides of this text box are the same
width as for a nonsheared text string, and the distance between the top and
bottom sides also remains the same.

You can use character shear to draw an oblique shadow of a text string. The
function in VF07.C (see Figure 9) colors the background of the client window
blue and displays the text string Shadow twice.

The first call to GpiCharStringAt displays the shadow. This is drawn in dark
blue using a positive character shear. The second call to GpiCharStringAt
draws the characters upright in red with a slightly smaller character box
height. You can run this function by selecting Font with Shadow from the
VECTFONT menu.

Figure 7:

/*--------------------------------------------
   VF05.C -- Display "Hello, World" in circle
  --------------------------------------------*/

#define INCL_GPI
#include <os2.h>
#include <math.h>
#include <stdlib.h>
#include "vectfont.h"

VOID Display_Rotate (HPS hps, LONG cxClient, LONG cyClient)
     {
     static CHAR szText[] = "Hello, world! " ;
     static LONG cbText = sizeof szText - 1L ;
     static LONG alWidthTable[256] ;
     double      ang, angCharWidth, angChar ;
     FONTMETRICS fm ;
     GRADIENTL   gradl ;
     INT         iChar ;
     LONG        lCircum, lRadius, lTotWidth, lCharRadius, cyChar ;
     POINTL      ptl ;

                         // Create the font and get font metrics

     CreateVectorFont (hps, LCID_MYFONT, "Tms Rmn") ;
     GpiSetCharSet (hps, LCID_MYFONT) ;

     GpiQueryFontMetrics (hps, (LONG) sizeof fm, &fm) ;

                         // Find circle dimensions and scale font

     lRadius = min (cxClient / 4, cyClient / 4) ;
     lCircum = (LONG) (2 * PI * lRadius) ;
     cyChar  = fm.lMaxBaselineExt * lRadius / fm.lMaxAscender ;

     ScaleFontToBox (hps, cbText, szText, lCircum, cyChar) ;

                         // Obtain width table and total width

     GpiQueryWidthTable (hps, 0L, 256L, alWidthTable) ;

     for (lTotWidth = 0, iChar = 0 ; iChar < (INT) cbText ; iChar ++)
          lTotWidth += alWidthTable [szText [iChar]] ;

     ang = PI / 2 ;      // Initial angle for first character

     for (iChar = 0 ; iChar < (INT) cbText ; iChar++)
          {
                         // Set character angle

          angCharWidth = 2 * PI * alWidthTable [szText [iChar]] / lTotWidth
;

          gradl.x = (LONG) (lRadius * cos (ang - angCharWidth / 2 - PI / 2))
;
          gradl.y = (LONG) (lRadius * sin (ang - angCharWidth / 2 - PI / 2))
;

          GpiSetCharAngle (hps, &gradl) ;

                         // Find position for character and display it

          angChar = atan2 ((double) alWidthTable [szText [iChar]] / 2,
                           (double) lRadius) ;

          lCharRadius = (LONG) (lRadius / cos (angChar)) ;
          angChar += ang - angCharWidth / 2 ;

          ptl.x = (LONG) (cxClient / 2 + lCharRadius * cos (angChar)) ;
          ptl.y = (LONG) (cyClient / 2 + lCharRadius * sin (angChar)) ;

          GpiCharStringAt (hps, &ptl, 1L, szText + iChar) ;

          ang -= angCharWidth ;
          }
     GpiSetCharSet (hps, LCID_DEFAULT) ;          // Clean up
     GpiDeleteSetId (hps, LCID_MYFONT) ;
     }

Figure 8:

/*----------------------------------------------------------
   VF06.C -- Display seven different character shear angles
  ----------------------------------------------------------*/

#define INCL_GPI
#include <os2.h>
#include <stdio.h>
#include "vectfont.h"

VOID Display_CharShear (HPS hps, LONG cxClient, LONG cyClient)
     {
     static POINTL aptlShear[7] = { -100,  41, -100, 100,
                                     -41, 100,    0, 100,
                                      41, 100,  100, 100,
                                     100,  41 } ;
     CHAR          szBuffer[40] ;
     FONTMETRICS   fm ;
     INT           iIndex ;
     POINTL        ptl ;

                         // Create and scale Helvetica font

     CreateVectorFont (hps, LCID_MYFONT, "Helv") ;
     GpiSetCharSet (hps, LCID_MYFONT) ;
     ScaleVectorFont (hps, 480, 480) ;

                         // Get font metrics for scaled font

     GpiQueryFontMetrics (hps, (LONG) sizeof (FONTMETRICS), &fm) ;

     ptl.x = cxClient / 8 ;
     ptl.y = cyClient ;

     for (iIndex = 0 ; iIndex < 7 ; iIndex++)
          {
          GpiSetCharShear (hps, aptlShear + iIndex) ;  // Char shear

          ptl.y -= fm.lMaxBaselineExt ;

          GpiCharStringAt (hps, &ptl,
               (LONG) sprintf (szBuffer, "Character Shear (%ld,%ld)",
                         aptlShear[iIndex].x, aptlShear[iIndex].y),
               szBuffer) ;
          }
     GpiSetCharSet (hps, LCID_DEFAULT) ;               // Clean up
     GpiDeleteSetId (hps, LCID_MYFONT) ;
     }

Figure 9:

/*--------------------------------------------------
   VF07.C -- Display characters with sheared shadow
  --------------------------------------------------*/

#define INCL_GPI
#include <os2.h>
#include "vectfont.h"

VOID Display_Shadow (HPS hps, LONG cxClient, LONG cyClient)
     {
     static CHAR szText[] = "Shadow" ;
     static LONG cbText = sizeof szText - 1 ;
     POINTL      ptl, ptlShear ;
     SIZEF       sizfx ;

     CreateVectorFont (hps, LCID_MYFONT, "Tms Rmn Italic") ;
     GpiSetCharSet (hps, LCID_MYFONT) ;
     ScaleFontToBox (hps, cbText, szText, 3 * cxClient / 4, cyClient) ;
     QueryStartPointInTextBox (hps, cbText, szText, &ptl) ;

     ColorClient (hps, cxClient, cyClient, CLR_BLUE) ;

     GpiSavePS (hps) ;

     ptlShear.x = 200 ;                             // Set char shear
     ptlShear.y = 100 ;
     GpiSetCharShear (hps, &ptlShear) ;

     GpiQueryCharBox (hps, &sizfx) ;
     sizfx.cy += sizfx.cy / 4 ;                     // Set char box
     GpiSetCharBox (hps, &sizfx) ;

     GpiSetColor (hps, CLR_DARKBLUE) ;
     GpiCharStringAt (hps, &ptl, cbText, szText) ;  // Display shadow

     GpiRestorePS (hps, -1L) ;

     GpiSetColor (hps, CLR_RED) ;
     GpiCharStringAt (hps, &ptl, cbText, szText) ;  // Display text

     GpiSetCharSet (hps, LCID_DEFAULT) ;            // Clean up
     GpiDeleteSetId (hps, LCID_MYFONT) ;
     }

A Primer on Paths

To explore more capabilities of vector fonts, it's necessary to become
familiar with the GPI path, which is similar to a PostScript path. In GPI,
you create a path by calling line drawing functions between calls to the
GpiBeginPath and GpiEndPath functions:

GpiBeginPath(hps, idPath);
    <... call line drawing functions ... >
GpiEndPath (hps) ;

This is called a path bracket. In OS/2 1.1, idPath must be set equal to 1L.
The functions that are valid within a path bracket are listed in the
documentation of the Presentation Manager functions.

The functions you call within the path bracket do not draw anything.
Instead, the lines that make up the path are retained by the system. Often
the lines you draw in a path will enclose areas, but they don't have to.

After the GpiEndPath call, you can do one of three things with the path
you've created:

■    Call GpiStrokePath to draw the lines that comprise the path. These
lines are drawn using the geometric line width, joins, and ends (discussed
shortly).

■    Call GpiFillPath to fill enclosed areas defined by the path. Any open
areas are automatically closed. The area is filled with the current pattern.

■    Call GpiSetClipPath to make the enclosed areas of the path a clipping
area. Any open areas are automatically closed. Subsequent GPI calls will
only display output within the enclosed area defined by the path.

Each of these three functions causes the path to be deleted. Prior to
calling any of these three functions, you can call GpiModifyPath, which I'll
describe toward the end of this article.

Normally, GpiCharStringAt and the other text output functions are not valid
in a path bracket. The exception is when a vector font is selected in the
presentation space. When called from within a path bracket, GpiCharStringAt
does not draw the text string. Instead, the outlines of the characters
become part of the path.

Paths opens up a whole collection of PostScript-like stylistic techniques
that you can use with vector fonts.

Hollow Characters

Let's begin by calling GpiCharStringAt in a path bracket and then use
GpiStrokePath to draw the lines of the path. GpiStrokePath has the following
syntax:

GpiStrokePath(hps, idPath, 0L);

In the initial version of the Presentation Manager, the last parameter of
the function must be set to 0L. When used to stroke a path created by
calling GpiCharStringAt, only the outline is drawn and not the interiors.
This creates hollow characters.

The function in VF08.C (see Figure 10) uses this technique to display the
outline of the characters in the text string Hollow. You can run this
function by selecting Hollow Font from the VECTFONT menu.

You may want to display characters in one color with an outline of another
color. In this case, you must call GpiCharStringAt twice, first to draw the
interior in a specific color and second, in a path bracket followed by a
GpiStrokePath call to draw the outline. The function in VF09.C (see Figure
11) does something like this to draw text with a drop shadow.

The shadow is drawn first using a normal GpiCharStringAt call. Another
GpiCharStringAt function draws the text again in the current window
background color at a 1/6-inch offset to the first string. This is
surrounded by an outline created by a third GpiCharStringAt call in a path
bracket followed by GpiStrokePath.

Quite similar to this is the creation of characters that look like solid
blocks, as shown in the function in VF10.C ( see Figure12). Eighteen
character strings are drawn at 1-point offsets using CLR_DARKGREEN. This is
capped by the character string drawn again in CLR_GREEN and the border in
CLR_DARKGREEN.

Figure 10:

/*-----------------------
   VF08.C -- Hollow font
  -----------------------*/

#define INCL_GPI
#include <os2.h>
#include "vectfont.h"

VOID Display_Hollow (HPS hps, LONG cxClient, LONG cyClient)
     {
     static CHAR szText[] = "Hollow" ;
     static LONG cbText = sizeof szText - 1 ;
     POINTL      ptl ;

     CreateVectorFont (hps, LCID_MYFONT, "Tms Rmn Italic") ;
     GpiSetCharSet (hps, LCID_MYFONT) ;
     ScaleFontToBox (hps, cbText, szText, cxClient, cyClient) ;
     QueryStartPointInTextBox (hps, cbText, szText, &ptl) ;

     GpiBeginPath (hps, ID_PATH) ;
     GpiCharStringAt (hps, &ptl, cbText, szText) ;     // Text in path
     GpiEndPath (hps) ;

     GpiStrokePath (hps, ID_PATH, 0L) ;                // Stroke path

     GpiSetCharSet (hps, LCID_DEFAULT) ;               // Clean up
     GpiDeleteSetId (hps, LCID_MYFONT) ;

Figure 11:

*---------------------------------
   VF09.C -- Font with Drop Shadow
  ---------------------------------*/

#define INCL_GPI
#include <os2.h>
#include "vectfont.h"

VOID Display_DropShadow (HPS hps, LONG cxClient, LONG cyClient)
     {
     static CHAR szText[] = "Hello!" ;
     static LONG cbText = sizeof szText - 1 ;
     POINTL      ptl ;

     CreateVectorFont (hps, LCID_MYFONT, "Tms Rmn Italic") ;
     GpiSetCharSet (hps, LCID_MYFONT) ;
     ScaleFontToBox (hps, cbText, szText, cxClient, cyClient) ;
     QueryStartPointInTextBox (hps, cbText, szText, &ptl) ;

     GpiCharStringAt (hps, &ptl, cbText, szText) ;     // Shadow

     ptl.x -= 12 ;       // 1/6 inch
     ptl.y += 12 ;

     GpiSetColor (hps, CLR_BACKGROUND) ;
     GpiCharStringAt (hps, &ptl, cbText, szText) ;     // Text string

     GpiBeginPath (hps, ID_PATH) ;
     GpiCharStringAt (hps, &ptl, cbText, szText) ;     // Outline
     GpiEndPath (hps) ;

     GpiSetColor (hps, CLR_NEUTRAL) ;
     GpiStrokePath (hps, ID_PATH, 0L) ;

     GpiSetCharSet (hps, LCID_DEFAULT) ;               // Clean up
     GpiDeleteSetId (hps, LCID_MYFONT) ;
     }

Figure 12:

/*----------------------------
   VF10.C -- Solid block font
  ----------------------------*/

#define INCL_GPI
#include <os2.h>
#include "vectfont.h"

VOID Display_Block (HPS hps, LONG cxClient, LONG cyClient)
     {
     static CHAR szText[] = " Block " ;
     static LONG cbText = sizeof szText - 1 ;
     INT         i ;
     POINTL      ptl ;

     CreateVectorFont (hps, LCID_MYFONT, "Tms Rmn Italic") ;
     GpiSetCharSet (hps, LCID_MYFONT) ;
     ScaleFontToBox (hps, cbText, szText, cxClient, cyClient) ;
     QueryStartPointInTextBox (hps, cbText, szText, &ptl) ;

     ColorClient (hps, cxClient, cyClient, CLR_WHITE) ;
     GpiSetColor (hps, CLR_DARKGREEN) ;

     for (i = 0 ; i < 18 ; i++)
          {
          GpiCharStringAt (hps, &ptl, cbText, szText) ;     // Block

          ptl.x -= 1 ;
          ptl.y -= 1 ;
          }

     GpiSetColor (hps, CLR_GREEN) ;
     GpiCharStringAt (hps, &ptl, cbText, szText) ;     // Text string

     GpiBeginPath (hps, ID_PATH) ;
     GpiCharStringAt (hps, &ptl, cbText, szText) ;     // Outline
     GpiEndPath (hps) ;

     GpiSetColor (hps, CLR_DARKGREEN) ;
     GpiStrokePath (hps, ID_PATH, 0L) ;

     GpiSetCharSet (hps, LCID_DEFAULT) ;               // Clean up
     GpiDeleteSetId (hps, LCID_MYFONT) ;
     }


Filling the Path

When first introducing paths, I mentioned that you can do one of three
things with a path: stroke it, fill it, or use it for clipping. Let's move
on to the second; filling the path.

To fill a path, you call:

GpiFillPath(hps, idPath, lOption);

This fills the path with the current pattern. Any open areas of the path are
automatically closed before being filled. The lOption parameter can be
either FPATH_ALTERNATE or FPATH_WINDING to fill the path using alternate or
winding modes. For vector fonts, FPATH_WINDING causes the interiors of some
letters (such as O) to be filled. You'll probably want to use
FPATH_ALTERNATE instead.

If the current area filling pattern is PATSYM_SOLID, the code

GpiBeginPath(hps, idPath);
GpiCharStringAt(hps, &ptl,
             cch, szText);
GpiEndPath(hps);
GpiFillPath(hps,idPath,
          FPATH_ALTERNATE);

does roughly the same thing with a vector font as does a GpiCharStringAt by
itself. When using GpiFillPath you will want to set a pattern other than
PATSYM_SOLID. (A bug in OS/2 1.1 causes the current pattern to be reset to
PATSYM_SOLID during a path bracket in which GpiCharStringAt is called. You
can get around this bug by calling GpiSetPattern after you end the path.)

The function in VF11.C (see Figure 13) uses GpiFillPath to display the text
string Fade eight times filled with the eight GPI shading patterns
(PATSYM_DENSE1 through PATSYM_DENSE8) and then finishes by calling
GpiCharStringAt outside of a path bracket. You can run this function by
selecting Fading Font from the VECTFONT menu.

Figure 13:

/*------------------------------------------------------
   VF11.C -- Fading font with various pattern densities
  ------------------------------------------------------*/

#define INCL_GPI
#include <os2.h>
#include "vectfont.h"

VOID Display_Fade (HPS hps, LONG cxClient, LONG cyClient)
     {
     static CHAR szText[] = "Fade" ;
     static LONG cbText = sizeof szText - 1 ;
     LONG        lPattern ;
     POINTL      ptl ;

     CreateVectorFont (hps, LCID_MYFONT, "Tms Rmn Italic") ;
     GpiSetCharSet (hps, LCID_MYFONT) ;
     ScaleFontToBox (hps, cbText, szText, cxClient, cyClient) ;
     QueryStartPointInTextBox (hps, cbText, szText, &ptl) ;

     GpiSetBackMix (hps, BM_OVERPAINT) ;

     for (lPattern = 8 ; lPattern >= 1 ; lPattern--)
          {
          GpiBeginPath (hps, ID_PATH) ;
          GpiCharStringAt (hps, &ptl, cbText, szText) ;  // Text out
          GpiEndPath (hps) ;

          GpiSetPattern (hps, lPattern) ;
          GpiFillPath (hps, ID_PATH, FPATH_ALTERNATE) ;  // Fill path

          ptl.x += 2 ;
          ptl.y -= 2 ;
          }

     GpiSetPattern (hps, PATSYM_SOLID) ;
     GpiSetBackMix (hps, BM_LEAVEALONE) ;
     GpiCharStringAt (hps, &ptl, cbText, szText) ;       // Solid

     GpiSetCharSet (hps, LCID_DEFAULT) ;                 // Clean up
     GpiDeleteSetId (hps, LCID_MYFONT) ;
     }

Geometrically Thick Lines

At first, it may seem as though there is no difference between drawing a
line normally, like this:

GpiMove(hps, &ptlBeg);
GpiLine(hps, &ptlEnd);

and calling these same two functions within a path bracket and then stroking
the path, like this:

GpiBeginPath(hps, idPath);
GpiMove(hps, &ptlBeg);
GpiLine(hps, &ptlEnd);
GpiEndPath(hps);
GpiStrokePath(hps, idPath, 0);

There are, in fact, some very significant differences.

First, the line drawn with the normal GpiLine function has what is called a
cosmetic line width. The default width of the line is based on the
resolution of the output device. It is a device-dependent width for a normal
line. The width of the line does not change when you use matrix transforms
to set different scaling factors in the coordinate space. Although GPI
provides a function called GpiSetLineWidth to change the cosmetic line
width, this function is not implemented in MS (R) OS/2 Version 1.1.

But a line drawn by stroking a path has a geometric line width. This is a
line width (in world coordinates) that you set with the GpiSetLineWidthGeom
function. Because this line width is specified in world coordinates, it is
affected by any scaling that you set using matrix transforms.

Second, a line drawn with GpiLine can have different line types that you set
with the GpiSetLineType function. These line types are various combinations
of dots and dashes. The line is drawn with the current line color and the
current line mix.

But a line drawn by stroking a path does not use the line type. The line is
instead treated as an area that follows the path of the line but which has a
geometric width. This area is filled with the pattern that you set with
GpiSetPattern, and is colored with the current pattern foreground and
background color, and the current pattern foreground and background mix.

Third, a line drawn by stroking the path can have various types of line
joins and ends. By calling GpiSetLineJoin you can specify that lines meet
with a rounded, square, or miter join. GpiSetLineEnd lets you specify
rounded, square, or flat ends to the lines.

The function in the VF12.C file (see Figure 14) demonstrates the use of
geometrically thick lines filled with patterns to give the letters a kind of
neon look.

You can run this function by selecting Neon Effect from the VECTFONT menu.
The function strokes the path using various geometric line widths filled
with a PATSYM_HALFTONE pattern and several colors. The outline of the font
is white, but it is surrounded with a halo of red.

A better effect could be achieved on devices capable of more than 16 colors.
In this case, you can use a solid pattern but color each stroke with a
different shade of red.

Figure 14:

/*-----------------------------------------------------
   VF12.C -- Neon font using geometricall thick lines
  -----------------------------------------------------*/

#define INCL_GPI
#include <os2.h>
#include "vectfont.h"

VOID Display_Neon (HPS hps, LONG cxClient, LONG cyClient)
     {
     static CHAR szText[] = " Neon " ;
     static LONG cbText = sizeof szText - 1 ;
     static LONG lForeColor[] = { CLR_DARKRED, CLR_DARKRED, CLR_RED,
                                  CLR_RED,     CLR_WHITE,   CLR_WHITE };
     static LONG lBackColor[] = { CLR_BLACK,   CLR_DARKRED, CLR_DARKRED,
                                  CLR_RED,     CLR_RED,     CLR_WHITE };
     static LONG lWidth[] = { 34, 28, 22, 16, 10, 4 } ;

     INT         iIndex ;
     POINTL      ptl ;

     CreateVectorFont (hps, LCID_MYFONT, "Tms Rmn Italic") ;
     GpiSetCharSet (hps, LCID_MYFONT) ;
     ScaleFontToBox (hps, cbText, szText, cxClient, cyClient) ;
     QueryStartPointInTextBox (hps, cbText, szText, &ptl) ;

     ColorClient (hps, cxClient, cyClient, CLR_BLACK) ;

     for (iIndex = 0 ; iIndex < 6 ; iIndex++)
          {
          GpiBeginPath (hps, ID_PATH) ;
          GpiCharStringAt (hps, &ptl, cbText, szText) ;   // Text out
          GpiEndPath (hps) ;

          GpiSetColor (hps, lForeColor[iIndex]) ;
          GpiSetBackColor (hps, lBackColor[iIndex]) ;
          GpiSetBackMix (hps, BM_OVERPAINT) ;
          GpiSetPattern (hps, PATSYM_HALFTONE) ;
          GpiSetLineWidthGeom (hps, lWidth[iIndex]) ;

          GpiStrokePath (hps, ID_PATH, 0L) ;           // Stroke path
          }
     GpiSetCharSet (hps, LCID_DEFAULT) ;               // Clean up
     GpiDeleteSetId (hps, LCID_MYFONT) ;
     }

Clipping to the Text Characters

The third option after creating a path is to call GpiSetClipPath:

GpiSetClipPath(hps, idPath, lOption);

The lOption parameter can be either SCP_RESET (which equals 0L, so it's the
default) or SCP_AND. The SCP_RESET option causes the clipping path to be
reset so that no clipping occurs. The SCP_AND option sets the new clipping
path to the intersection of the old clipping path and the path you've just
defined in a path bracket. Any open areas in the path are automatically
closed.

You can combine the SCP_AND option with either SCP_ALTERNATE (the default)
or SCP_WINDING. As with GpiFillPath, you'll probably want to use alternate
mode when working with paths created from vector fonts.

The function in VF13.C (see Figure 15) calls GpiCharStringAt with the text
string WOW within a path bracket. This is followed by a call to
GpiSetClipPath. The clipping path is now the interior of the letters. The
function draws a series of colored lines emanating from the center of the
client window.

The function in VF14.C (not shown here) uses a similar technique but draws a
series of areas defined by splines.

Figure 15:

/*--------------------------
   VF13.C -- Clipped Spokes
  --------------------------*/

#define INCL_GPI
#include <os2.h>
#include <math.h>
#include "vectfont.h"

VOID Display_Spokes (HPS hps, LONG cxClient, LONG cyClient)
     {
     static CHAR szText[] = "WOW" ;
     static LONG cbText = sizeof szText - 1 ;
     static LONG lColors[] = { CLR_BLUE, CLR_GREEN, CLR_CYAN,
                               CLR_RED,  CLR_PINK,  CLR_YELLOW,
                               CLR_WHITE } ;
     double      dMaxRadius ;
     INT         i, iNumColors = sizeof lColors / sizeof lColors[0] ;
     POINTL      ptl ;

     CreateVectorFont (hps, LCID_MYFONT, "Tms Rmn") ;
     GpiSetCharSet (hps, LCID_MYFONT) ;
     ScaleFontToBox (hps, cbText, szText, cxClient, cyClient) ;
     QueryStartPointInTextBox (hps, cbText, szText, &ptl) ;

     ColorClient (hps, cxClient, cyClient, CLR_BLACK) ;

     GpiBeginPath (hps, ID_PATH) ;
     GpiCharStringAt (hps, &ptl, cbText, szText) ;     // Text string
     GpiEndPath (hps) ;

     GpiSetClipPath (hps, ID_PATH, SCP_AND | SCP_ALTERNATE) ;

     dMaxRadius = sqrt (pow (cxClient / 2.0, 2.0) +
                        pow (cyClient / 2.0, 2.0)) ;
                                                       // Draw spokes
     for (i = 0 ; i < 360 ; i++)
          {
          GpiSetColor (hps, lColors[i % iNumColors]) ;

          ptl.x = cxClient / 2 ;
          ptl.y = cyClient / 2 ;
          GpiMove (hps, &ptl) ;

          ptl.x += (LONG) (dMaxRadius * cos (i * 6.28 / 360)) ;
          ptl.y += (LONG) (dMaxRadius * sin (i * 6.28 / 360)) ;
          GpiLine (hps, &ptl) ;
          }
     GpiSetCharSet (hps, LCID_DEFAULT) ;               // Clean up
     GpiDeleteSetId (hps, LCID_MYFONT) ;
     }

Modifying the Path

Between the call to GpiEndPath to end the path and the call to
GpiStrokePath, GpiFillPath, or GpiSetClipPath, you can call GpiModifyPath.
This function uses the current geometric line width, join, and end to
convert every line in the path to a new line that encloses an area around
the old line. For example, suppose that the path contained a single straight
line. After GpiModifyPath the path would contain a closed line in the shape
of a hot dog. The width of this hot dog is the geometric line width. The
ends of the hot dog could be round, square, or flat, depending on the
current line end attribute.

Following the creation of a path, these two functions in succession:

GpiModifyPath(hps, ID_PATH, MPATH_STROKE);
GpiFillPath(hps, ID_PATH,
            FPATH_WINDING) ;

are usually equivalent to:

GpiStrokePath(hps, ID_PATH, 0L);

GpiModifyPath and GpiStrokePath are the only two functions that use the
geometric line width, joins, and ends.

In theory, you can call GpiStrokePath after GpiModifyPath, like this:

GpiModifyPath(hps, ID_PATH,
              MPATH_STROKE);
GpiStrokePath(hps, ID_PATH,
              0L);

This should do something, and it should be rather interesting, but GPI
usually reports that it can't create the path because it's too complex.

Instead, let's look at GpiModifyPath followed by GpiSetClipPath. The
function in VF15.C (see Figure 16) is almost the same as the one in VF13.C
(see Figure 15) except that it sets the geometric line width to 6 (1/12
inch) and calls GpiModifyPath before calling GpiSetClipPath.

Note that the colored lines are clipped not to the interior of the
characters but to their original outlines. By the use of GpiModifyPath, the
outlines of the characters have themselves been made into a path that is
1/12 inch wide. This is the path that is used for clipping.

Figure 16:

/*--------------------------
   VF15.C -- Clipped Spokes
  --------------------------*/

#define INCL_GPI
#include <os2.h>
#include <math.h>
#include "vectfont.h"

VOID Display_ModSpokes (HPS hps, LONG cxClient, LONG cyClient)
     {
     static CHAR szText[] = "WOW" ;
     static LONG cbText = sizeof szText - 1 ;
     static LONG lColors[] = { CLR_BLUE, CLR_GREEN, CLR_CYAN,
                               CLR_RED,  CLR_PINK,  CLR_YELLOW,
                               CLR_WHITE } ;
     double      dMaxRadius ;
     INT         i, iNumColors = sizeof lColors / sizeof lColors[0] ;
     POINTL      ptl ;

     CreateVectorFont (hps, LCID_MYFONT, "Tms Rmn") ;
     GpiSetCharSet (hps, LCID_MYFONT) ;
     ScaleFontToBox (hps, cbText, szText, cxClient, cyClient) ;
     QueryStartPointInTextBox (hps, cbText, szText, &ptl) ;

     ColorClient (hps, cxClient, cyClient, CLR_BLACK) ;

     GpiBeginPath (hps, ID_PATH) ;
     GpiCharStringAt (hps, &ptl, cbText, szText) ;     // Text string
     GpiEndPath (hps) ;

     GpiSetLineWidthGeom (hps, 6L) ;                   // 1/12 inch
     GpiModifyPath (hps, ID_PATH, MPATH_STROKE) ;
     GpiSetClipPath (hps, ID_PATH, SCP_AND | SCP_ALTERNATE) ;

     dMaxRadius = sqrt (pow (cxClient / 2.0, 2.0) +
                        pow (cyClient / 2.0, 2.0)) ;
                                                       // Draw spokes
     for (i = 0 ; i < 360 ; i++)
          {
          GpiSetColor (hps, lColors[i % iNumColors]) ;

          ptl.x = cxClient / 2 ;
          ptl.y = cyClient / 2 ;
          GpiMove (hps, &ptl) ;

          ptl.x += (LONG) (dMaxRadius * cos (i * 6.28 / 360)) ;
          ptl.y += (LONG) (dMaxRadius * sin (i * 6.28 / 360)) ;
          GpiLine (hps, &ptl) ;
          }
     GpiSetCharSet (hps, LCID_DEFAULT) ;               // Clean up
     GpiDeleteSetId (hps, LCID_MYFONT) ;
     }

Is It Enough?

I think it's clear that the facilities provided by GPI for working with
vector fonts equal--and sometimes exceed--those in PostScript. The
GPI interface is very powerful and very versatile.

Is that enough? No, it's not. The implementation of vector fonts in GPI has
a structural flaw that still leaves PostScript the king.

Take a close and careful look at Figure 3 and the display of the Helv font.
You'll notice that the two legs of the H are different in width by one pixel
when they should be the same width. This is undoubtedly caused by a rounding
error. It's obviously more noticeable on a low-resolution video display than
it would be on a 300 dpi laser printer, but even on a laser printer such
errors will affect the legibility of the text.

Errors such as this do not occur with PostScript fonts. PostScript fonts are
true algorithms that are able to recognize and correct any anomalies in the
rendition of the characters. In contrast, GPI fonts (which are encoded as a
simple series of polylines and polyfillets) are drawn blindly without any
feedback or correction.

So, while we can rejoice in what we have in GPI, there is still the need for
some improvement.

Basic as a Professional Programming Language (An Interview with Ethan Winer)

C and Macro Assembler (MASM) are the languages of choice for most
programmers working in the personal computing environment. Occasionally one
hears of a programmer using BASIC, but generally what one hears from
professional programmers is that BASIC is a toy language--nice for
learning, but hardly a language for serious program development.

That attitude is beginning to change. In fact, BASIC has grown up quite a
bit over the past year. The Microsoft(R) QuickBASIC Version 4 user interface
provides an effective working environment for both the beginner and the
advanced user, and the BASIC language itself, via the Microsoft BASIC
Compiler Version 6.0, now has the professional language features programmers
need.

One believer in the ability of BASIC to handle the needs of programmers is
Ethan Winer, founder of Crescent Software--a firm that specializes in
software development in BASIC--as well as the developer of QuickPak
Professional, a library of advanced routines designed especially for the
BASIC compiler environment. Winer has also written numerous articles on
BASIC for many industry publications.

Recently we talked to Winer, who shared with us the reasons BASIC is his
language of choice, his reasons for developing QuickPak Professional, and
some insights into the technical issues involved in BASIC programming.

MSJ: Why has BASIC become the language of choice for you and Crescent
Software?

EW: Unlike C and Pascal, Microsoft BASIC has always included a wealth of
built-in commands and features. It can produce sophisticated graphics on an
assortment of monitors, open and manipulate files and devices, play music,
and access memory and hardware ports directly. The release of Microsoft
QuickBASIC 4 and the BASIC 6.0 compiler, has added even more commands and
features.

BASIC has always been the easiest of the high-level languages to use, and
the latest version is as powerful and capable as either C or Pascal.
Furthermore, Microsoft has made clear they intend to keep improving both
their BASIC 6.0 Professional Compiler and Microsoft QuickBASIC.

Of the features that raise BASIC to the level of other professional
languages, which do you find particularly useful?

The overwhelming advantage of BASIC is that it's extremely easy to learn
and use; the commands and functions have sensible names and their purpose is
usually obvious. Unlike C or assembly language, it is almost impossible to
overwrite the operating system or corrupt memory unless you really try, but
this doesn't limit the control a program has over the system resources. For
example, the PEEK and POKE commands let you read or write to any memory
address, and INP and OUT will directly access hardware ports.

Another important advantage of BASIC is that it handles type conversion
automatically. That is, you can freely multiply an integer times a double
precision variable, and BASIC will automatically handle the math. String
handling is equally powerful; there are functions to assign or extract
characters anywhere in a string. Graphics is yet another powerful feature.
BASIC has built-in commands to draw circles, boxes, filled boxes, and so
forth, on all of the popular monitor types.

Why did Crescent Software focus on developing a library of programmer's
tools for BASIC?

There is no disputing the fact that programming languages have come a long
way in the past few years. Yet no matter how complete or well designed a
language is, programmers will always find they need some additional
capability or feature. Third party libraries--or toolboxes--have
traditionally filled that gap. A language like C requires external library
support, due to its inherent limitations. For example, "pure" C can neither
clear the screen nor locate the cursor, so C programmers must use toolbox
routines, often viewing them as a necessary fact of life. Likewise, Pascal
has its own limitations, and toolboxes are now equally common for that
language as well.

But perhaps the most important reason to use a third-party library is to
reduce the effort needed to achieve a completed program. No doubt
knowledgeable programmers could design a pull-down menu system or a
full-featured text editor with mouse support, but that requires an enormous
amount of time, time better spent designing the rest of the program.
Furthermore, end users are used to snappy screen displays, pop-up windows,
and all the other hallmarks of a sophisticated user interface. A good
toolbox product can provide "canned" solutions to such programming problems,
as well as enhance and extend a language.

Did the library simply emerge from a collection of utilities you built up
over time, or did you set out specifically to create the library?

Designing a set of tools requires much more effort than, say, merely writing
an interface to the various DOS and BIOS services. We would often start
designing a program only to discover that one or more special support
routines were also needed. In many cases our needs led to the development of
a custom function or subroutine that turned out to be useful in a broader
sense.

One example is a set of functions that returns the minimum and maximum of
two values. These are used extensively in many of the BASIC programs
provided in QuickPak Professional and they eliminate what would otherwise be
many IF/THEN statements. The MinInt assembler function returns the minimum
of two integer values, eliminating such code as:

IF X > MaxAllowed THEN
    Value = MaxAllowed
ELSE
    Value = X
END IF

Instead a single statement does the same thing, but much more elegantly:

Value = MinInt(X, MaxAllowed)

We made many other custom functions to assist the QEdit editor that also
resulted in meaningful additions to the package.

In designing QuickPak Professional we had several very clear goals in mind.
We wanted to provide ready-made solutions to common programming problems.
These fall into two general categories: routines that are difficult for most
programmers to create themselves, and services BASIC just cannot do
directly.

An example of the first category would be routines written in assembly
language, perhaps to search or sort an array very quickly. Other difficult
programs would be a text editor, a spreadsheet, and a delay timer with
microsecond resolution.

The second category is comprised of DOS and BIOS interrupts, because BASIC
cannot easily access them. You would need these to list the files on a disk,
change their date and time, or read and write a disk's volume label.

Second, we wanted these services to be very easy to use. For example, all of
the routines that search and process strings would be in both case sensitive
and insensitive versions. It was also important to limit the number of
passed parameters to the absolute minimum, and implement the routines as
functions where appropriate. Using functions rather than called subroutines
is the most natural way to extend BASIC.

Third, in some cases BASIC's error handling abilities are not powerful
enough. C or Pascal let you try opening a file and then check whether an
error occurred, but BASIC requires a special error handling subroutine set
up in advance. If an error occurs, you end up in the error
handler--often with no idea of how you got there or where in the
program to return to. So some means to test a disk drive or printer or even
bypass BASIC's file handling altogether would be a useful addition.

From the looks of all the source code and tutorials you've provided, it
would appear that you have very strong convictions about teaching BASIC.

Yes. What programmers often need is not just more language features, but an
understanding of how to use those capabilities already present. The single
most useful tool a programmer can acquire is knowledge.

Most accomplished programmers will tell you the best way to become
proficient is by studying other people's programs. Indeed, many programmers
are self-taught, learning solely from books and articles in popular computer
magazines. By using someone else's program, studying the source code and
perhaps even modifying it, a much deeper understanding results.

We want people to understand how these routines work, and be able to learn
from them. This means not only providing all of the BASIC and assembly
source, but also writing a series of tutorials explaining the underlying
concepts.

The manuals that come with Microsoft QuickBASIC 4 are excellent as far as
they go, but they gloss over a number of important topics; for example
passing TYPE variables and arrays to subprograms and functions. Instead, the
examples that use a TYPE array skirt the issue by declaring the array as
SHARED. Likewise, the manual makes no mention of saving and loading arrays
and EGA screen images to disk.

Programmers who would like to become more proficient in BASIC must
understand these concepts. QuickPak Professional includes much of this
information, along with tutorials on accessing files, storing string and
numeric data in a program's code segment, and a comparison of the various
ways procedures are designed.

One of the nice things about BASIC is that it relieves the programmer of the
need to know about the nuts and bolts of the operating system. Unfortunately
this also results in some limitations. Can you provide an example where
you've removed some of these limitations while keeping BASIC's ease of use?

BASIC has many built-in commands, but it is admittedly lacking in the
ability to access DOS and BIOS interrupts. Microsoft QuickBASIC 2 added a
form of the CALL INTERRUPT feature, though only as an add-on available by
linking with a special object module. Further, CALL INTERRUPT is clumsy to
implement, and its use requires a knowledge of DOS services that many BASIC
programmers do not possess.

Rather than simply provide a "watered down" replacement for CALL INTERRUPT,
we felt it was important that the BASIC programmer not have to understand
how DOS and BIOS services are accessed.

Let me give you an example. Any DOS function that accepts a file name
expects that name to be in an ASCIIZ format; this is how C strings are
stored. Unlike C, BASIC strings do not contain a zero byte to mark the end;
instead, a string descriptor is maintained for each string or string array
element. A string descriptor is a 4-byte table containing the length of the
string and the address of the actual data.

Using CALL INTERRUPT required BASIC programmers to add the zero byte
manually whenever a file name has to be passed to a DOS file service. Since
one of our highest priorities was ease of use, we instead chose to have the
file name copied to a temporary holding area; then the routine adds the
terminating zero byte before calling DOS.

An important DOS service most programs need is the ability to get a list of
file names from disk. No application worth its salt will force the operator
to remember the names of files to be loaded. Instead, a menu should list all
the files present, with some sort of "point and shoot" method provided for
selecting one.

DOS provides no direct way to get a complete list of file names in one
operation. Even if it did, the BASIC program would need to know beforehand
the number of names present so sufficient string memory could be reserved
for them. The solution we devised was to create a function that returns the
number of file names matching a given search specification. Once this is
known, a BASIC string array may be reserved for them, and a second
subroutine retrieves all of the names from DOS at once.

This brings up an interesting point. Microsoft BASIC provides two ways to
create a subroutine: subprograms accessed with the CALL keyword, and
functions invoked from a BASIC expression. You seem to rely heavily on
functions.

Functions are indeed useful and a major and welcome addition. One can
develop a BASIC function in either BASIC or assembly language. The
instructions for implementing an assembler function in QuickBASIC can be
found in the Mixed-Language Programming Guide that comes with Microsoft
Macro Assembler Version 5 (MASM).

One major advantage of a function is the elimination of a passed parameter,
in situations where it is appropriate. If the routine that returns the
number of matching files were set up as a program it would be called
like this:

CALL FCount("*.*", Count)
PRINT Count; "files were found"

But designing the same routine as a function makes it both easier to use and
understand:

PRINT FCount("*.*");_
 "files were found"

The output of a BASIC function may be used directly within a PRINT statement
or, in the case of setting aside sufficient string elements, as part of the
DIM command:

DIM Array$(FCount("*.*"))

Using a function like this is the most natural way to extend the BASIC
language, and since one less parameter is needed, a function will be faster
than an equivalent called subroutine.

 Parameters in all Microsoft languages are passed on the stack, and it is no
secret that stack operations can be very slow. This is why functions are
especially good when used without parameters. For instance, we have included
a set of functions to return the status of the various keyboard settings.
The usual method to determine the status of, say, the Alt key is to use
BASIC's DEF SEG, PEEK, and AND commands:

DEF SEG = 0
AltKey = PEEK(&H417) AND 8
IF AltKey THEN PRINT_
    "the Alt key is depressed"

By using a dedicated function, you can replace all that code with a single
statement like:

IF AltKey THEN PRINT_
    "the Alt key is depressed"

Better still, without passed parameters only five instructions in assembler
code are necessary to implement such a function.

Assembly language surfaces again. It would appear that a knowledge of MASM
might be necessary even for the BASIC programmer. What do you think?

Many programmers who use high-level languages would like to learn assembly
language but are intimidated by what they believe will be a long and painful
process. Learning assembly language is actually not that painful, and even a
casual understanding from the perspective of a BASIC programmer is useful.
Of course, you need not understand assembly language to use an assembler
routine.

Are there any special things to know about assembly language functions?

The most obvious thing that comes to mind is that, before an assembly
language function can be used in Microsoft BASIC, the program that uses the
function must declare it. Having to declare procedures is new to BASIC, but
in this case it is not unreasonable. Or else--in the Alt key
example--Microsoft QuickBASIC, would assume that the Alt key is simply
an integer variable. By declaring it ahead of time, Microsoft QuickBASIC
knows that the Alt key is a function to be called, and the value returned in
AX holds the result.

And using assembly language speeds things up here and there.

That's an equally important reason for using assembly language. We wanted to
speed up those operations that typically are very slow in any high-level
language. Again, assembly language is the key to great performance, and
about two-thirds of the routines in QuickPak Professional are in assembler.

The majority of MS-DOS(R) language compilers, for example, do screen
printing through the operating system in order to be compatible with as many
machines as possible. Whether this is done via DOS or the BIOS, the results
are too slow. Microsoft QuickBASIC 4 partly addresses this by writing
directly to video memory, but there is still much room for improvement. Each
character must be examined to see if it is a special control character and
for each character it takes extra time just to see if the screen needs to be
scrolled. We devised several quick printing routines to accommodate multiple
video pages and to display new text without destroying the colors already on
the screen.

One particularly useful routine displays a portion of a string array,
containing it within a specified area of the screen. This greatly simplifies
the creation of, for example, a browse facility, where you can scroll up and
down through the entire array on the screen. Again, our intent was to have
these routines do as much as possible and save the programmer unnecessary
work.

Assembler routines would also give the BASIC programmer substantially faster
array processing.

This is exactly where assembly language really gives BASIC a much needed
boost, in the area of processing arrays. We have, for example, provided a
set of functions for each of Microsoft BASIC's numeric array types to return
the largest or smallest values. Using a dedicated assembler routine is
typically six or seven times faster than an equivalent function in BASIC.

An even greater improvement can be had when saving an entire string array to
disk and then reloading it later. One of the slowest operations BASIC
performs is reading data from a sequential file; it must examine every
single byte to see if it is either a carriage return that marks the end of a
string, or the CHR$(26) byte that marks the end of a file. Creating a custom
assembler routine to capture the entire file in one operation saves an
enormous amount of time.

The fastest way to save a numeric array in BASIC is to use BSAVE, as opposed
to making multiple PRINT statements to a file. The major problem with the
latter is the overhead required to convert the values from the internal
format used by the PC into the equivalent ASCII digits. Further, BSAVE takes
up less disk space.

In the case of integers, only 2 bytes are used to store the number in
memory, regardless of its value. Contrast that with up to five digits (six
if the number is negative) to store the number as ASCII digits. Worse, each
number in the file would also need either a carriage return/line feed pair,
or a delimiting comma. BSAVE instead captures a "snapshot" of any area of
memory, and sends it to disk in one operation. The complementary command is
BLOAD, which retrieves a previously BSAVE'd file.

What about string arrays?

Unlike numeric arrays, BASIC string arrays do not occupy contiguous memory
addresses. Instead, the descriptor tables are contiguous, and the actual
data could be anywhere in near memory. Therefore, before BSAVE can be used
on a string array, the data from all of the elements must be gathered up
into a single block. A pair of dedicated routines processes all of the
string elements in one operation, and copies them to an integer array and
back again. An additional routine lets the programmer retrieve an individual
string element if needed.

Numeric arrays in Microsoft QuickBASIC are not restricted to near memory.
Just being able to copy a string array out to far memory is a useful
feature. And if far memory becomes congested, it's easy to save the arrays
to disk to open additional space. Further, because we delimit the end of
each string with a carriage return/line feed pair, the file is directly
readable by any application.

You've provided BASIC users with two "new" data types. What are Very Long
Integers and Bit Arrays?

While I was working on QuickPak Professional, several interesting concepts
and routines resulted, including the development of two "new" variable
types.

One important and long-overdue feature introduced with Microsoft QuickBASIC
4 is support for long (4-byte) integers. Where appropriate, long integers
have a decided advantage over floating point numbers since you can process
them very quickly, without any rounding errors. Accountants like that.
Considered as pennies, their range of +/-$21,474,836.47 is generally
adequate--except for serious financial work.

As a solution, we devised a set of routines for adding, subtracting,
multiplying, and dividing what we call Very Long Integers. These variables
use 8 bytes apiece and let you manipulate extremely large numbers without
rounding errors. You assign a Very Long by aliasing it into a conventional
double precision variable, and BASIC isn't any the wiser.

In the opposite direction, we also created a pair of routines to manipulate
Bit Arrays. The smallest variable that BASIC provides is a 16-bit integer.
When a program needs the range of values offered by integers, using them
makes a lot of sense. But often all that is needed are simple true or false
variables, and an integer array can waste an enormous amount of memory. A
1000-element bit array occupies only 125 bytes, compared to 2000 bytes for a
conventional integer array.

There is a routine you supply that lets a BASIC program write to two
monitors at the same time. How did you accomplish that?

It isn't difficult to create a routine that changes the active
monitor, and the very act of switching monitors always clears the screen in
the process. Because there is nothing to prevent a program from writing
directly to screen memory for an inactive monitor, our solution was both
effective and easy to implement.

All of the video routines in QuickPak Professional automatically determine
the type of monitor installed when called. This is necessary for two
reasons. First, the video segment is different for color and monochrome
monitors, so the correct segment must be known before a routine can write
directly to screen memory. Second, and equally important, CGA monitors
create "snow" unless the reading and writing is synchronized to the
horizontal retrace.

In the routine that writes to any monitor, though, the caller must instead
indicate the type of monitor to write to with a code; 1 means monochrome, 2
is a CGA, and 3 means an EGA or VGA. Even though a PC can support two
monitors at once, they must be different--that is, both cannot be
color, or both monochrome. Thus the programmer would simply specify the
monitor type that is not the current one. Of course there is also a function
that reports the currently active type of monitor.

You've also given BASIC programmers a way to dump screen images regardless
of the screen mode they may be in. What can you tell us about that?

We also wanted to address graphics. A major limitation in the
GRAPHICS.EXE program that comes with DOS is that it works only in the CGA
screen modes. In addition, GRAPHICS works only with printers that are
compatible with the Epson(R)/IBM(R) command set. Since BASIC supports the
EGA, VGA, and Hercules(R) standards, it was important to provide a routine
that could print a graphics image from any of these modes.

We realized that to be truly useful a screen print routine must know not
only the Epson printer codes (which all modern printers can emulate), but
also the HP(R) LaserJet(R) codes. Since a LaserJet can print in several
sizes, the caller should be able to specify which resolution to use. The
only remaining problem was what to do with the colors.

A stunning three-dimensional pie chart displayed in sixteen colors is
useless as a printout; all you'd get is a big black circle. Clearly, the
best solution was to substitute a different hatching pattern for each of the
possible screen colors. But what complicates things considerably is the
different ways that graphics screen memory is organized.

In text modes, each successive character on the screen is contained in
successive bytes in display memory. But in CGA graphics modes the screen
memory is organized using a method known as interlacing. In an interlaced
system, every other row is in a different block of memory, which makes
accessing the memory much more difficult.

Hercules graphics also interlaces, except that it uses every fourth row. EGA
and VGA adapters use yet another system, where each color is in an entirely
different segment. This complicated both reading screen memory and
translating the colors into hatching patterns.

One of your accomplishments was the creation of a word processor, QEdit,
written in BASIC. BASIC's variable length strings must have been very useful
there.

Languages like C or Pascal that permit only fixed length strings take much
more programming to get the same results. The program either wastes a
substantial amount of memory for text lines that are shorter than the
maximum, or it must maintain a table of pointers to keep track of where in
memory each string begins. As characters and lines are inserted or deleted,
the pointers must be constantly updated.

The fact that BASIC supports variable length strings reduced our effort
considerably. BASIC does all of this very quickly and automatically, and
wordwrapping in QEdit will keep up with even the fastest typist.

[See code samples at end of article for an example of a wordwrap function in
BASIC, and an example of how to code a function to obtain screen colors]

Variable length strings need to be dynamically allocated. This must have
caused some significant problems.

One of our biggest difficulties was creating the string array processing
routines, which required additional research. In particular, we had to get a
fair amount of information about Microsoft BASIC's internal workings. As you
mentioned, since BASIC permits strings of nearly any length, they are
allocated dynamically as necessary. This complicates memory management
considerably and, not surprisingly, Microsoft considers many of those
details to be proprietary.

This became evident immediately when we started writing the routines to sort
a string array. I noted earlier that BASIC strings use a descriptor table to
tell each string's length and memory location. At first glance, it seems
that any two strings can be exchanged by just swapping their descriptor
tables. Descriptors are well documented in the Microsoft QuickBASIC manuals,
but unfortunately that isn't the entire story.

What is not mentioned is how string data is tied. It took us several days to
figure it out by trial and error.

Once we could exchange strings for sorting purposes, it was a simple matter
to insert or delete elements in an array. On a standard IBM PC, inserting a
single string at the beginning of a 2000 element array takes more than two
seconds using Microsoft QuickBASIC. Contrast that to less than a tenth of a
second for the equivalent routine written in assembler.

Some of the string functions in QuickPak must have caused similar problems.
But again it seems that by providing "functions" you've made life easier.

 String functions were indeed yet another difficult feature to
implement--and again, because of the lack of information. Besides the
advantage of eliminating a passed parameter, a function that can directly
return a string saves the programmer from having to set aside space
beforehand.

One of the routines in QuickPak Professional obtains the current directory
for a specified drive. Since a directory name can be as long as 64
characters, such a routine would first need a string of that length
allocated. When the routine is finished, the extra characters at the end
must be removed manually.

Because DOS uses a zero byte to mark the end of any strings it returns, the
program must use BASIC's INSTR function to find that byte; then it keeps
only those characters that precede it.

Dir$ = SPACE$(64)
    'set aside 64 bytes
Drive$ = "C"
    'specify which drive
CALL GetDir(Drive$, Dir$)
    'GetDir loads Dir$
Zero = INSTR(Dir$, 0)
    'find the zero byte
Dir$ = LEFT$(Dir$, Zero--1)
    'keep what's before the 0

When GetDir is designed as a string function, it is considerably easier to
use:

Drive$ = "C"
Dir$ = GetDir$(Drive$)

or

Dir$ = GetDir$("a")

As a function, GetDir locates the zero byte, and then returns a string that
is only as long as actually needed.

Let's go back and talk about QEdit again. You seem to have emulated the
Microsoft QuickBASIC 4 edit commands in QEdit. With all of the editors
already available, why did you create another?

Though there certainly are many editor programs available commercially, I
don't know of any that come with free source code, and none meant to be
added to another program. People often ask us to develop new routines, but
by far the one they request most is a text editor that can be customized and
added to their own programs. Of course, "editor" means different things to
different people, and every programmer has his or her own idea of how one
should work.

Because our customers are familiar with the Microsoft QuickBASIC 4 edit
commands, it made the most sense to emulate those. But since we provide the
editor's source code, it is easy to customize the various keystrokes that
are recognized. But deciding on the editing commands was just the beginning.

To be truly useful, a text editor must be able to accept text both as
individual lines and in the "one line per paragraph" method of many word
processors. We also wanted the operator to be able to change margins, size
the window, and scroll the text using a mouse--without the calling
program having to do any additional work. Of course, keystrokes must be
processed quickly enough to be totally transparent to the user.

QEdit contains a number of important features such as full mouse support,
block operations, and on-line help. But perhaps the most important feature
is the ability to remove features that aren't needed. After all, if someone
wants a bare bones editor with margins and wordwrap, there should be a way
to exclude the code for block operations and mouse support. We therefore
placed those portions of QEdit into separate sections, where users can
easily delete or remark them out.

One additional feature we wanted for our own use was block operations on
columns, as well as for words and lines. Most editors work only in sentence
mode, so that marking a block downward captures the entire length of the
line. In a word processor this is often sufficient. But when programming,
the ability to mark a long section and instantly change the indent level is
very useful. A column mode also simplifies copying or moving a table of
numbers.

Apparently QEdit was designed for use as a pop-up utility.

In reality, though QEdit is not meant as a standalone word processor, it
certainly could be. It was, however, designed so that users can add it as a
"pop-up" to a main BASIC program, thus showing that even sophisticated
programs can be written in BASIC.

What sort of problems did you encounter?

Because of the number of features in QEdit, it uses four separate help
screens, accessed with the F1 key. The help text is in the code segment of
the program, and a special assembler routine retrieves it. This saves nearly
2Kb of string memory. Also, since users can remove the mouse and block
operations, a variable is modified in each of those sections so the help
code will know which screens are appropriate.

Besides storing the help text outside of string memory, we used the same
technique for the cut-and-paste clipboard. Otherwise, with a fairly large
document loaded, BASIC could run out of memory when capturing the block.
Which brings up an important point. As difficult as it was to capture a
column that may span several screens, deleting or pasting it somewhere else
was even tougher. Moreover, to prevent the text block from stealing string
space, the clipboard is kept in an integer array. We had already written a
string manager to copy all or part of a string array to an integer array and
back, but capturing and pasting a column required two additional custom
assembler routines.

One routine copies elements from a string array to an integer array, but
starting at a given offset and for a specified number of characters. If a
line ends short of the column width, it pads the remainder with blanks. The
second custom routine retrieves the string portions one by one from the
integer array, so the wordwrap routine can insert the clipboard contents
back into the text.

We also needed a routine to close a portion of a screen that had been saved
earlier. Because QEdit is a pop-up, it saves the underlying screen
automatically. When the screen size changes, it complicates matters.

So you also had to develop routines for sizing windows on the screen. How
was this handled?

The user can at any time place the mouse cursor at the upper left or bottom
right corner, and move the corner to a new location. Of course, sizeable
windows are nothing new, but we wanted the window to actually change, rather
than simply show where it would be upon release of the mouse button.

With some programs, changing the window size produces an enormous amount of
flicker. In QEdit, we employed a custom routine to close only that portion
of the window actually needed to  reduce the window size.

We developed many other related routines both for QEdit and QuickPak
Professional in general. For example, one quickly scans a string array for
the number of active lines. Another contains the mouse cursor within a
specified area on the screen. Another determines the current video mode so a
program can accommodate 25, 43, or 50 lines automatically.

As you can see, an enormous amount of detail is required to implement a
full-featured word processor, regardless of the language used. Microsoft
QuickBASIC and the Microsoft BASIC 6.0 compiler are ideal for any
application that requires extensive string manipulation, but they are
especially appropriate for a word processor.

You have certainly made a strong case for BASIC as a "real" language.

I truly believe in BASIC as a serious applications language. It has made
computer programming--real programming--accessible to millions of
people. It is unfortunate that many otherwise informed programmers view
BASIC 6.0 and Microsoft QuickBASIC as unsuitable for "real" applications
simply because of the BASIC interpreter that comes with DOS. Besides
exceptional performance, the Microsoft BASIC products also support
recursion, structured code and data, and many other features needed for a
modern, professional language.

Sample Code:

DEFINT A-Z
DECLARE SUB WordWrap (X$, Wide)

CLS

A$ = "BASIC strings use a descriptor table ot tell each string's"
B$ = "length and memory location. At first glance, it seems that"
C$ = "any two strings can be exchanged by just swapping their"
D$ = "descriptor tables. Once we could exchange strings for"
E$ = "sorting purposes, it was a simple matter to insert or"
F$ = "delete elements in an array. On a standard IBM PC, inserting"
G$ = "a single string at the beginning of a 2000 element array"
H$ = "takes more thatn two seconds using Microsoft QuickBASIC."

W$ = A$ + B$ + C$ + D$ + E$ + F$ + G$ + H$
PRINT W$
PRINT
Wide = 60               'the maximum width of the display
WordWrap W$, Wide



SUB WordWrap (X$, Wide%)

  Length% = LEN(X$)           'remember the length
  Pointer% = 1                'start at the beginning of the string

  'scan a block of sixty characters backwards, looking for a blank
  'stop at the first blank, or if we reached the end of the string
   DO
     FOR X% = Pointer% + Wide% TO Pointer% STEP -1
        IF MID$(X$, X%, 1) = " " OR X% = Length% + 1 THEN
         'LOCATE , LeftMargin   'optional to tab in the left edge
          PRINT MID$(X$, Pointer%, X% - Pointer%);
         'LPRINT [TAB(LeftMargin)]; MID$(X$, Pointer%, X% - Pointer%)
          Pointer% = X% + 1
          WHILE MID$(X$, Pointer%, 1) = " "
           Pointer% = Pointer% + 1 'swallow extra blanks to next word
          WEND
          IF POS(0) > 1 THEN PRINT  'if cursor didn't wrap next line
              EXIT FOR                  'done with this block
           END IF
       NEXT
    LOOP WHILE Pointer% < Length%       'loop until done

END SUB

Sample 2:


DEFINT A-Z
DECLARE SUB GetColor (FG, BG)           'gets BASIC's current colors
DECLARE SUB SplitColor (XColor, FG, BG) '.ASM - splits a color into FG
                                        ' and BG
DECLARE FUNCTION OneColor% (FG, BG)     '.ASM - combines FG/BG into one
                                        ' color


CLS
INPUT "Enter a foreground color value (0 to 31): ", FG
INPUT "Enter a background color value (0 to 7) : ", BG
COLOR FG, BG

PRINT : PRINT "BASIC's current color settings are: ";
GetColor FG, BG
PRINT FG; "and"; BG

PRINT "That combines to the single byte value of"; OneColor%(FG, BG)
PRINT "Broken back out results in";
SplitColor OneColor%(FG, BG), NewFG, NewBG
PRINT NewFG; "and"; NewBG

COLOR 7, 0      'restore defaults before ending

'This function obtains BASIC's current colors by first saving the
'character and color in the upper left corner of the screen.  Next,
'a blank space is printed there, and SCREEN is used to see what color
'was used.  Finally, the original screen contents are restored.
'
SUB GetColor (FG%, BG%) STATIC
  V% = CSRLIN                       'save the current cursor location
  H% = POS(0)
  SaveChar% = SCREEN(1, 1)          'save the current character
  SaveColor% = SCREEN(1, 1, 1)      'and its color
  SplitColor SaveColor%, SaveFG%, SaveBG%

  LOCATE 1, 1                       'print with BASIC's current color
  PRINT " "; CHR$(29);              'back up the cursor to 1,1
  CurColor% = SCREEN(1, 1, 1)       'read the current color
  COLOR SaveFG%, SaveBG%            'restore the original color at 1,1
  PRINT CHR$(SaveChar%);            'and the character

  LOCATE V%, H%                     'put the cursor back where it was
  SplitColor CurColor%, FG%, BG%    'split the color into separate
                                    'FG and BG
  COLOR FG%, BG%                'restore BASIC's current value for it
END SUB



Organizing Data in Your C Program with Structures, Unions, and Typedefs

 Greg Comeau

Many of you have no doubt used structures, unions, and typedefs in your C
programming careers. In general, structures are easy to use, and they help
in the programming paradigms involved with the correct usage of data in
programs. They also keep code readable and maintainable. Unions and typedefs
can perform some of these duties as well, but their usage tends to become
more complicated and cryptic.

Since the mechanism of struct and union is pretty well established, I will
not spend time describing their basic syntax (the mechanism by which the
grammar of the C language dictates the way they may be written) or semantics
(the way the compiler treats them in a meaningful fashion). Instead I will
describe some of the idiosyncrasies that crop up with structures, unions,
and typedefs, providing a resource to use to avoid coding bugs, recognize
portability problems, and know more about the C language.

Recent Additions

The initial specification of C appeared publicly in the classic text The C
Programming Language by Brian W. Kernighan and Dennis M. Ritchie (herein
referred to as K&R). In that specification, structure assignments, structure
arguments, and functions that returned structures were not allowed. Present
day compilers, however, allow these activities to occur, and these features
have been formalized by the American National Standards Institute's draft
proposal for C (ANSI C) which, I understand, will have no more public review
periods and will at last become a bona fide standard sometime around March
of 1989.

Another feature that has made its way into that draft is initialization of
automatic structures. However, this is not available under all compilers,
especially those that typically come with the UNIX(R) or XENIX(R) Compiler
Development Set. For example, the structure definition in Figure 1a will not
compile under those compilers unless the static storage class is added as an
attribute as is shown in Figure 1b.

This places an extra burden  on the programmer and is due to the compiler
following the older language specification rather than some technical
constraint in compiler technology. This restriction may also force you to
fill up more of your programs' static storage space area than you'd like. If
space is at a premium, this may present problems. The alternative is to code
explicit assignments for each member somewhere in the function. In terms of
efficiency, this probably isn't going to make much of a difference since
this is what the compiler would do with the initialization of the automatic.
However, it would make your code more cluttered.

Automatic unions and arrays used to have this problem too. They have been
allowed to be initialized without constraints by the ANSI C proposal. Since
this has become common practice and has been mandated by ANSI, I would think
twice about any vendor that does not yet support this.

Figure 1:

A
1    main()
2    {
3    struct {
4        int a;
5    } s = { 1 };
6    }

B
1    main()
2    {
3    static struct {
4        int a;
5    } s = { 1 };
6    }

C
1    main()
2    {
3    struct {
4        int a;
5    } s1, s2;
6
7    if (s1 == s2)    /* syntax error */
8        printf("s1 == s2\n");
9    else
10        printf("s1 != s2\n");
11    }

Comparing Structures

If you were to try to compile the example code from Figure 1c, you'd find
that it produces a syntax error. This is because you cannot compare
structures or unions by name the way you'd perform a structure assignment
(for example, struct1 = struct2;). The only way to accomplish the comparison
is by checking each member of the structure individually.

This may sound like a silly restriction to place on a structure (like the
inability to easily assign and initialize used to be) and it is--to an
extent. However, there is a twofold reason for this. First, if the operation
were allowed, it would be expected that it should work for the inequivalency
operator (!=), for the relational operators (<, >, <=, >=), and perhaps even
for some others. This would not be 100 percent desirable since it would tend
to put the compiler through quite a few gyrations.

You may very well think "So what?" about this point in time. But I haven't
told you the entire story yet. What happens is that a data aggregate like a
structure could possibly have extraneous memory associated with it (refer to
the sizeof section below), and generating proper code would be rather
extravagant, especially to support the relational operators. Furthermore,
cross compilation, or rather cross assembly, of such code could produce
difficult to track bugs.

Member Access

Most C programmers I know have no problem understanding C's two member
access operators: the . and the ->. The dot allows for the retrieval of a
given member within a structure or union, and the arrow allows retrieval
from a member referenced by a pointer to a structure or union. However,
there seems to be much confusion about the use of other C operators with the
member access operators. The ones that seem to cause the most confusion are
the & (address of) and the * (indirection) operators. For the novice,
something like &*p->x might as well be hieroglyphics. Actually, if you type
in the sample listing in Figure 2, many of you, novice and experienced
alike, may be surprised to see exactly what this construct and derivations
of the construct do.

The use of the member access operators is mandated strictly by the operator
precedence chart. This chart (one can be found on page 137 of the
Microsoft(R) C Compiler (referred to herein as MSC) Version 5.1 Language
Reference) makes it clear that both the dot and arrow operators have the
highest precedence allowed (along with ( ) and [ ]). Please remember this.
As stated in "A Guide to Understanding Even the Most Complex C
Declarations," MSJ  (Vol. 3, No. 5), precedence is also an important
declaration consideration. Knowing what the highest precedence operators are
is one of the key facts C programmers should know (meaning have memorized)
since many things in the language depend upon it. Also, from a coding
viewpoint, it usually makes reading, writing, and maintenance a snap. I
cannot emphasize this point strongly enough.

Figure 2 should now make more sense. I'll assume that most of you are fine
until at least line 13. If we break down each of the subsequent statements,
we should get the parenthesized results found in Figure 3 (you can use it as
a guide). Therefore, since pfoo->x means to access a member x of a
structure, which is pointed to by pfoo, line 12 says to get the address of
the member x, whose structure is being accessed indirectly via pfoo. Note
that this does not mean to take the address of pfoo and treat it as a
pointer to a structure that contains a member x.

Line 14 instructs the compiler to get the contents of the member x of the
structure that pfoo points to. This works out fine since x is an int *.
However, this does not mean to treat the contents of the pfoo variable as a
struct pointer to obtain x. If it meant that, it would be equivalent to
pfoo->x, and this obviously isn't the case.

Line 16 is a bit interesting to me since it is a case of operators negating
each other. For instance, if we were to have a declaration such as int x, we
could say *&x. To take this one step at a time, this construct first takes
the address of x, which implicitly gets cast into an int *, and then obtains
the contents of the value of that address. Well, isn't that simply the same
as having just used x itself in the first place? This same form of
destructive interference happens on line 16 since it's going to get the
contents of the address of pfoo->x (and we know it's going to interpret it
in this way because of operator precedence).

Finally, although line 17 is constructed somewhat differently from line 16,
the end results are the same. However, the reason is admittedly cryptic and
requires memorization since it is not immediately intuitive. For instance,
in the code in Figure 4, one would expect &*pvar in line 10 to map into
&9999 since *pvar is 9999 (which is an error, of course, since you can't
take the address of a constant). Instead, if we look at this as though we
were reading it as the address of the contents of pvar, then since the
contents of pvar is var, its address is &var. This same logic applies to
line 17 in Figure 2.

I've included lines 15, 18, and 19 in case you decide to investigate the
example any further. It might be worth proving to yourself that you
understand the above by assuming that the declaration of the member x was
changed to char *x. For practice, given this situation, would you be able to
change the printf format specifications in Figure 2 to something more
appropriate (such as %s or %c)?

Figure 2:

1    struct s {
2    int     y;
3    int     *x;
4    };
5
6    struct s foo;
7    struct s *pfoo = &foo;
8
9    main()
10    {
11    printf("%d\n", foo.x);
12    printf("%d\n", pfoo->x);
13    printf("%d\n", &pfoo->x);
14    printf("%d\n", *pfoo->x);
15    printf("%d\n", &foo.x);
16    printf("%d\n", *&pfoo->x);
17    printf("%d\n", &*pfoo->x);
18    printf("%d\n", sizeof(int));
19    printf("%d\n", &foo);
20    }

Figure 3:

1    &(pfoo->x)
2    *(pfoo->x)
3    &(foo.x)
4    &(*(pfoo->x)) or simply pfoo->x
5    *(&(pfoo->x)) or simply pfoo->x

Figure 4:

1    main()
2    {
3    int     var = 9999;
4    int     *pvar = &var;
5
6    printf("%d\n", pvar);
7    printf("%d\n", *pvar);
8    printf("%d\n", &pvar);
9    printf("%d\n", *&pvar);
10    printf("%d\n", &*pvar);
11    printf("\n");
12    printf("%d\n", var);
13    printf("%d\n", &var);
14    printf("%d\n", *&var);
15    printf("%d\n", &*&var);
16    printf("%d\n", *&*&var);
17    }


The sizeof Structure

Another rather cryptic and nasty sort of thing to be aware of when using
structures is their size. Basically, what you see is not necessarily what
you get. But why should you care?

Let me explain by using another code sample (see Figure 5). I ran it on an
80386 computer and got the results shown. The sizeof(char) and the
sizeof(int) print 1 and 4 since this is the size of a byte and a natural
word on a 386 (under UNIX and the DOS large model). Note that things seem to
be going along smoothly while printing the storage space requirements for
chara and charab, but all of a sudden the space for huh seems to be out of
whack. This is not a bug in the compiler or in the program. There are parts
of C that have been allowed to be classified as undefined behavior and this
is one of those areas. In this circumstance, huh takes up 8 bytes instead of
5 because intb uses 4 bytes and it must be aligned on a word boundary.
Therefore, since chara only uses one byte, the compiler will insert a hole
of 3 unnamed and inaccessible bytes after chara to ensure that intb ends up
on the right boundary alignment.

Some of you may be saying, "Sure that's fine, but why not shift things
around so that you don't waste any space?" That doesn't quite work either as
can be seen when I printed out the size of huh2. It turns out to be the same
size that huh was. Think about it--could this be a coincidence because
of an inefficient or lazy compiler or is there some substance to it? The
answer must be the latter since an array of type huh2 must be able to be put
in storage in such a way that all huh2 intb variables would be word aligned.

Going one step further, this means that every structure also has an
alignment requirement, a fact that is not readily apparent. Note that I'm
not talking about structure tags here since they do not use any execution
time memory; however, they will conform to these requirements and produce
the correct sizes and offsets if interrogated.

In fairness, it is worth mentioning that not all CPUs have this type of
alignment requirement. Some enforce alignment only to even addresses, others
by multiples of 2, 4, 8, . . . , bytes. And others, even though they would
like you to use the suggested alignment, have a lax but less efficient
alignment (that is, none). Microsoft C (MSC) Version 5.1 supports this
mechanism if you find that there are too many wasted holes in your
structures and space is at a premium. You can enable this by using the /Zp
command-line switch to CL or by using the #pragma pack statement within your
code. These two techniques are explained in the Microsoft C Optimizing
Compiler Version 5.1  User's Guide in section 3.3.15.

Figure 5:

1    #define offsetof(type, identifier) (&(((type *)0)-\
     >identifier))
2
3    main()
4    {
5        struct chara {
6        char    chara;
7    };
8
9    struct charab {
10        char    chara;
11        char    charb;
12    };
13
14    struct huh {
15        char    chara;
16        int     intb;
17    };
18
19    struct huh2 {
20        int     intb;
21        char    chara;
22    };
23
24    printf("%d\n", sizeof(char));
25    printf("%d\n", sizeof(int));
26
27    printf("%d\n", sizeof(struct chara));
28    printf("%d\n", sizeof(struct charab));
29    printf("%d\n", sizeof(struct huh));
30    printf("%d\n", sizeof(struct huh2));
31
32    printf("%d\n", offsetof(struct charab, charb));
33    printf("%d\n", offsetof(struct huh, intb));
34    printf("%d\n", offsetof(struct huh2, intb));
35    }

The output for this program on an 80386 computer is:

1
4
1
2
8
8
1
4
0

Reading/Writing Structures

Very often it is necessary to write a structure to a disk file or RAM or
perhaps through a pipe or other interprocess communications facility
available under such operating systems as OS/2, XENIX, or UNIX. Because the
program "distributing" the structure may not have been compiled with the
same packing options or may not even be running on the same machine that is
responsible for processing any of the data within the structure, it is
advised that you should always create a dummy stub program that will dump
the values of the structure before assuming that the data within it must be
correct. Remember--alignment restrictions may create holes; holes
change the physical layout of the structure. You cannot make assumptions
without seeing the source code and/or compile options used.

Getting the offsetof

It is generally considered bad programming style to include hard-wired
constants within your code. And because of potential structure holes, it is
neither wise nor portable to use hard-wired constants to represent structure
member offsets. For instance, returning to Figure 5, because of the
inability to track it down easily, it would not be reasonable for you to
keep track of how many bytes into huh that intb was located. Instead you
should use the offsetof macro.

The offsetof macro is available with most recent compiler releases and can
be found in <stddef.h>. It takes two arguments: a type representing a
structure and a member of that structure. The result is the offset of the
member in bytes from the beginning of the structure. I have included one
possible implementation of the macro that will calculate a member's offset;
it should work on most machines (see the #define offsetof in Figure 5). The
basic idea behind the way the macro works is to pretend the structure begins
at address zero [hence the (type*)0] so that any reference to any member
from address zero must give its true relative offset in bytes. You are
encouraged to type in Figure 5 to experiment with the structures, especially
lines 32-34.

Forcing Type Alignment

Up to this point I have been emphasizing the use of structures. Another
popular C construct along these lines is the union construct. It is similar
to a struct in its syntactical nomenclature but different in its semantics.
Unfortunately, its function within C programs is often misunderstood.

Some of you are probably curious about my statement that unions are
misunderstood, so let me define what a union is and what a union isn't (or
rather, what it isn't supposed to be). A union is a variable that has the
ability to hold one, and only one of many different types of named objects
(that is, objects with overlapping storage) at a given point in time
regardless of the types of those objects. Just think about that for a
moment. Ramifications of this definition are as follows:

■    The purpose of a union is to allow for the reuse of a memory location
or variable.

■    The memory spaces for all members of the union begin at the same
address.

■    The sizeof (a union) = = sizeof (the union's largest member).

■    The value of only one of the union's members may be stored within the
union.

■    As a style issue, it is desirable that all the union's members are
related to a specific piece of program logic using it.

It should now be clear that unions can suffice as a method of forcing type
alignment and for redefining types. Redefinition of types is described in
the next section, and type alignment is described as follows.

Very often in systems programming an operating system resource or request
call, a device driver, or a specific hardware device (perhaps using DMA) may
require that data transferred to or from it have specific alignment
requirements. Luckily C, the de facto systems programming language, can
satisfy this requirement via unions.

For example, let's make a relatively safe assumption--that most of the
time an operating system or device is to be word aligned (which taken one
step further on most machines usually means that there should be an even
address boundary). Let's also assume that the info is to be passed into a
hardware device expecting 6 bytes of information. Simply coding something
like char info[6]; may or may not necessarily work since C doesn't guarantee
that info will begin on an even address--therefore you've got a 50
percent chance of getting it right and even worse odds for tracking down the
bug when code is changed 6 months down the road.

If you haven't guessed already, the way to ensure that info can be word
aligned is through something like:

union device_data {
int dummy; /* alignment */
char    info[6];
};

Since dummy is word aligned (why?) and the address of each member of a union
begins at the same location, info must also be word aligned! Of course, your
code never needs to be concerned about dummy again since it has served its
purpose. All very elegant, no?

Redefinitions: Abuse of Unions?

Because C does not enforce one facet of unions, namely that there may only
be one object in use at a time, this is left up to the programmer. In other
words, you are only supposed to take from a union what you put into it;
therefore, if you assign to a particular member, you are only supposed to
retrieve from that same member until you assign to another member. The
problem here is that C leaves this up to you, at least at a syntactical
level. For instance, given the declarations:



union example {
  int     i;
  double  d;
  } ex;
int     j;

probably no C compiler will prohibit you from coding:

ex.d = 999.999;
j = i;

even though you'd be guaranteed that j wouldn't hold anything valid, unless
perhaps you were interested in getting a random number. However, if you are
careful, this side effect can be turned to occasional advantage, what I'd
like to term a nonportable nicety--something I don't recommend that you
use, but if you do, I hope that you document the fact and clearly understand
what it is you are doing.

For example, I was working on a consulting project last year that required
use of the Intel(R) RMX operating system. The operating system was running
on an 80386, which is a segmented architecture machine. At one point in the
project, we needed to obtain some dynamic memory. For some rather obtuse
reason, it was decided that none of the standard C run-time routines such as
malloc would be appropriate for what we wanted to do. Instead we issued an
RMX system call in order to obtain   the memory. The problem we were faced
with then was that the call returned an entity called a token, which, every
time we called it, turned out to be the beginning of a segment. However, the
token only contained a 2-byte segment number and not the 2-byte zero offset
as well, and we needed to use the token as a character pointer.

The way we resolved the problem was by using code similar to that listed in
Figure 6a. However, given the reasons that have already been mentioned, this
technique is not guaranteed to work under all C compilers. Furthermore,
we're making the assumption that pointer = = int is valid and to make
matters worse, our ordering of the segment and offset variables are also
system-dependent since byte or word ordering is hardware-dependent. As you
can see, all in all it is not a very healthy affair.

Knowing this, many of you probably are agreeing but also noticing how easily
the conversion routine functioned. Before this looks too enticing, let's
look at the preferred method of redefining types: the cast operator. Figure
6b has a version of toktoptr that uses casts.

This may look rather ugly and the cast code is also making some nonportable
assumptions. The best thing I can say is that you must recognize that using
unions for redefinitions instead of as reusable storage may work, but the
method is simply not valid and is totally nonportable even among compilers
on the same machine. The chances are also high that it will produce
incorrect code with many of the optimizing compilers currently on the
market. The cast, although also system dependent in many ways, is generally
the less problematic method, and besides it's "legal" C.

The reason I am explaining this is not to teach you a new trick but to make
you aware of a bad coding practice and to prepare you for the unexpected if
it ever pops up while you are maintaining a piece of code. Also, if you do
decide to use this technique, you will know about the problems that can crop
up because of it.

Figure 6:

1    #define getmemory()    20 /* e.g. simulate RSX call */
2
3    char    *
4    toktoptr(token)
5    short   token;
6    {
7    union {
8        struct {
9        short offset;
10        short segment;
11        } segoff;
12        char    *ptr;
13    } convert;
14
15    convert.segoff.segment = token; /* get segment number */
16    convert.segoff.offset = 0;    /* offset is zero within the
                                           segment */
17
18    return (convert.ptr);    /* assume segment &
                    offset overlay ptr
                                         perfectly */
19    }
20
21    main()
22    {
23    int token = getmemory(); /* RSX memory call */
24    char    *p = toktoptr(token);
25
26    printf("%d\n", p);
27    }

Figure 6b:

1    #define getmemory()    20 /* e.g. simulate RSX call */
2
3    char    *
4    toktoptr(token)
5    short   token;
6    {
7    return ((char *)(((long)token) <<
            (sizeof(short) * 8)));
8    }
9
10    main()
11    {
12    int token = getmemory(); /* RSX memory call */
13    char    *p = toktoptr(token);
14
15    printf("%d\n", p);

16    }

Unions in General

A more general use of unions is for building coded records. Since a union is
capable of declaring a valid identifier, it may appear within a structure
(and vice versa). An example of this occurs in Figure 7, which illustrates a
case in which a certain record is obtained, perhaps through a communications
line, and needs to be processed. Each record has a code recordtype
signifying what shape it is to take on so that it can be directed to the
appropriate case of the switch statement that knows about it and the names
of its members.

This is a very elegant and easily maintainable feature of C. I was able to
create a data structure that did not have to use any unnecessary data space
nor did I need to resort to playing any games with pointers to multiple
structures to be able to use the correct type structure.

Note that contrary to what  was discussed in the preceding section, you can
reuse the first int of codedrecord's members without harm. This is an
extension as a special case. That is, if any initial segment of overlapping
structures within a union is the same, a write to one of the objects can be
followed by a read from another object. For instance, if the logistics
behind the construction of type1 and type2 data structures were similar, it
would be valid to store a value in a and then use c with absolutely no ill
effects because a and c are of the same type and located at the same
offsets. As a style issue it may even be better to take a, c, and e out of
union and make it a single entity in genericrecord. However, this
would depend entirely on the logical connection between these
identifiers as well as the logic behind the code that uses them.

One last point about unions: though the draft proposal of ANSI C now allows
for union initialization to occur in C code, the initialization must be
represented as a constant expression and the initialization expression can
only initialize the first member of the union. This is something to be aware
of if your compiler does a conversion to a different type and quietly
performs the assignment, perhaps by truncation. You may have to swap the
order of the variables in the union in some cases to ensure proper
initialization.

Figure 7:

1    #include <stdio.h>
2
3    union codedrecords {
4    struct {
5        int     a;
6        float   b;
7    } type1;
8    struct {
9        int     c;
10        long    d;
11    } type2;
12    struct {
13        int     e;
14        short   f;
15    } type3;
16    };
17
18    struct genericrecord {
19    int     recordtype;
20    union codedrecords cr;
21    };
22
23    struct genericrecord gr;
24
25    main()
26    {
27    /* ... */
28
29    switch(genericrecord.recordtype) {
30    case TYPE1:
31        /* code that uses genericrecord.cr.type1.? */
32        break;
33
34    case TYPE2:
35        /* code that uses genericrecord.cr.type2.? */
36        break;
37
38    case TYPE3:
39        /* code that uses genericrecord.cr.type3.? */
40        break;
41
42    default:
43        fprintf(stderr, "Invalid Record Type!!!");
44        exit (1);
45        break;
46    }
47
48    /* ... */
49    }

The typedef

Before going into some explanations and examples of typedefs and structures,
let me get two common misconceptions about typedefs and one source of
confusion out of the way. First, even though the typedef keyword is
syntactically categorized as a storage class specifier, it is a misnomer and
does not allocate any execution time storage. It is only categorized as such
for notational convenience.

Second and more pertinent to our discussion is that typedefs do not create
or define a new type, as the keyword may imply. What they do is allow the
programmer to create a new name for a base or derived type that already
exists. In other words, a typedef allows you to create a synonym for the
type.

Furthermore, the synonym is placed into the general name space (an area used
to categorize identifiers) of the compiler's symbol table. The general name
space holds function and most variable and enumeration names as well. The
end result is that typedef names are practically no different from any
others. That is to say, they serve to lock into a name, much like a struct
or union tag does. In this way, typedef names can be used to classify
identifiers declared with them.

Finally, an additional source of confusion with typedef is that you are not
allowed to place any storage class specifiers within the type name being
created during a typedef statement. This is because you are not allowed to
have more than one storage class specifier within any of your declarations.
Some would say this is a blessing in disguise since hiding storage class
information within a typedef could result in code that is harder to maintain
and write than it should be. In other words, it wouldn't be directly obvious
from the declaration of a variable based on the typedef what all its
attributes are. It's a thin line since I'm not sure what an object-oriented
programmer would say about this abstraction.

While on this subject, one more thing to remember is that while you may not
have storage class specifiers associated with the typedef name, you can use
the const and volatile type qualifiers. However, if you use incremental
typedefs, only one appearance of a given qualifier is allowed to be applied
to the previous typedef declarator. I could be wrong, but I suspect that
many compilers would have a problem issuing a proper diagnostic error about
this.

Allocating Structures

There are two common techniques for allocating a structure. They involve the
use of #define and typedef. Both can be used to achieve similar goals;
however, their syntax, and amazingly their semantics, are very different. If
possible the allocation should be performed via typedef. Let's see why.

Since K&R never elaborated upon the use of typedef, and since many of the
earlier non-AT&T(R) C compilers usually didn't support it (possibly for that
reason), until the past few years typedefs were either ignored or
misunderstood. The usual method for allocating a structure was to use the
#define directive. For instance, before the void keyword became popular on
all compilers, it was usually suggested to include

#define void int

in all programs, usually via a programmer supplied include file, say
mydefs.h, that functioned similarly to the macros currently found in
stddef.h. However, even though this worked without a hitch (try compiling
the code listed in Figure 8), it will create problems with only slightly
more complicated type specifications. Do not get off on the wrong track by
using old code as an example.

The problems appear because #define is concerned only about textual
substitutions of strings or tokens of strings. And this is fine since that
is the duty of the preprocessor. However, it doesn't know or care about C
syntax. The preprocessor is only responsible for accomplishing the text
substitutions; as long as it is fed a valid C program, it must emit a valid
C program. On the other hand, since the compiler does classify typedef
names, it can and will enforce type checking of expressions involving
typedefs.

Figure 8:

A
1    #define v int    /* don't use void in this example since
                    it's a keyword */
2
3    main()
4    {
5    int x;
6    v   y;
7
8    x = y;
9    }

B
1    typedef int v;    /* don't use void in this example since
                    it's a keyword */
2
3    main()
4    {
5    int x;
6    v   y;
7
8    x = y;
9    }

If we use some of the examples presented by K&R, whose interpretation is
more or less left as an exercise for the reader (even in the new revised
second edition), we can investigate why #define will fail. Using their
declaration:

typedef char  * String;

the equivalent #define would be:

#define MAXLINES (5)
#define String  char *

If we also use their sample invocations of String:

String p,
lineptr[MAXLINES],
alloc(int);

a first glance might not indicate any problems. However, if we follow
through the text substitutions, the line gets passed (at least as a
transparent step) to the C compiler as:

char * p, lineptr[5], alloc(int);

certainly not what was desired.

What happened is that only p was declared as a char *. And lineptr and alloc
were declared as an array of char and a function returning char,
respectively. We were trying to obtain:

char *p, *lineptr[5], *alloc(int);

Besides the #define problem, this also points out a pitfall of using a
multideclarator declaration. You were warned of this in "A Guide to
Understanding Even the Most Complex C Declarations."

As another simple case in which #define could not possibly work, consider:



typedef int * array20[20];

There's just no way to coerce it to produce:

array20  samplearray;

Finally, relating all this back to structs and unions, there are also the
type checking capabilities of the compiler that are a concern to us. For
instance, using K&R's Treenode example, an equivalent define (ignoring
Treeptr for our purposes) would be:

#define Treenode struct {\
char  *word;\
int   count;\
}

Invocations of this might appear as follows:

Treenode  tn1, tn2;
Treenode  tn3, *ptn;

However, although tn1 and tn2 are assignment and member compatible, they
have nothing to do with other declarations  that might use Treenode even if
they are pointers such as ptn. Therefore:

tn1 = tn2;

tn1.count = tn2.count;

are fine, but:

tn1 = tn3;
ptn = &tn1;

are undoubtedly syntax errors.

To make matters worse, if we use the style constraint of one declarator per
declaration:

Treenode  tn1;
Treenode  tn2;
Treenode  tn3;
Treenode  *ptn;

none of these have the ability to have anything to do with the
others--if we perform the preprocessor substitution, we get four
unnamed yet distinct structure tags. The compiler doesn't care that they
might all look the same. Every invocation of Treenode will create a new
structure.

If instead we (properly) use a typedef:

typedef struct {
char  * word;
int count;
} Treenode;

all the executable statements above are valid. The compiler only creates one
reference to the struct in the symbol table and after that everything falls
into place including type checking. All is now syntactically sound. Since a
typedef is C code, all the Treenode invocations will have the same type.

Proper Perspective

The basics of structures and unions are well defined and most programmers
are capable of using them with reasonably good results. However using them
to their full capacity and in a portable way requires a bit more knowledge
than commonly available. This also applies to typedef, which has often been
either neglected or misunderstood (typedef becomes very important under the
OS/2 and Presentation Manager programming environments--Ed.). Since a
structure is the main data construct in C (as well as in many other
languages), now that you have these facts under your belt, you should have a
better understanding of the C language. You can use it to your advantage in
a wise, efficient, and portable manner.

Skipping over any detail of a language specification is a mistake. Certainly
some things are useless and awkward, but it's disappointing to think that
many programmers avoid parts of languages simply because they are somewhat
complicated. Though C is many times a very terse language and often cryptic,
an understanding of its more subtle and complex points opens up C's unique
power.

My own experience with C,  as well as that of other programmers, clearly
shows that continuing to use it while not understanding it is not helpful.
This usually results in slower development and mistakes that will be costly.
If you stick with it and put in that extra energy to understand its advanced
syntax and especially the underlying philosophy, you will be able to tap
into its most powerful and advanced features.


Whitewater's Actor : An Introduction to Object-Oriented Programming Concepts

 Zack Urlocker

Object-oriented programming techniques are not new, but they are becoming
more popular as programmers tackle increasingly complex projects.
Object-oriented programming can help simplify the development of elaborate
programs by breaking them down into logical objects that manage their own
behavior and hide internal complexity. Windowing applications in particular
are easier to develop and maintain if object-oriented programming techniques
are used. Although object-oriented programming is best done in a pure
object-oriented language, such as Actor(R) or Smalltalk, it can also be used
in other languages.

This article provides an overview of object-oriented programming,
demonstrating how it can simplify the development of Windows programs. If
you haven't done much programming in Microsoft(R) Windows, some knowledge
about object-oriented programming can help you understand how Windows works.
Most of the sample code is taken from PC-Project, a critical path
project management program that I wrote in Actor.

PC-Project lets you model a real-world project by creating milestones
and tasks, assigning times, and determining the critical path of the
project. The critical path shows which activities, if delayed, will cause a
delay in the overall completion of the project. PC-Project also lets
you allocate resources and costs to tasks and create a Gantt time-line chart
of the project. PC-Project is available with complete source code from
various Bulletin Board systems or directly from the author. Figure 1 shows
the PC-Project application running under Windows.

What Is Object-Oriented ?

In traditional procedural languages like C or Pascal, the programmer defines
data structures and writes functions and procedures to operate on the data.
Although normally a correspondence exists between which functions operate on
which types of data, most procedural languages offer no formal support for
this correspondence; it is entirely the programmer's responsibility to
manage such an abstraction.

In an object-oriented lan-guage, both data and operations that work with
that data are combined into a single logical unit known as an object.
Dividing a program into objects encompassing both data and operations makes
the program more closely represent the logical design that is being
implemented. As a result, object-oriented programs are generally easier to
understand and maintain than procedural programs.

Object-oriented program-ming encourages the creation of abstract data types;
that is, the implementation of an object is referred to abstractly and is
encapsulated by high-level operations. Objects have a clear division between
public protocol and private imple-mentation. For example, we might have a
stack object that defines a public protocol based on the push and pop
operations. The stack may be implemented as an array with variables that
maintains the first and last positions, but this representation would be
considered private. By adhering to the public protocol, we can change the
implementation of stacks, say, to linked lists, without having to rewrite
any of our code. Figure 2 illustrates a stack object and the separation of
public protocol and private implementation.

Programming in an object-oriented language involves creating objects and
sending them commands or messages to do things. For example, we can create a
window with the caption Sample and then show it on the screen. In Actor,
this is done using the messages shown below:

/* create it */
W := defaultNew(
    Window, "Sample");
show(W, 1); /* display it */
close(W);  /* close it */

In this case Window is a predefined Actor class or type of object already in
the system. Therefore dozens of lines of code are eliminated that would
otherwise have to be written in C to accomplish the same thing without
affecting performance. Window objects have private data that manage their
location, size, caption, parent, handle, and so on. Window objects know how
to create themselves, position themselves on the screen, resize, close, and
so on, as part of their public protocol. Thus, the statement close(W); is a
message being sent to the W window to close itself.

Although it may seem that messages are the same as function calls in other
languages, they are not. The receiver of a message, which is the first
parameter after the parenthesis, determines how to respond. Different
classes of objects can respond to the same message in different ways, a
language characteristic known as polymorphism.

For example, if W were a member of the ProjWindow class that received a
close message, it would first check to make sure that the current project
was saved. If W were a member of the GanttWindow class, it would inform its
parent window that it was closing. In fact, W doesn't even need to be a
window at all. Other objects, such as files or communication channels, could
respond to a close message.

Polymorphism is achieved in Actor by defining a method, with the
corresponding message name, for the class. A method definition is similar to
a function definition in other languages. Polymorphism allows you to write
more general, reusable code, since you don't have to worry about what types
of objects you are dealing with, as long as they follow the same public
protocol. You can let the objects themselves manage the details of
implementation.

Inheritance

Objects are organized hierarchically in classes. Most object-oriented
languages include classes for things like arrays, files, strings, stacks,
and queues. In Actor there are also predefined classes for dealing with such
Windows entities as text windows, dialog boxes, and scroll bars.

We can create new subclasses that inherit all the character-istics of an
existing class. For example, in PC-Project the ProjWindow class
descends from the Window class and adds to it functionality related to
project management. Because of this inheritance capability, classes are much
more powerful than data types in other lan-guages. The advantage is that all
the generic windowing capabilities, like resizing, displaying, and dragging
work properly without your having to write or test a single line of code.
Using inheritance you focus on those parts of the program that are
application-specific instead of constantly "reinventing the window," so to
speak.

Inheritance encourages the development of small, reusable classes that
become building blocks for more sophisticated classes. This approach results
in less code to maintain and test and more rapid development from prototype
to final application.

In PC-Project I used inheritance to group the characteristics that are
common to the dialog boxes used for editing activities and for the
activities themselves. Figure 3 describes the classes in PC-Project.
Figure 4 shows how they are related in the class tree.

How does object-oriented programming work with procedural languages? Purists
maintain that object-oriented programming is possible only in a late-bound
language that has a class facility and inheritance. However, object-oriented
design techniques are applicable in many languages. For example, both Ada
and Modula-2 include facilities that allow the creation of abstract data
types. You can also add object-oriented extensions to C by using a C++
preprocessor. Any program can be designed, if not implemented, as logical
objects that encompass data and operations with a clear separation of public
protocol and private implementation.

Many programmers are pleasantly surprised to find that object-oriented
languages encourage them to use techniques they have been faking for years
in other languages. Through the rest of this article I encourage you to
consider how object-oriented techniques could be used in your current
language.

Figure 3:

ProjWindow    window that can display a PERT diagram
GanttWindow    window that can display a Gantt chart

ActivDialog    formal class of dialog box for activities
MStoneDialog    dialog box for editing Milestones
TaskDialog    dialog box for editing Tasks
PERTDialog    dialog box for editing PERTTasks

Network     generic network of nodes with a start and end
Node    generic node capable of connecting itself

Project    network that knows the critical path method
Activity    node with an earlyStart and lateFinish
Milestone    an activity that uses no time or resources
Task    an activity that has time, resources, and cost
PERTTask    Task where the time is estimated by PERT

Resource    used by a task; has a name and cost

Windows

Windows, like object-oriented languages, operates on a message-passing
paradigm. Windows is an event-driven system, meaning that programs respond
to events that the user or other programs initiate. These events correspond
to actions like pressing a key, clicking the mouse, or selecting a menu
item. Whenever an event occurs, Windows sends a message to notify the
program.

More specifically, when the user presses a key, for example, Windows sends a
WM_KEYDOWN message with the virtual key code of the key that was pressed.
The "WM" is mnemonic for "Windows message." Other Windows messages are
WM_COMMAND (indicating the user selected a menu command), WM_LBUTTONDOWN
(the user clicked the left mouse button), WM_VSCROLL (the user clicked in
the vertical scroll bar), and WM_PAINT (Win-dows wants the window to redraw
itself).

Windows messages are always sent with two parameters to convey additional
information. These are known as the word parameter, or wParam, and the long
parameter, or lParam. The wParam contains a 16-bit word value; the lParam
sometimes contains a 32-bit long pointer to other data.

If an application has multiple windows, as PC-Project does, the
Windows messages are sent to the appropriate window. For example, if you
press the F1 function key while the Gantt window has the focus, a message is
sent to that window; the main window is not informed. Making windows
responsible only for their own events simplifies application development.

Actor Objects

The relationship between Actor objects and Windows entities is similar to
the relationship between file variables and files in most high-level
languages. For example, in Pascal, you can declare a variable of type File.
To use the variable, however, you must assign it the name of an actual disk
file. The file variable is an abstraction of the physical file on disk. In
the same way, Actor objects belonging to classes like Window and Dialog are
abstractions of underlying areas of memory managed by Windows.

Although both Actor and Windows send messages, the messages are processed
separately. Unlike Windows messages, Actor messages are not queued at all
and are therefore very efficient. The main function in a Windows
application, called WinMain, normally includes a very short loop that
translates and dispatches Windows messages. The application must also define
a WndProc function that processes the messages.

From an object-oriented perspective, it is the window itself that responds
to the messages. After all, a window is not just a data structure, it is
both the data and the functionality. Thus, Actor manages the WinMain and
WndProc functions, the Windows message queue, and other low-level details.

Actor automatically translates Windows messages into equivalent Actor
messages, enabling you to process all messages in the same way. The Actor
classes Window and WindowsObject define many high-level messages that hide
the generic details of Windows programming and allow the programmer to
concentrate on application specific behavior. Figure 5 shows the flow of
messages between Windows and Actor objects.

The WM_PAINT method defined in the Actor class Window, automatically locks
down an area of memory known as a display context used for redrawing. It
then calls the Windows function BeginPaint, sends an Actor paint message,
calls the EndPaint function, and lastly frees the memory used for drawing.

The WM_PAINT method defined for class Window is shown in Figure 6. Since
this method is inherited by all descendants of the class Window, they only
need to define a higher-level paint method that knows how to redraw the
contents of the window. The Actor paint message will be sent whenever
Windows sends a WM_PAINT message.

In PC-Project, the ProjWindow class defines a paint method to redraw a
network or PERT diagram of the project. The paint method, shown in Figure 7,
includes no calls to Windows functions to manage the display context, since
this job will be handled by the WM_PAINT method of the Window class.

The paint method loops through all the nodes in the project, and if a node
is visible, it sends a draw message to the node. To determine where to draw
a node, a pos message is sent to the node to get its logical display
position. For example, the first node in a project is at the logical display
position (0,0). This position is converted into the Windows coordinate
(10,30) by sending a displayToWindow message to the window, with the logical
display position as an argument.

Notice that while it is the node's responsibility to manage its logical
location, it is the window's responsibility to determine if the node is
visible and convert the logical display location into its own Windows
coordinates. Again, the goal in object-oriented programming is to let the
objects manage their own behavior as much as possible.

Figure 6:

/* Trap MS-Window's message to paint self, a Window.
   Sends a paint(self) message with the display context.

   self is a Window or ancestor of class Window.
   wParam and lParam arguments are ignored.
   hDC, hPS, lpPS are local variables.
     hDC : handle to display context for drawing
     hPS : handle to paint struct
     lpPS : long pointer to locked down paint struct
*/
Def  WM_PAINT(self, wParam, lParam | hDC, hPS, lpPS)
{
   hPS := asHandle(paintStruct);        /* get hPS */
   lpPS := globalLock(hPS);             /* lock down mem */
   hDC := Call BeginPaint(hWnd, lpPS);  /* get hDC */
   paint(self, hDC);                    /* send paint msg */
   Call EndPaint(hWnd, lpPS);           /* done painting */
   globalUnlock(hPS);                   /* free memory */
   ^0;                                  /* ok, return 0 */
}

Figure 7:

/* Respond to MS-Windows messages to paint the window as a PERT
   (network) diagram.
   Draw each visible node in its proper position.
   Display the name and any other info required.
   Then draw the lines to connect the outputs of the node.

   self is the ProjWindow that receives the message.
   hDC, a handle to a display context, is sent as an arg.
   wPoint, x, y are local variables.
   aNode is the temporary loop variable for the do message.
*/
Def  paint(self, hDC | wPoint, x, y)
{
  do(nodes(project),
    {using(aNode)

     wPoint := displayToWindow(self, pos(aNode));
     x := x(wPoint);        /* horiz windows posn */
     y := y(wPoint);        /* vert windows posn */

    if visible(self, aNode)
      draw(aNode, self, x, y, hDC);
      drawTextInfo(self, aNode, x, y, hDC);
    endIf;

    /* always draw connections since they may be visible */

    drawConnections(self, aNode,x,y, getOutputs(aNode),hDC);
  });
}

Windows Messages

In addition to Windows sending messages, like the WM_PAINT message described
above, window objects can send messages to other windows or even to
themselves. For example, in PC-Project if the user presses the up
arrow, the window will scroll up as necessary. This is done by trapping the
WM_KEYDOWN Windows message and sending a WM_VSCROLL Windows message to the
window.

When a scroll message is sent, the wParam argument indicates the scroll
direction, defined by a constant such as SB_LINEUP or SB_LINEDOWN. (The "SB"
is mnemonic for "scroll bar.") In the case of a scroll message, the lParam
argument is ignored, so by convention we send a long zero, 0L.

To send a scroll message to a window using C, we call the sendMessage
function with a handle to the window that will receive the message, the
Windows message constant, and the wParam and lParam arguments, thus:

sendMessage(W.hWnd,
          WM_VSCROLL,
          SB_LINEUP,
          0L);  /* C */

Since Actor automatically translates Windows messages into Actor messages of
the same name, there is no need to call the sendMessage function (though you
could if you wanted to). Instead the scroll message can be sent directly to
the W window:

WM_VSCROLL(W, SB_LINEUP,
         0L);  /* Actor */

Actor will then call the sendMessage function with appropriate arguments.
Note that in Actor the internal representation of the window and its handle
are hidden; the window object is responsible for managing its data and
responding to all messages. Of course, you could define a higher-level
method, perhaps called scrollUp, that would hide the details of the
WM_VSCROLL message and SB_LINEUP constant.

Once you understand how Windows sends messages in response to user events, a
basic principle of object-oriented programming should become clear: objects
are like event-driven data structures. This similarity makes programming for
a windowing environment with an object-oriented language very natural.

A Keyboard Interface

Messages can easily be used to create a keyboard interface. Although
PC-Project uses the mouse extensively, I wanted to make sure that the
user could use the keyboard if he/she preferred to. Windows provides
automatic support for keyboard menu commands, but what about scrolling and
selecting activities? Normally these are done by clicking the mouse in a
scroll bar or clicking on an activity in the project window.

I was able to trap all key presses and simulate mouse actions in the main
window of PC-Project by defining a WM_KEYDOWN method for the
ProjWindow class. For example, if the user presses the up arrow, a
WM_VSCROLL message is sent. If he/she presses the F2 function key or Enter,
a WM_LBUTTONDOWN message is sent.

After years of conditioning with Lotus(R) 1-2-3(R), many PC users
intuitively press the slash (/) key to enter commands. Even Microsoft Excel,
a model Windows program, employs this method as an alternative to the
Windows user interface. Similarly, programs such as Microsoft Word use the
Esc key to bring up the command menu. I wanted PC-Project to
accommodate all these different user interfaces. But how?

Windows allows us to define accelerator keys that trigger WM_COMMAND
messages rather than WM_KEYDOWN messages. The accelerator table is written
as part of a resource script file and is separate from the application's
source code.

I defined the slash and Esc keys as accelerators that would send a
WM_COMMAND Windows message with the wParam argument as the constant
PW_COMMAND_MODE. The WM_COMMAND message is trapped so that when wParam has
the value PW_COMMAND_MODE, an Actor commandMode message is sent. If the keys
are defined as accelerators, rather than trapped as in the WM_KEYDOWN
method, they work in all windows of the application and there is no need to
remove the keystroke from the input buffer.

But the question remained, how to respond to the commandMode message in
order to activate the menu bar without selecting any item? To answer this
question, I used the SPY.EXE utility of the Microsoft Windows Software
Development Kit to see what messages Microsoft Excel sends when the slash
key is pressed. It sends a WM_SYSCOMMAND message with the constant F100H.

The source code in Figure 8 shows how the resources are defined as well as
how the WM_COMMAND and commandMode messages are trapped. You can use these
techniques to add a keyboard user interface to your own Windows programs in
Actor or C.

The WM_COMMAND method is written in essentially the same style as would be
used in C, by employing a lengthy case statement that determines what action
to take. Although this approach works, it is not very object-oriented. A
more typical approach in Actor would be to write a method called command
that eliminates the case statement by using a lookup table known as a
dictionary. Dictionary is a predefined Actor class that allows array-like
access to elements of a collection using arbitrary keys.

For example, we would create a dictionary called actions that used as its
key the values of the wParam argument and as values the literal message to
be sent. The pound symbol (#) is used to specify a literal symbol name. The
dictionary would be initialized as shown in Figure 9. The command method
becomes much shorter; we simply look up the message in the actions
dictionary and perform it. Compare this method, as shown in Figure 10, to
the one shown in Figure 8.

Writing the command method in this way makes it much easier for descendant
classes to modify their behavior without having to redefine the entire
method. They only have to add or change elements in the actions table. This
approach leads to much better code reusability than is possible with the
procedural approach.

Figure 8:

/* initialize the actions dictionary */

actions := new(Dictionary, 10);
actions[PW_FILE_OPEN] := #fileOpenAs;
actions[PW_FILE_NEW] := #fileNew;

                ■
                ■ <other cases omitted>
                ■

actions[PW_HELP] := #help;
actions[PW_COMMAND_MODE] := #commandMode;

Figure 9:

PROJECT.RC

; Accelerators are used to enhance the keyboard interface
; note: cursor keys are not defined as accelerators and are
; trapped in the WM_KEYDOWN for ProjWindow
;

#define VK_SLASH    191                 ; For Lotus-like commands

PC-Project ACCELERATORS
BEGIN
  VK_SLASH, PW_COMMAND_MODE, VIRTKEY
  VK_ESC, PW_COMMAND_MODE, VIRTKEY
  VK_F1, PW_HELP, VIRTKEY
  "^o", PW_FILE_OPEN
  "^n", PW_FILE_NEW
END

How to Trap WM_COMMAND and commandMode Messages

/* Handle menu events and accelerator keys identically.
   self refers to the ProjWindow that receives the message.
*/
Def  WM_COMMAND(self, wParam, lParam)
{
  select
    case wParam == PW_FILE_OPEN        /* ^O or menu */
      fileOpenAs(self);
    endCase
    case wParam == PW_FILE_NEW         /* ^N or menu */
      fileNew(self);
    endCase
                .
                .  <other cases omitted... >
                .
    case wParam == PW_HELP             /* F1 or menu */
      help(self);
    endCase
    case wParam == PW_COMMAND_MODE     /* slash or Esc */
      commandMode(self);
    endCase
  endSelect;
}
/* Enter "command mode" in response to a slash key
   accelerator.  This simulates Lotus 1-2-3 style
   commands by sending an Alt key sysCommand message. */

Def  commandMode(self)

{
  WM_SYSCOMMAND(self, 0xF100, 0L);
}


Figure 10:

Def command(self, wParam, lParam)
{
  perform(self, actions[wParam]);
}


Dialog Boxes

Dialog boxes represent a more advanced challenge. In PC-Project I
needed several types of dialog boxes to allow data to be edited. For
example, when the user clicks the mouse button in the project window, I want
to bring up a dialog box that lets the user edit the selected activity's
name, description, starting date, and so on. This is complicated slightly by
the fact that different types of activities have different data and thus
require different dialog boxes. For example, tasks have time and cost,
whereas milestones do not.

Because all the activity classes--that is, Milestones, Tasks, and
PERTTasks (tasks with an estimated time)--are logically similar, they
descend from a single class called Activity. The Activity class is known as
a formal class, since we will never create objects of that class, only
objects of the descendant classes.

By making good use of inheritance, we can minimize the amount of code that
needs to be written. A general editInfo method can be written for the
Activity class that will be inherited by Milestone, Task, and PERTTask. When
the user clicks on an activity in the project window, we determine which
activity is selected and then send it an editInfo message.

However, the editInfo method must be able to run the appropriate dialog box
for each type of activity. How do we know which type of dialog box to run?
We simply send a message to the activity. Descendants of the Activity class
that use the editInfo method should define a method called dialogClass that
returns the type of dialog to use when editing. The dialogClass method is
considered part of the public protocol for activities.

The source code to trap the mouse click and edit an activity in Figure 11
shows the WM_LBUTTONDOWN method for the ProjWindow class.

In the same way that the Milestone class descends from the formal class
Activity, the MStoneDialog class descends from the formal class ActivDialog,
which in turn descends from the Actor class Dialog. In this section I will
use higher-level Actor methods that hide some of the details of programming
dialog boxes.

Dialogs are created by sending the dialog objects a runModal message. The
runModal message requires as arguments the resource ID (a constant) and a
parent window. The layout of the dialog and its fields, known as edit
controls, are defined in the resource script file. Before a dialog actually
runs, Actor sends an initDialog message. We can trap this message to load
the edit controls with initial values. Since edit controls are objects, they
manage the details of handling keyboard input, tabbing, and so on.

By making use of inheritance I have the ActivDialog class initialize the
edit controls that it knows about and the MStoneDialog class initialize the
additional edit controls that it adds. To initialize an edit control with a
value, you send a setItemText message to the dialog and specify as arguments
a constant indicating the edit control and the value to be used. For
example, the message

setItemText(self, NAME,
     getName(activity));

will load the name of the activity being edited into the edit control named
NAME.

I wrote several access methods, such as getName, getEarlyStart, and
setValues that provide a safe way to access an activity's private data.
Therefore, if I change the representation of a Milestone, I don't need to
change code in other classes. This helps maintain a logical division in the
program. The dialog box is responsible only for editing and does not need to
be concerned with whether changes to the data require a recalculation of the
critical path; that is the responsibility of the setValues method of the
activity.

The command method defined for the ActivDialog is used to trap user events
related to the dialog. In this case, there are only two events that we're
interested in--clicking on OK or clicking on Cancel--any other
event is ignored. If the user clicks on OK, an update message is sent to the
dialog box. Although the command method is defined in the ActivDialog class,
it is inherited by the MStoneDialog, TaskDialog, and PERTDialog classes,
which define their own update method. The update method sends several other
messages, including setValues, which informs the activity that was being
edited of its new values so that it can take appropriate action. The code
required for the dialog box classes is shown in Figure 12.

The other dialog classes, like TaskDialog or PERTDialog, work in a similar
fashion, but they require less code because of inheritance. They merely have
to initialize any additional edit controls in the initDialog message and
define their own setValues method; everything else is inherited.

Figure 11:

/* Respond to a left button mouse click message. The lParam is the point in
the window where clicked. Convert the window location into a "logical"
display point and then send a displayLookup message to the project. This
message will return the activity at the location, which is considered to be
logically true, or logically false if there is no activity at that location.
If there is an activity, edit it; otherwise just beep. The variable self
refers to the ProjWindow that receives the message. The variables dPoint and
activity are local.
*/
Def  WM_LBUTTONDOWN(self, wParam, lParam | dPoint, activity)
{
  dPoint := windowToDisplay(self, lParam);    /* convert */
  activity :=displayLookup(project, dPoint); /* find it */
  if activity             /* logically true if found */
    editInfo(activity);   /* user clicked on an activity */
  else                    /* false if nothing found */
    beep();               /* user clicked on dead space */
  endif;
}

The editInfo Method for Class Activity and Descendants

/* Display and edit an activity's information. Descendants that use this
method should have a dialogClass() method that returns the type of dialog to
be used.

   self refers to the activity that received the message.
   dlg and retValue are local variables.
   ThePort is the main window used as the dialog' s parent.
*/
Def  editInfo(self | dlg, retValue)
{
  showWaitCurs();                 /* show an hour glass */
  dlg := new(dialogClass(self));  /* we know what kind */
  setEditItem(dlg, self);         /* what to edit? self! */
  retValue := run(dlg, ThePort);  /* run the dialog */
  showOldCurs();                  /* change cursor back */
  ^retValue;                      /* return the run value */
}

The dialogClass Method for Class Milestone

/* Return the appropriate Actor dialog class for editing.
*/
Def  dialogClass(self)
{
  ^MStoneDialog;
}

Figure 12:

/* This is a formal class to define behavior common to the various activity
dialog boxes in PC-Project. Descendants should define the res() method to
return the resource ID to be used, initDialog() to initialize additional
fields, and update() to update values in the activity.

   ActivDialog descends from class Dialog and inherits all
   of its methods and variables.
*/

/* Set the object being edited. */
Def  setEditItem(self, anEditItem)
{
  activity := anEditItem;
}

/* Run the dialog with the appropriate resource. */
Def  run(self, parent | retValue)
{
  ^runModal(self, res(self), parent);
}

/* Initialize all of the fields in the dialog.
   Descendants may wish to initialize additional fields or override
   this method.
*/
Def  initDialog(self, wParam, lp)
{
  setText(self, makeCaption(activity));

  setItemText(self, NAME, getName(activity));
  setItemText(self, DESC, getDesc(activity));
  setItemText(self, UES, getUserEarlyStart(activity));
  setItemText(self, ULF, getUserLateFinish(activity));
  setItemText(self, ES, asString(getEarlyStart(activity)));
  setItemText(self, EF, asString(getEarlyFinish(activity)));
  setItemText(self, LS, asString(getLateStart(activity)));
  setItemText(self, LF, asString(getLateFinish(activity)));
  setItemText(self, SLACK, asString(getSlack(activity)));
}

/* Handle the Ok and Cancel buttons. If Ok was clicked, then update
   the activity. This command method is used by descendants. They
   will define their own update method. */
Def command(self, wParam, lParam)
{
  select
    case wParam == IDOK
       update(self);
       end(self, IDOK);
    endCase
    case wParam == IDCANCEL
       end(self, IDCANCEL);
    endCase
    default
      ^1;        /* ignore it */
  endSelect;
  ^0;
}

MStoneDialog class [in black bar]

/* The MstoneDialog class descends from class ActivDialog
   and inherits all of its methods and variables.
*/

/* Return the resource ID used with this dialog box. */
Def  res(self)
{
  ^MSTONE_BOX;
}


/* Initialize additional fields in the dialog.
   Uses the ancestor's initDialog first.
*/
Def  initDialog(self, wParam, lParam)
{
  initDialog(self:ActivDialog, wParam, lParam);
  setItemText(self, INPUT, getInputNames(activity));
  setItemText(self, OUTPUT, getOutputNames(activity));
}


/* Update the activity after Ok was pressed.
   Inform the network if the name changes,
   check the connections and set the values.
*/
Def  update(self)
{
  setName(activity, getItemText(self, NAME));
  addNode(getNetwork(activity), activity);
  checkConnection(activity,
          getItemText(self, INPUT),
          getItemText(self, OUTPUT));
  setValues(self);
}


/* Set the values of the activity.  checkDate displays
   an error message if the date is illegal.
*/
Def  setValues(self | ues, ulf)
{
  ues := checkDate(getItemText(self, UES));
  ulf := checkDate(getItemText(self, ULF));
  setValues(activity, getItemText(self, NAME),
      getItemText(self, DESC), ues, ulf);
}


Worth the Effort

I hope that this "under-the-hood" discussion of PC-Project has helped
illustrate the concepts of object-oriented programming and how Windows
works. Sometimes it seems like a lot of work to program for Windows, but the
end result, a program with a graphical user interface, consistent commands,
and device independence makes it worthwhile.

Although it is difficult to make use of polymorphism and inheritance in
procedural languages, I encourage you to use object-oriented design
techniques no matter what language you are using. By designing objects that
encompass both data and their operations and by clearly separating public
protocol and private implementation, you will be able to write more
understandable and maintainable code.



Sidebar

Actor, designed and created by The Whitewater Group, is an object-oriented
language that allows you to create standalone Microsoft Windows
applications. Since Actor is an interactive environment that runs under
Windows, you can type statements in the workspace window and get immediate
feedback. Windows, menus, and dialogs can be created and modified directly
in the development environment. Actor has a source-level debugger and an
inspector that lets you debug programs interactively.

Code is written in a browser, a special editor that lets you create new
classes and compiles methods as you write them. The compiler translates
Actor statements into a low-level format used at run time. The browser also
shows the hierarchy of classes in the system. Figure A shows a screen shot
of a browser in the Actor development environment.

Actor is a pure object-oriented language. That means that everything in the
system is an object and all operations are performed by sending messages to
objects. Even an expression like 5 * X is a message to X to multiply itself
by 5. In hybrid languages such as C++, some things are objects that respond
to messages and others are not.

Actor's syntax is a combination of Pascal and C. For example, the := symbol
is used for assignment, whereas curly braces, { }, denote a block. Comments
appear within C-style delimiters, /* and */. Actor includes an if/else
statement, a case/select statement, while loops, and so on.

Method definitions begin with the keyword Def, followed by the name of the
method and parameters within parentheses. The first parameter in a method
definition is the receiver of the message and is always called self. The
vertical bar, |, is used to separate arguments from temporary local
variables. No type definitions are necessary, since Actor determines the
class of an object at run time. The sample method definition shown in Figure
B has two arguments, resource and parent, and one local variable, retValue.

The caret, ^, is used to return a value. If no value is explicitly returned,
the receiver of the message, self, is returned.

This very brief view of Actor indicates that it has a relatively standard
procedural language syntax. It differs from procedural languages in the way
it is programmed. Programming in Actor can be summarized as the process of
creating objects and sending messages to the objects. Messages provide the
objects with the information necessary to execute a method. In a sense, the
message is matched to a method already defined within the object. Actor is
dedicated to the goal of creating high-level applications that hide complex
underlying issues.

For more information about Actor, contact:

The Whitewater Group

Technology Innovation Center

906 University Place

Evanston, IL 60201

(312) 491-2370


MDI: An Emerging Standard for Manipulating Document Windows

 Kevin P. Welch

The Multiple Document Interface (referred to herein as MDI) is a user
interface style developed for Microsoft(R) Windows and OS/2 Presentation
Manager (referred to herein as PM) that supports the viewing of multiple
child windows within a main application. Each of these smaller child windows
can be used to display different sets of data or multiple views of the same
set of data.

This article describes MDI, focusing on the user interface as well as
programming aspects of the standard. In the process, it describes a Windows
library (MDI.LIB), which will let you easily incorporate the MDI interface
into your own applications. The use of this library will then be
demonstrated within the context of a simple application (COLORS.EXE).
Finally, the MDI standard will be contrasted and compared to the IBM(R)
Systems Application Architecture (SAA) Common User Access (CUA) guidelines
for user interfaces.

Background & Motivation

Windows and PM developers have long been fascinated with applications that
contain windows within windows. This interest stems from both the natural
capabilities of the host environments and the obvious original influence of
the Apple(R) Macintosh(R). MDI can therefore be considered an outgrowth of
programmers' interest and experience as they have attempted to create a
Macintosh-like environment inside Windows. This work, after considerable
refinement in the Windows environment, is now being applied to OS/2 PM.

To a degree, the development of MDI satisfied some creative instincts
expressed by developers while also providing Macintosh-like functionality
for IBM-compatible personal computers. In addition, its specification is
facilitating the development of an entirely new class of interoperable
applications that sport similar user interfaces on the Macintosh, Windows,
and OS/2 PM.

From this beginning, the MDI specification was refined and extended
(primarily by Microsoft) in a determined effort to make it reasonably CUA
conformant. The result of this effort was the formal definition of MDI in
the Microsoft Windows Software Development Kit (SDK) followed by its
implementation in Microsoft Excel--the first major application to use
the new specification.

Following the lead of Microsoft Excel, many software developers have tried
using multiple child windows in their applications, but unfortunately only a
few have succeeded in fully implementing the original specification. This
failure is due in part to the fact that MDI is reasonably difficult to
implement correctly since it requires a good low-level understanding of the
underlying environment. Furthermore, implementing MDI in Windows involves a
number of subtle tricks, which in the best circumstances might be considered
poor programming practice.

Despite its difficulties, in recent months the acceptance of MDI has been
further solidified with its incorporation into the new (although in my
opinion not very well designed) OS/2 file system. This coupled with the also
new but more consistent PM programming model should further enhance its
appeal to both developers and publishers alike.

Definition & Specification

From the start, it must be clearly understood that MDI is a user interface
style; that is, it is not a set of absolute rules but a collection of a few
underdefined guidelines. Although many software developers have implemented
MDI within the general style guidelines, few have implemented it in exactly
the same way.

The first thing to understand about MDI is its main, or top-level, desktop
window. This window is almost always resizable, with a title bar equipped
with a standard system menu and minimize/maximize icons. In most
circumstances, the title bar, or caption, of the main window contains only
the name of the application (more on this later). As with most windows, the
standard system menu is provided with the following options and
accelerators:

Restore    Alt-F5
Move    Alt-F7
Size    Alt-F8
Minimize    Alt-F9
Maximize    Alt-F10
Close    Alt-F4

The main window is also used to display each of the application menus
belonging to its associated child windows. The application menus vary
according to the type of document the active child window contains. For
example, when the user moves from a child window that contains a chart
document to one that contains a spreadsheet document, the main application
menu changes to reflect the capabilities of the active child window. Some
menu items can remain active regardless of which child window is selected
(for example, various file or formatting commands that have equivalent
meaning in different contexts).

In addition, the main menu provides a Window management pull-down menu.
Commands on the pull-down menu are applicable regardless of the child window
selected, allowing the user to manage the main window and all its children.
The first part of the pull-down menu has commands that allow the user to
manage the size, position, and visibility of each of the child windows. The
next part has a list of all the currently visible (including iconic) child
windows.

The New command lets the user create a new view into the currently selected
document. That is, the user creates a new child window that contains the
currently active document. Although this might not always be appropriate, it
is useful in situations in which the user wishes to view a different portion
of the same document. It could be used, for example, to support multiple
enlargements of a drawing in a paint program: one window could contain the
entire image and another a detailed view.

Since screen "real estate" is quite limited, most MDI implementations
incorporate some mechanism to arrange the various child windows. Here,
following the New command, are two commands that help manage the visual
arrangement of the child windows.

The Tile command "tiles" each of the active child windows inside the parent
client area. Although many effective tiling algorithms can be devised, most
assign some sort of priority to the currently active child window and place
it in the largest space available. Note that in most implementations the
Tile command is a one-time event--any subsequent movement of the child
windows will destroy the tiling.

The Tile Always command is an extension of the Tile command as it forces all
of the child windows to remain continuously tiled. When the size of one
child window is adjusted, the relative sizes of the other children are
changed to compensate. Any change to the parent window or to one of the
children automatically causes a tiling to occur around it (much as it did
with Windows Version 1.03). Although this technique has not been used in any
major Windows or PM applications, it has many merits that warrant serious
consideration.

Frequently users end up with many documents open simultaneously, resulting
in a cluttered desktop. Following the tiling commands are therefore two
commands that enable the user to hide or show one or more child windows. The
Hide command lets the user hide the currently active child window. One
popular variation on this theme is to let the user simultaneously hide one
or more child windows using a dialog box that contains a multiple selection
list box. This makes it possible for the user to clear a portion of the
desktop in one fluid motion.

When windows are hidden, they remain active but cannot be accessed. By using
the Unhide command, the user can select one or more windows (from a list of
all hidden windows) and make them visible again. The windows can be restored
to their original size and location or, if tiling is active, merged into the
desktop.

The last group of items on the Window pull-down menu is a list of all
currently active child windows. The windows are listed by title, with each
title preceded by a digit that serves as a short mnemonic. This facilitates
quick and consistent keyboard access to each child window regardless of the
current title. If a currently active child window exists, it is indicated by
a check mark beside its title.

In some applications, certain commands may only be applicable to the main
window. When this is the case, the main window may be listed at the
beginning of the window list. This allows the user to access the commands
that are supported by the main window quite easily. Applications that do not
need this feature can omit the main window from the window list.

Child Windows

The next thing to understand about MDI--after its main desktop
window--is its associated child windows. Like the desktop, each child
window is resizable and contains a title bar. The title bar normally has the
name of the document being edited. If a single document is being viewed by
more than one child window, a number is appended after the document name;
for example,

CHART.XLC:1
CHART.XLC:2
CHART.XLC:3

Only one child window can be active at a time, and it is distinguished from
the others by a change in the color or pattern of the title bar (usually
with the same mechanisms used to differentiate the main desktop from other
top-level windows). Note that the main desktop window remains active when
one of the child windows is enabled. To a certain extent, this appears to be
a visual contradiction since the input focus seems to be simultaneously
shared between two windows, to say nothing of the programmatic hoops you
have to jump through to accomplish this sleight of hand.

The active MDI child window also contains a control or system menu box.
Although similar to the one maintained by the parent window, it is activated
by the Alt-Minus key combination (I'm not completely sure of the rationale
behind this). The commands on the child system menu are identical to those
on the main window, except that the Alt key is replaced by the Ctrl key. The
end result is a system menu that contains the following options and
accelerators:

Restore    Ctrl-F5
Move    Ctrl-F7
Size    Ctrl-F8
Minimize    Ctrl-F9
Maximize    Ctrl-F10
Close    Ctrl-F4

It is left up to the application to disable or gray any of these commands
that are inappropriate. In most implementations, only the Minimize command
is disabled.

The Move and Size commands (accessed by Ctrl-F7 and Ctrl-F8, respectively)
allow the location and size of the child window to be controlled. These
functions mirror the ones available on the desktop but are restricted to
keep the child window inside the parent. Like most operations, the movement
and resizing of the child windows can also be accomplished using the mouse.

An interesting item to note is how the child window frame is handled when
moved. Although the mouse is clipped to the client area of the desktop, in
Windows the frame can extend outside the parent window boundary. In PM
implementations of MDI, the frame is clipped by the system to the client
area of the desktop.

The Minimize command (Ctrl-F9), seldom implemented in Windows, reduces the
child window to an icon inside the MDI desktop. The resulting icon can then
be selected, moved around the desktop to a new location, and restored to its
original size and location. As is the case with all visible child windows in
the MDI desktop, the icon can be hidden or selected using the Window
pull-down menu. Note that throughout this process the icon must remain
inside the client area of the desktop window and cannot be moved elsewhere
on the display. Although this is relatively easy to accomplish in OS/2 PM,
it adds a whole new level of complexity to a Windows implementation of MDI
(and so is seldom implemented). In PM, however, it is considerably easier to
implement this feature, and I expect that more applications will take
advantage of it when implementing MDI. Refer to the WITHIN sample
application in the MS(R) OS/2 Software Development Kit Version 1.1 (OS/2
SDK) if you're curious.

The Maximize command (Ctrl-F10) causes the child window to be enlarged to
fill the entire client area of the desktop window. As a shortcut, you can
use the mouse and click inside the maximize icon or double-click anywhere in
the title bar.

Since the client area of the child window fills the main window, you can
consider that the title bar of the child "slides under" the menu bar of the
desktop window. When this happens, two other changes occur. The first
regards the main window caption. Originally, it contains only the
application name. But when a child window is maximized, the title bar of the
desktop is changed to include the name of the currently active document,
much as is done in normal, non-MDI applications. The second, and perhaps
even more complicated change, involves the movement of the child window's
system menu to the beginning of the application menu. This allows continued
access to the child system menu, letting you close or restore the window to
its original size.

If you think things are complicated enough, consider what happens when a new
MDI child is created or an existing one is selected while another is
maximized. The MDI specification dictates that when a different child window
is selected or a new one created, it automatically assumes the
characteristics of the previously selected window. This implies that if you
create a new child window while in a maximized state, the new window will
also be displayed in a maximized state. Similarly, when you close a
maximized child window, the MDI desktop automatically selects the next
available child window and maximizes it for you--whether you wanted it
to or not.

The Restore command (Ctrl-F5), as you might expect, causes the maximized
child window (or minimized if implemented) to be restored to its original
size and location among the other windows, much as it does with top-level
system menus.

Finally, the Close command (Ctrl-F4) destroys the currently selected child
window. In situations in which the child window is one of several views into
a common document, the title bars of the remaining windows are automatically
renumbered to reflect the change. If the child window being closed is the
last one accessing a document, a dialog box is normally displayed to confirm
any required save operations. As is the case with all system menus,
double-clicking the mouse inside the system menu box is a shortcut for
choosing the Close command.

Another item to note about all these commands is that they apply only to the
child window that is currently active. This means only that the window has a
system menu and minimize/maximize menu boxes. Unfortunately, the original
MDI specification does not make this terribly clear. The end result is that
each of the child windows is responsible for changing its visible attributes
when it receives and loses the input focus.

If that isn't enough, the MDI specification also calls for a number of
keyboard accelerators to move between the various child windows:

F6    select next active document subdivision, clockwise

Shift-F6    select next active document subdivision, counterclockwise

Ctrl-F6    select next active document, from front to back

Shift-Ctrl-F6    select next active document, from back to front

Despite the fact that the keyboard accelerators are not listed on the child
system menu, they represent the only mechanism for moving between child
windows without a mouse. It is left up to users to remember (assuming they
read their manuals) what they are and how they work. Furthermore, the task
of implementing these accelerators is yet another activity that must be
managed by the already overburdened MDI desktop window.

Design Issues

Before I get into the actual programming issues involved with implementing
MDI, it seems appropriate to discuss the design issues that are bound to
come up when working with the specification. Perhaps foremost is the
additional complexity associated with using MDI. If you don't have the idea
by now, it takes a great deal of time to implement MDI and get it to work
correctly. From a design standpoint, MDI requires that each child window be
object-oriented in nature (maintaining its own instance data) yet be able to
access shared data that is held in common when multiple views are in effect.
In addition, the standard has some serious performance implications since it
introduces more support code and yet another level of hierarchy into the
system.

It is also natural to compare multiple instances of tightly coupled
applications to the MDI alternative. On the positive side, a group of
independent applications are often easier to design, implement, and test,
especially when the environment takes care of the data-handling issues for
you. This approach works especially well when using non-Windows-aware
applications or those whose hold on the environment is tenuous in the best
of circumstances.

On the negative side are the resulting cluttered display, lack of
interoperable consistency with the Macintosh, and difficulties of
well-integrated interprocess communication between separate applications.
Furthermore, since many applications require the use of multiple views into
the same document, MDI is something you will probably have to think
seriously about.

Another troublesome design area with MDI is the way that it treats menus.
Although menus are relatively simple to structure when each child window is
homogeneous in nature and shares the same or similar capabilities, the
design can be difficult when child windows are very heterogeneous or involve
compound documents.

Menu design is further complicated when the desktop is empty or all the
child windows are hidden. Most of the normal menu options will be disabled
and of no interest to the user. Many applications respond to this situation
by severely pruning their menus, adding additional menu-handling complexity.

One of the most serious drawbacks of the MDI standard is the problem that
results when the desktop window is resized. Although you can't move a child
window completely outside the client area, it is possible to have it end up
there if you change the size of the MDI parent. The end result is a window
(perhaps even an active one) that is completely invisible. Despite the fact
that you can't point to the invisible window with a mouse, you can use the
child window accelerators to get to it--but you still can't see it.

An interesting visual phenomenon can be created if you use the Ctrl-Minus
key combination while the active child window is outside the client area. As
you would expect, the child's system menu appears but the child window
remains hidden. The result is that the child's system menu appears
unconnected to the desktop, magically making itself visible with no apparent
connection to anything else. Interesting, maybe, but certainly somewhat
confusing for users.

MDI API

By now you may be wondering whether MDI is something you want to take
on--especially since it doesn't really do anything except manage a
collection of related child windows. But there is a good reason to use
it--the definition and implementation of an Application Program
Interface (API) that manages the MDI. The end result of the API is a small
library of object modules (approximately 16Kb in total size) that performs
all the work of integrating MDI into your application for you. And best of
all, it's no extra charge with the price of your MSJ subscription.

In order to accomplish the task of integrating the API into an application I
enlisted the help of friend, long-time associate, and Windows
guru--Geoffrey Nicholls. Together we came up with an API that lets you
write complex MDI applications as if they were standalone, independent
applications.

We recognized from the start that developing such an API would be quite
dirty (doing things we would never do in conventional Windows programming)
and that we would really have to try to keep it small and simple. We also
realized that we would not be able to implement the entire specification,
only some of the more important facets--the rest we would leave to you.
Finally, we wanted to make it as object-oriented as possible. After several
false starts and rewrites we ended up accomplishing our goals but in so
doing perhaps used property lists and window offsets to excess.

The MDI API we came up with in a sense represents an analog of the existing
Windows API. From previous experience, we knew that to a large degree the
operating characteristics of a window are defined by the default window
message processing function. The MDI API attempts to change this foundation
and give each window a new and different set of characteristics. The net
result is a small number of routines with familiar parameter lists that can
be used together to give your application MDI characteristics.

MdiMainCreateWindow()    Main window creation function
MdiMainDefWindowProc()    Main MDI window default window function
MdiChildCreateWindow()    MDI child window creation function
MdiChildDefWindowProc()    MDI child window default window function
MdiGetMessage()    MDI application message retrieval function
MdiTranslateAccelerators()    MDI application translate accelerator function
MdiGetMenu()    MDI menu retrieval function
MdiSetAccel()    MDI set document window accelerator table function

Message Flow and Process Sequencing

In the next two sections we will examine the inner workings of the MDI API.
We first describe the general message flow and processing sequence used by
the API. Then we describe each of the top-level functions and explain some
of the subtle ways in which they work. As you read these sections, refer to
the MDI source code listings accompanying this article. The code is
reasonably well documented, so you should be able to understand it if you
have a good background in Windows programming.

Now, perhaps the most efficient way to learn about the MDI API is to study
the MDI message flow diagram carefully. It tracks the path of each message
received by an application that uses the API, focusing on those that are of
particular interest.

The first thing to notice is the rather normal message retrieval,
translation, and dispatch loop at the top of the diagram. This occurs much
as it would in any other Windows application, the only difference being in a
specific check for menu-related messages (performed inside the MdiGetMessage
function). When such messages are encountered, they are immediately
dispatched to a window function in order to activate the various system and
application menus correctly.

After the message-handling loop, each message is dispatched to an
appropriate window function. As far as MDI is concerned, there are only two
types of windows present--a desktop or parent window and child or
document windows. On the left side of the diagram, the flow of events for
the desktop window is listed; on the right side, a similar flow for each of
the document windows is listed.

Tracing down the left, or desktop side, each message is processed by the
main application window function, then passed on to the default MDI main
window function. The remainder of events listed below occur inside this
default function, finally ending in most messages being sent on to the
standard DefWindowProc.

As you can see from the diagram, the default MDI main window function is
primarily interested in activation, initialization, and command-related
messages. All other messages are sent on without modification to the
DefWindowProc. Of those intercepted, some result in a particular action
being performed (like the activation of a particular child window); others
are processed and sent directly to an appropriate child
window--bypassing the default window function.

On the right, or document side of the diagram is the sequence of events that
occur when messages are received by the default MDI child window function.
As is the case with the left side, the flow of events listed occur inside
this default function, ending with most of the messages being sent on to the
standard DefWindowProc.

Like the default desktop window function, the child window function is
primarily interested in activation, initialization, and command-related
messages. Of particular importance are the various system commands. Some are
handled directly and not passed to the system. In certain cases, such as
those that involve activation of a menu or a new document window, the
message is sent directly to the desktop.

In a normal Windows application, the activation of a child window occurs
with little fanfare, but in an MDI implementation a number of important
steps must be performed. The first is changing the title bar color. Although
Windows allows only one window to have the input focus, when a child window
is selected it seems that both the desktop and the child are simultaneously
active. This is done by manually sending a WM_NCPAINT message (with
appropriate parameters) to the DefWindowProc of the window being activated.

Furthermore, under the interface specification, only the currently active
document window contains an MDI system menu and maximize/minimize icons.
Because of this, the second step is to change the style of the window to
include these new attributes and then force the system to display the
changes.

The next major task to perform when a document window is activated involves
replacing the desktop menu. Each child window is associated with its own
menu. When it is activated, the current desktop menu is replaced and the new
one inserted, retaining all the attributes that it previously had. Finally,
just before the input focus is transferred to the document, the Window
pull-down menu has to be updated to reflect the current status of the
desktop.

Like Figure 12, Figures 13 and 14 list the sequence of events that
transpires either when a document window is maximized or a different
document window is selected while in maximized state. Of these two
sequences, the only somewhat technical task is the automatic insertion of
the MDI system menu at the beginning of the application menu. This involves
retrieving the MDI menu icon from Windows with the following call:

hBitmap = LoadBitmap(
      NULL,
      MAKEINTRESOURCE(
      OBM_CLOSE) );

The resulting bitmap contains both the standard system menu icon and the MDI
one. After retrieving the bitmap dimensions (via a GetObject call), you can
extract the MDI system menu bitmap for use when updating the application
menu. This entire sequence of events (never before publicly documented) is
performed by the MdiCreateChildSysBitmap function, should you have the
inclination to see how it is actually accomplished.

Top-Level Functions

Now that I've discussed the message flow and process sequencing of MDI, I
will focus on the top-level function calls provided by the API.
Programmatically speaking, if you understand these functions, you will be
able to use the MDI interface in your own applications without a great deal
of difficulty.

The first of these calls is MdiMainCreateWindow, which is responsible for
the creation of the main desktop window and all the associated MDI property
lists required to make the interface work. The actual data structures used
by the API are maintained with property lists attached to the desktop and
document windows. A property list is an attempt to give each window access
to some sort of instance data (to borrow an object-oriented programming
term). This is accomplished by associating a window handle with a named
block of memory. Using the interface provided by Windows, any window can
set, enumerate, retrieve, and destroy properties. Although there is no
predefined limit to the number of properties that a window can have, the
property list itself, like other window-related data, is actually allocated
in the local heap of the user library. The MdiMainCreateWindow function also
attaches the Window pull-down menu to the main application menu, making the
MDI interface almost transparent to the desktop.

The second API call is MdiMainDefWindowProc. As described in the MDI message
flow diagram, this function is responsible for the default processing of all
desktop-related messages. More specifically, it is interested in messages
that involve the activation and deactivation of the desktop, menu-related
messages, and messages relating to the visibility and sizing of the
associated document windows. Implemented as a large switch statement, it
passes most of the messages on to the DefWindowProc.

The next API call is the MdiChildCreateWindow function. This function,
identical in many ways to the standard Windows CreateWindow call, creates a
new child window inside the desktop. Behind the scenes it sets up the
related property lists, keeps track of the window menu and accelerator
table, and activates the child window correctly, depending on the current
state of the desktop.

As with the desktop window, each document window is associated with a
default MDI message-processing function, MdiChildDefWindowProc. This
function is responsible for the default handling of all document window
related messages, especially those that involve system menu choices and
window creation, activation, and destruction. Those messages not handled are
either sent directly to the desktop window or are passed on to the
DefWindowProc.

After the default child window function is a replacement for the standard
Windows message function, MdiGetMessage. This function is responsible for
retrieving all of the application messages from the system queue. It also
checks for keyboard menu access and activates the correct menu when cursor
keys are used while a pull-down menu is visible.

Following this is the MdiTranslateAccelerators function, which is
responsible for translating each message according to the currently active
accelerator table. Though most Windows applications have only one
accelerator table, MDI applications can have several--one for the main
desktop and one for each type of document window. This function
automatically checks the state of the application and uses the appropriate
accelerator table.

Finally, there are two utility functions contained in the
MDI API--MdiGetMenu and MdiSetAccel--that are implemented as
macros. These two functions are required since most applications need to
define an accelerator table and access the current menu. The current menu
handle is retrieved by the MdiGetMenu macro which returns it to the document
window. The MdiSetAccel macro attaches an accelerator table to the property
list of the document window. The accelerator table can then be used
automatically when messages are translated and dispatched throughout the
application.

Taken together, these eight functions represent the entire MDI API. Although
you cannot use these functions with impunity, they should work well even in
the most demanding applications. If carefully used, they hide most of the
subtleties of MDI and let you focus on solving customer problems, not
implementing yet another scheme for managing child windows. The only really
nasty side effect is the installation of a system keyboard message hook.
This hook intercepts cursor movement keystrokes while manipulating menus.
Without this message hook it would be very difficult to implement a truly
authentic MDI keyboard user interface.

Building MDI.LIB

In order to build the MDI API library, you will have to create these files:

MDI    Library make file
MDI.H    Library header file
MDI1.C    Main MDI API functions
MDI2.C    Activation and switching of document windows
MDI3.C    Handling of menus and keyboard user interface

In addition to these source files, you will need the Microsoft Windows
Version 2.1 SDK and the Microsoft C Optimizing Compiler Version 5.1.

The library MAKE file (MDI) will compile each of the modules in the medium
model and combine them into an object library using the LIB utility provided
with the C compiler. The resulting library is then ready for use without
modification by any medium model Windows application. If you wish, you can
change the make file compilation flags and create equivalent small, compact,
or large versions of the same library.

Using the MDI API

I will use the program COLORS.EXE to show how the MDI API is used in the
context of a simple application. I chose this program since it will clearly
demonstrate the simple and straightforward use of the MDI API. In many ways,
COLORS can be considered a collection of three different, yet related
programs. For one, although the three parts of COLORS share the same window
procedure, they act as if they were three separate applications. Using the
MDI API they are brought together into one desktop.

With the COLORS desktop, you can create a number of red, green, and blue
colored document windows. The colored windows are created by using the
New... option under the File pull-down menu. Each window is successively
numbered and, via an associated pull-down menu, can be modified to display
different intensities of color. Of the three types of document windows, the
blue one is unique in that it is also associated with an accelerator table.
By using keys 0, 1, 2, 5, and 7 you can change the intensity of the blue
background color to 0 percent, 100 percent, 25 percent, 50 percent, and 75
percent respectively. See Figure 16 for an example.

When several documents are present on the desktop, you can move from window
to window using one of three mechanisms--selecting a window with the
mouse, moving to another window using the keyboard user interface, or
pulling down the Window menu and manually selecting a different window.
Additionally, using the Window pull-down menu, you can hide the currently
active document window or possibly redisplay hidden ones in a traditional
MDI fashion.

You can build COLORS from the source code listed in Figure 17, which
includes the following files:

COLORS
COLORS.DEF
COLORS.RC
COLORS.H
COLORS.C

Each reference to the MDI API is clearly identified and highlighted in
Figure 17. The first reference to notice is in the application MAKE File.
Here, COLORS is dependent on both MDI.H and MDI.LIB. In addition, the MDI
library is referenced in the linkage command line, which allows COLORS to
use any of the public MDI API routines we previously defined.

The next MDI reference of interest is contained in COLORS.DEF, which is
where both the MdiMsgHook and MdiDlgUnhide functions are exported. They both
must be exported since they represent movable entry points used by the
system. Failure to do so will cause serious problems.

The third reference to the MDI API is in COLORS.RC. Note the inclusion of
the MDI.H header file and the definition of a number of MDI-related
resources at the end of the file. The first of these resources, the MDI
window menu, is used as a template for the Window pull-down menu. The MDI
API creates a duplicate of this menu, attaching a list of the currently
visible document windows at the end.

The next resource is the MDI child window accelerator table. This table,
which is automatically loaded by the API, is used to implement the document
window keyboard user interface. Last in the resource file is the template
for the MDI Unhide dialog box. This dialog box is displayed when the
Unhide... command is selected from the Window pull-down menu. With this
dialog box you can select a hidden document window and have it redisplayed
on the desktop. You can also change the style and characteristics of the
dialog box to suite your application, although you should be careful not to
alter the name and identifiers used.

Following the resource file is COLORS.C, which contains all of the C source
code for the COLORS application and is structured much like any other
windows program. COLORS.C (like COLORS.RC) also references MDI.H. In
addition to defining all MDI related identifiers, MDI.H needs to be included
since it defines function prototypes for each member of the MDI API.

The first MDI-related task COLORS performs is the creation of the main
desktop window using the MdiMainCreateWindow function inside MainInit. This
call creates an empty window that contains the default desk-top menu. Not
long after MdiMainCreateWindow is a call to ColorCreate, a utility function
that creates a new document window using the MdiChildCreateWindow function
and associates it with an appropriate accelerator table. In this case, the
default action is to create a single red document window.

Once the desktop has been created and initialized, the application
retrieves and processes all related messages. Like most Windows
applications, this is accomplished with a simple GetMessage loop followed by
the translation and dispatch of each message retrieved. In this case,
however, MdiGetMessage and MdiTranslateAccelerators are used in place of the
normal Windows functions.

The next reference to the MDI API occurs in the MainWndProc of COLORS.
Each message that is dispatched by the message-processing loop is sent
directly to the responsible window function. The MainWndProc handles all the
messages that relate to the desktop window. In addition, since the desktop
window is the only one that contains a menu, it also receives all
menu-related messages.

The desktop message processing function traps only the file-related commands
and passes the rest of the messages to the MdiMainDefWindowProc for
additional handling. The MdiMainDefWindowProc in turn processes the commands
in which it is interested (redirecting some to the appropriate document
window function) and passes the rest on to the system via the DefWindowProc.

Throughout this process, the main window message processing function can
receive menu commands belonging to any one of the child windows. Because of
this capability, it is recommended that each document window menu share a
common set of commands that are applicable at the desktop level. In COLORS,
these commands are all those listed under the File pull-down menu.

Note that it is only necessary to conceptually separate the desktop and
document menus, not each of the associated document menus. This is because
menu commands not intercepted by the desktop are only destined for the
currently active document window, not for those that are inactive.

The last references to the MDI API occur in the ColorWndProc
message-processing function. This function, shared among each of the colored
child windows, responds to the document menu commands and paints the window
background using the default color at the selected intensity level.
Throughout ColorWndProc, MdiGetMenu is used in place of GetMenu. This is
because the desktop window contains the menu for the document window and
isn't always the immediate parent [else GetMenu(GetParent(hWnd)) would be a
suitable alternative].

Like the desktop window function, ColorWndProc passes most of the messages
on to the default MDI message-processing function, in this case
MdiChildDefWindowProc. This function in turn processes a subset of the
messages and passes the balance on to the DefWindowProc. In certain
situations, messages are redirected to the desktop window and not sent
directly to the system.

When you build COLORS, experiment with it and see how the internal functions
respond in a variety of situations. Try and hide all the document windows or
create new ones while one is in a maximized state. In particular try out the
keyboard user interface, moving from document to document without the aid of
a mouse.

In a while you will begin to appreciate how much is going on in the
background to make the interface work consistently. Yet despite the visual
sophistication, there is the increased overhead required by the API. If you
switch rapidly between different document windows, then the additional
overhead will be readily apparent. Although in part due to the relatively
simpleminded message-handling approach of COLORS (which passes everything on
to the default window function), to a large degree it can be attributed to
the MDI API itself.

Nevertheless, keep in mind that the MDI API implemented here was designed
for clarity and readability, not for size and performance. Our internal
working version of the API (on which the published library was based)
implemented the full MDI specification considerably more efficiently than
this one does (including Window New, Tiling, and the ability to minimize
document windows). The central structure, however, remains the
same--with a little tuning and enhancement, the base API presented here
is capable of supporting world-class MDI applications with unparalleled
ease. Coupled with a little rethinking of your current data-handling
techniques, you will be able to adapt many of your existing Windows
applications to the MDI user interface easily. And, perhaps best of all,
with the MDI API you can accomplish this with few changes to your source
code.

MDI and SAA

In addition to the interoperability benefits of MDI, one of the most
significant forces behind its acceptance in the development community is
IBM's SAA. Although a comprehensive overview of SAA is outside the scope of
this article, we will briefly describe it to show how the SAA specification
influences MDI.

SAA is a set of selected software interfaces, conventions, and protocols
that serve as a common framework for application development, portability,
and use across three major computing platforms--the IBM System/370,
System/3X, and the personal computer.

A significant part of SAA is the CUA specification. This standard defines,
in a lengthy set of rules and guidelines, what SAA-compliant user interfaces
should look like and how they are to be used. The end result is a 300+ page
document (available from your local IBM representative or branch office)
that describes in laborious detail the SAA human/machine interface.

Why are SAA and CUA important? Regardless of what you think about them, the
compelling fact of the matter is that many large corporations are attempting
to settle on a common user interface that spans a variety of hardware
platforms. This drive is in part motivated by the hope that users will be
able to migrate easily from machine to machine without the customary
learning curve associated with the transition. Corporations are starting to
require vendors to provide SAA, CUA-compliant software. Microsoft has been
actively trying to capitalize on this by making Windows SAA, CUA-compliant
(hence all the unusual keyboard accelerators).

MDI, being an integral part of the Microsoft Windows strategy, fits into
this overall standard. The net effect--and this is why MDI is important
for you as a software developer--is that if you use the MDI interface
(as opposed to some other scheme) in your application, your potential users
will already be familiar with the interface and you could potentially sell
more software. At the very least, you should take a close look at the IBM
SAA, CUA specification and give it careful consideration. Personally, I
have a hard time living with the constraints CUA puts on me as a developer,
but I am willing to live with them if I can put my programs in front of a
larger customer base.

I hope this discussion has given you ideas and insights that will help you
in your own development. MDI just might be the answer to some technical
problem you are struggling with. As you consider MDI, realize that to a
large extent it has evolved from the need for an organized way of handling
multiple documents within a single desktop. This evolution has been at best
troublesome and is still somewhat at odds with the underlying environment.
Perhaps in the future something like the MDI API might be included in the
Windows or OS/2 Presentation Manager API, saving both you and me a great
deal of effort. Until that time, you have access to a little more
information than you did before.

COLORS - MAKE File

# compilation flags
CFLAGS=-AM -c -Gsw -Osal -W2 -Zp

# COLORS
colors.res: colors.rc colors.ico colors.h mdi.h
   rc -r colors.rc

colors.obj: colors.c colors.h mdi.h
   cl $(CFLAGS) colors.c

colors.exe: colors.obj colors.def mdi.lib
   link4 colors,colors/ALIGN:16,colors,mdi+mlibw+mlibcew/NOE,colors
   rc colors.res

colors.exe: colors.res
   rc colors.res



COLORS.DEF - DEF File

NAME                    COLORS
DESCRIPTION             'Multiple Document Interface'
STUB                    'WINSTUB.EXE'

CODE                    MOVEABLE DISCARDABLE PURE LOADONCALL SHARED
DATA                    MOVEABLE MULTIPLE

HEAPSIZE                2048
STACKSIZE               2048

EXPORTS
   MainWndProc          @1
   ColorWndProc         @2
   MainDlgNew           @3
   MainDlgAbout         @4
   MdiMsgHook           @5
   MdiDlgUnhide         @6

COLORS.RC - Resource File

/* COLORS.RC - Resources for COLORS program */

/* COLORS section of file */

#include <windows.h>
#include "colors.h"
#include "mdi.h"

MainIcon ICON  colors.ico

MainMenu MENU
BEGIN
   POPUP "&File"
   BEGIN
      MENUITEM "&New...",         IDM_NEW
      MENUITEM "&Open...",        IDM_OPEN, GRAYED
      MENUITEM "&Save",           IDM_SAVE, GRAYED
      MENUITEM "Save &As...",     IDM_SAVEAS, GRAYED
      MENUITEM "&Close",          IDM_CLOSE, GRAYED
      MENUITEM SEPARATOR
      MENUITEM "&Exit",           IDM_EXIT
      MENUITEM "A&bout...",       IDM_ABOUT
   END
END

RedMenu MENU
BEGIN
   POPUP "&File"
   BEGIN
      MENUITEM "&New...",         IDM_NEW
      MENUITEM "&Open...",        IDM_OPEN, GRAYED
      MENUITEM "&Save",           IDM_SAVE, GRAYED
      MENUITEM "Save &As...",     IDM_SAVEAS, GRAYED
      MENUITEM "&Close",          IDM_CLOSE
      MENUITEM SEPARATOR
      MENUITEM "&Exit",           IDM_EXIT
      MENUITEM "A&bout...",       IDM_ABOUT
   END
   POPUP "&Red"
   BEGIN
      MENUITEM "&0 %",            IDM_0
      MENUITEM "&25 %",           IDM_25
      MENUITEM "&50 %",           IDM_50
      MENUITEM "&75 %",           IDM_75
      MENUITEM "&100 %",          IDM_100, CHECKED
   END
END

GreenMenu MENU
BEGIN
   POPUP "&File"
   BEGIN
      MENUITEM "&New...",         IDM_NEW
      MENUITEM "&Open...",        IDM_OPEN, GRAYED
      MENUITEM "&Save",           IDM_SAVE, GRAYED
      MENUITEM "Save &As...",     IDM_SAVEAS, GRAYED
      MENUITEM "&Close",          IDM_CLOSE
      MENUITEM SEPARATOR
      MENUITEM "&Exit",           IDM_EXIT
      MENUITEM "A&bout...",       IDM_ABOUT
   END
   POPUP "&Green"
   BEGIN
      MENUITEM "&0 %",            IDM_0
      MENUITEM "&25 %",           IDM_25
      MENUITEM "&50 %",           IDM_50
      MENUITEM "&75 %",           IDM_75
      MENUITEM "&100 %",          IDM_100, CHECKED
   END
END

BlueMenu MENU
BEGIN
   POPUP "&File"
   BEGIN
      MENUITEM "&New...",         IDM_NEW
      MENUITEM "&Open...",        IDM_OPEN, GRAYED
      MENUITEM "&Save",           IDM_SAVE, GRAYED
      MENUITEM "Save &As...",     IDM_SAVEAS, GRAYED
      MENUITEM "&Close",          IDM_CLOSE
      MENUITEM SEPARATOR
      MENUITEM "&Exit",           IDM_EXIT
      MENUITEM "A&bout...",       IDM_ABOUT
   END
   POPUP "&Blue"
   BEGIN
      MENUITEM "&0 %\t  0",       IDM_0
      MENUITEM "&25 %\t  2",      IDM_25
      MENUITEM "&50 %\t  5",      IDM_50
      MENUITEM "&75 %\t  7",      IDM_75
      MENUITEM "&100 %\t  1",     IDM_100, CHECKED
   END
END

BlueAccel   ACCELERATORS
BEGIN
   "0",        IDM_0
   "2",        IDM_25
   "5",        IDM_50
   "7",        IDM_75
   "1",        IDM_100
END

STRINGTABLE
BEGIN
   IDS_TITLE,              "Multiple Document Interface"
   IDS_MAINCLASS,          "MdiMainClass"
   IDS_COLORCLASS,         "MdiChildClass"
END

MainNew  DIALOG   50, 50, 144, 60
STYLE WS_DLGFRAME | WS_POPUP
BEGIN
   GROUPBOX "New"                              -1,   4,  4, 100, 52
   RADIOBUTTON "&Red",                 DLGNEW_RED,   8, 16,  88, 10
   RADIOBUTTON "&Green",             DLGNEW_GREEN,   8, 28,  88, 10
   RADIOBUTTON "&Blue",               DLGNEW_BLUE,   8, 40,  88, 10
   DEFPUSHBUTTON "&OK"                       IDOK, 108,  8,  32, 14
   PUSHBUTTON "&Cancel"                  IDCANCEL, 108, 28,  32, 14
END

MainAbout   DIALOG   22, 17, 156, 100
STYLE WS_DLGFRAME | WS_POPUP
BEGIN
    CTEXT "Multiple Document Interface",        -1,  0,  8,152,  8
    CTEXT "By Geoffrey D. Nicholls",            -1,  0, 20,152,  8
    CTEXT "and Kevin P. Welch",                 -1,  0, 28,152,  8
    CTEXT "(C) Copyright 1988",                 -1,  0, 40,152,  8
    CTEXT "Eikon Systems, Inc.",                -1,  0, 48,152,  8
    CTEXT "Version 1.00  1-Nov-88",             -1,  0, 60,152,  8
    DEFPUSHBUTTON "Ok"              IDOK, 60, 80, 32, 14, WS_GROUP
END


/* MDI section of file */

MdiMenu MENU
BEGIN
   MENUITEM "&New",               IDM_NEWWINDOW, GRAYED
   MENUITEM SEPARATOR
   MENUITEM "&Tile",              IDM_ARRANGE, GRAYED
   MENUITEM "Tile &Always",       IDM_ARRANGEALL, GRAYED
   MENUITEM SEPARATOR
   MENUITEM "&Hide",              IDM_HIDE, GRAYED
   MENUITEM "&Unhide...",         IDM_UNHIDE
END

MdiChildAccel  ACCELERATORS
BEGIN
   VK_F4,      IDM_CLOSE,        VIRTKEY, NOINVERT, CONTROL
   VK_F5,      IDM_RESTORE,      VIRTKEY, NOINVERT, CONTROL
   VK_F6,      IDM_NEXTWINDOW,   VIRTKEY, NOINVERT, CONTROL
   VK_F6,      IDM_PREVWINDOW,   VIRTKEY, NOINVERT, CONTROL, SHIFT
   VK_F7,      IDM_MOVE,         VIRTKEY, NOINVERT, CONTROL
   VK_F8,      IDM_SIZE,         VIRTKEY, NOINVERT, CONTROL
   VK_F10,     IDM_MAXIMIZE,     VIRTKEY, NOINVERT, CONTROL
END

MdiUnhide   DIALOG 50, 50, 132, 68
STYLE WS_DLGFRAME | WS_POPUP
BEGIN
   LTEXT "&Unhide",                            -1,   4,  4,  88, 10
   LISTBOX                  DLGUNHIDE_LB, 4, 16, 88, 48, WS_TABSTOP
   DEFPUSHBUTTON "&OK"                       IDOK,  96,  8,  32, 14
   PUSHBUTTON "&Cancel"                  IDCANCEL,  96, 28,  32, 14
END

COLORS.H - Header File

/ * COLORS.H - Include for COLORS program */

/* Resource file constants */

/* Strings */
#define  IDS_TITLE                1
#define    IDS_MAINCLASS          2
#define    IDS_COLORCLASS         3

/* Debugging menu choice */
#define    IDM_DEBUG              0x100

/* File Menu Choices */
#define    IDM_NEW                0x101
#define    IDM_OPEN               0x102
#define    IDM_SAVE               0x103
#define    IDM_SAVEAS             0x104
#define    IDM_PRINT              0x105
#define    IDM_ABOUT              0x106
#define    IDM_EXIT               0x107

/* Color Menu Choices */
#define    IDM_0                  0x108
#define    IDM_25                 0x109
#define    IDM_50                 0x10a
#define    IDM_75                 0x10b
#define    IDM_100                0x10c

/* New dialog box */
#define    DLGNEW_RED             0x100
#define    DLGNEW_GREEN           0x101
#define    DLGNEW_BLUE            0x102


/* Window extra constants */

#define    WE_COLOR               0
#define    WE_SHADE               2
#define    WE_EXTRA               4

#define    COLOR_RED              0
#define    COLOR_GREEN            1
#define    COLOR_BLUE             2


/* Function prototypes */

int PASCAL WinMain( HANDLE, HANDLE, LPSTR, int );

HWND              MainInit( HANDLE, HANDLE, int );
long FAR PASCAL   MainWndProc( HWND, unsigned, WORD, LONG );
BOOL              ColorInit( HANDLE );
HWND              ColorCreate( HWND, int );
long FAR PASCAL   ColorWndProc( HWND, unsigned, WORD, LONG );
int FAR PASCAL    MainDlgNew( HWND, unsigned, WORD, LONG );
int FAR PASCAL    MainDlgAbout( HWND, unsigned, WORD, LONG );

COLORS.C - Source File for COLORS.EXE

/* COLORS.C - Colorful MDI Children */

#include <string.h>
#include <stdio.h>
#include <windows.h>

#include "colors.h"
#include "mdi.h"

/* Static variables */

/* Text for client area */
static char    *szShadings[5] = {"0 %", "25 %", "50 %",
                                 "75 %", "100 %"};

/* Titles of documents */
static char    *szTitles[3] = { "Red", "Green", "Blue" };

/* Count of each document (for titles) */
static int     wCounts[3] = { 0, 0, 0 };

/* Color & Shading table */
static DWORD   rgbColors[3][5] = {
                  {  /* RED */
                  RGB(255,255,255),    /*   0 % */
                  RGB(255,192,192),    /*  25 % */
                  RGB(255,128,128),    /*  50 % */
                  RGB(255,64,64),      /*  75 % */
                  RGB(255,0,0)         /* 100 % */
                  },
                  {  /* GREEN */
                  RGB(255,255,255),    /*   0 % */
                  RGB(192,255,192),    /*  25 % */
                  RGB(128,255,128),    /*  50 % */
                  RGB(64,255,64),      /*  75 % */
                  RGB(0,255,0)         /* 100 % */
                  },
                  {  /* BLUE */
                  RGB(255,255,255),    /*   0 % */
                  RGB(192,192,255),    /*  25 % */
                  RGB(128,128,255),    /*  50 % */
                  RGB(64,64,255),      /*  75 % */
                  RGB(0,0,255)         /* 100 % */
                  } };

/*  * First routine called by windows.  Calls the initialization routine
 * and contains the message loop. */

int PASCAL WinMain(
   HANDLE      hInst,
   HANDLE      hPrevInst,
   LPSTR       lpszCmdLine,
   int         nCmdShow )
{
   HWND        hwndColors;       /* Handle to our MDI desktop */
   MSG         msg;              /* Current message */

   /* Initialize things needed for this application */
   hwndColors = MainInit( hPrevInst, hInst, nCmdShow );
   if ( !hwndColors )
   {
      /* Failure to initialize */
      return NULL;
   }

   /* Process messages */
   while ( MdiGetMessage( hwndColors, &msg, NULL, NULL, NULL ) )
   {
      /* Normal message processing */
      if ( !MdiTranslateAccelerators( hwndColors, &msg ) )
      {
         TranslateMessage( &msg );
         DispatchMessage( &msg );
      }
   }

   /* Done */
   return msg.wParam;
}

/* * First, initialize the MDI desktop and the color document windows.
Second, create the MDI desktop and then create one red document. */

HWND MainInit(
   HANDLE      hPrevInst,
   HANDLE      hInst,
   int         nCmdShow )
{
   char        szTitle[80];      /* Title of our MDI desktop */
   char        szClass[80];      /* Class name of our MDI desktop */
   HWND        hwndColors;       /* Handle to our MDI desktop */
   WNDCLASS    WndClass;         /* Class structure */

   /* Window classes */
   if ( !hPrevInst )
   {
      /* Main window */
      LoadString( hInst, IDS_MAINCLASS, szClass, sizeof( szClass ) );

      /* Prepare registration */
      memset( &WndClass, 0, sizeof( WndClass ) );
      WndClass.style          = CS_HREDRAW | CS_VREDRAW;
      WndClass.lpfnWndProc    = MainWndProc;
      WndClass.hInstance      = hInst;
      WndClass.hIcon          = LoadIcon( hInst, "MainIcon" );
      WndClass.hCursor        = LoadCursor( NULL, IDC_ARROW );
      WndClass.hbrBackground  = COLOR_APPWORKSPACE + 1;
      WndClass.lpszMenuName   = "MainMenu";
      WndClass.lpszClassName  = szClass;

      /* Register main class */
      if ( !RegisterClass( &WndClass ) )
         return NULL;

      /* Allow each of the MDI children to do its own initialize */
      if ( !ColorInit(hInst) )
         return NULL;
   }

   /* Create our main overlapped window */
   LoadString( hInst, IDS_TITLE, szTitle, sizeof( szTitle ) );
   hwndColors = MdiMainCreateWindow( szClass,
      szTitle,
      WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN,
      CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
      NULL,
      NULL,
      hInst,
      NULL );

   /* Did we create successfully? */
   if ( !hwndColors )
      return NULL;

   /* Give us one red child to begin with */
   if ( !ColorCreate( hwndColors, COLOR_RED ) )
   {
      DestroyWindow( hwndColors );
      return NULL;
   }

   /* Ready */
   ShowWindow( hwndColors, nCmdShow );
   UpdateWindow( hwndColors );

   /* Done */
   return hwndColors;
}

/* Handle messages for our MDI desktop.  This includes any WM_COMMAND
messages that are received when NO document is visible on the desktop.*/

long FAR PASCAL MainWndProc(
   HWND        hwndColors,
   unsigned    message,
   WORD        wParam,
   LONG        lParam )
{
   FARPROC     lpProc;           /* Procedure instance for dialogs */
   HANDLE      hInst;            /* Current instance handle */

   switch ( message )
   {
   case WM_COMMAND:
      hInst = GetWindowWord( hwndColors, GWW_HINSTANCE );
      switch( wParam )
      {
      case IDM_NEW:
         /* New dialog box */
         lpProc = MakeProcInstance( MainDlgNew, hInst );
         switch( DialogBox( hInst, "MainNew", hwndColors, lpProc ) )
         {
         case DLGNEW_RED:
            ColorCreate( hwndColors, COLOR_RED );
            break;

         case DLGNEW_GREEN:
            ColorCreate( hwndColors, COLOR_GREEN );
            break;

         case DLGNEW_BLUE:
            ColorCreate( hwndColors, COLOR_BLUE );
            break;
         }
         FreeProcInstance( lpProc );
         break;

      case IDM_OPEN:
         break;

      case IDM_ABOUT:
         /* About dialog box */
         lpProc = MakeProcInstance( MainDlgAbout, hInst );
         DialogBox( hInst, "MainAbout", hwndColors, lpProc );
         FreeProcInstance( lpProc );
         break;

      case IDM_EXIT:
         /* Tell application to shut down */
         PostMessage( hwndColors, WM_SYSCOMMAND, SC_CLOSE, 0L );
         break;
      }
      break;

   case WM_DESTROY:
      PostQuitMessage( 0 );
      break;
   }
   return MdiMainDefWindowProc(hwndColors, message, wParam, lParam);
}

/* Register the document class. */

BOOL ColorInit(
   HANDLE      hInst )
{
   char        szClass[80];      /* Class name */
   WNDCLASS    WndClass;         /* Class structure */

   /* Get class name */
   LoadString( hInst, IDS_COLORCLASS, szClass, sizeof( szClass ) );

   /* Prepare registration */
   memset( &WndClass, 0, sizeof( WndClass ) );
   WndClass.style          = CS_HREDRAW | CS_VREDRAW;
   WndClass.lpfnWndProc    = ColorWndProc;
   WndClass.cbWndExtra     = WE_EXTRA;
   WndClass.hInstance      = hInst;
   WndClass.hCursor        = LoadCursor( NULL, IDC_ARROW );
   WndClass.hbrBackground  = GetStockObject( GRAY_BRUSH );
   WndClass.lpszClassName  = szClass;

   /* Register */
   return RegisterClass( &WndClass );
}

/* Create a document window of a given color on the MDI desktop. It loads
the appropriate menu, and accelerator table if the color is BLUE. It
initializes color and shading in the window extra words.*/

HWND ColorCreate(
   HWND        hwndParent,
   int         wType )
{
   char        szClass[80];      /* Class name for documents */
   char        szTitle[80];      /* Title for this document */
   HANDLE      hAccel = NULL;    /* Accelerator for blue doc only */
   HANDLE      hInst;            /* Current instance handle */
   HMENU       hmenuChild;       /* Handle to document's menu */
   HWND        hwndChild;        /* Handle to document */

   /* Get important info */
   hInst = GetWindowWord( hwndParent, GWW_HINSTANCE );
   LoadString( hInst, IDS_COLORCLASS, szClass, sizeof( szClass ) );
   sprintf( szTitle, "%s%d", szTitles[wType], ++wCounts[wType] );

   switch( wType )
   {
   case COLOR_RED:
      hmenuChild = LoadMenu( hInst, "RedMenu" );
      break;

   case COLOR_GREEN:
      hmenuChild = LoadMenu( hInst, "GreenMenu" );
      break;

   case COLOR_BLUE:
      hmenuChild = LoadMenu( hInst, "BlueMenu" );
      hAccel = LoadAccelerators( hInst, "BlueAccel" );
      break;
   }

   /* Create */
   hwndChild = MdiChildCreateWindow( szClass,
      szTitle,
      WS_MDICHILD,
      CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
      hwndParent,
      hmenuChild,
      hInst,
      0L );

   /* Success? */
   if ( hwndChild )
   {
      SetWindowWord( hwndChild, WE_SHADE, IDM_100 );
      SetWindowWord( hwndChild, WE_COLOR, wType );
      MdiSetAccel( hwndChild, hAccel );
   }

   return hwndChild;
}

/* Handle messages for our documents.  WM_COMMAND messages arrive at this
procedure just as if the menu were attached to this window.*/

long FAR PASCAL ColorWndProc(
   HWND        hwndChild,
   unsigned    message,
   WORD        wParam,
   LONG        lParam )
{
   char        szText[20];       /* Client area text */
   HBRUSH      hBrush;           /* Brush for filling document */
   PAINTSTRUCT Paint;            /* Paint structure */
   int         wColor;           /* Color of current document */
   int         wShade;           /* Shading of current document */

   switch ( message )
   {
   case WM_COMMAND:
      switch( wParam )
      {
      /* File menu */
      case IDM_SAVE:
      case IDM_SAVEAS:
      case IDM_PRINT:
         break;

      case IDM_CLOSE:
         PostMessage( hwndChild, WM_SYSCOMMAND, SC_CLOSE, lParam );
         break;

      case IDM_0:
      case IDM_25:
      case IDM_50:
      case IDM_75:
      case IDM_100:
         CheckMenuItem( MdiGetMenu( hwndChild ),
            SetWindowWord( hwndChild, WE_SHADE, wParam ),
            MF_UNCHECKED );
         CheckMenuItem( MdiGetMenu( hwndChild ),
            GetWindowWord( hwndChild, WE_SHADE ),
            MF_CHECKED );
         InvalidateRect( hwndChild, (LPRECT) NULL, TRUE );
         break;
      }
      break;

   case WM_ERASEBKGND:
      return TRUE;

   case WM_PAINT:
      wColor = GetWindowWord( hwndChild, WE_COLOR );
      wShade = GetWindowWord( hwndChild, WE_SHADE );
      BeginPaint( hwndChild, &Paint );
      hBrush = CreateSolidBrush( rgbColors[wColor][wShade - IDM_0]);
      FillRect( Paint.hdc, &Paint.rcPaint, hBrush );
      DeleteObject( hBrush );
      strcpy( szText, szShadings[wShade - IDM_0] );
      TextOut( Paint.hdc, 0, 0, szText, strlen( szText ) );
      EndPaint( hwndChild, &Paint );
      break;
   }
   return MdiChildDefWindowProc( hwndChild, message, wParam, lParam);
}

/* Handle the NEW dialog box.*/

int FAR PASCAL MainDlgNew(
   HWND        hDlg,
   unsigned    message,
   WORD        wParam,
   LONG        lParam )
{
   static int  iButton;          /* Keep track of radio buttons */
   int         iReturn = FALSE;  /* Return value */

   switch ( message )
   {
   case WM_INITDIALOG:
      SendMessage( hDlg, WM_COMMAND, DLGNEW_RED, 0L );
      iReturn = TRUE;
      break;

   case WM_COMMAND:
      switch( wParam )
      {
      case DLGNEW_RED:
      case DLGNEW_GREEN:
      case DLGNEW_BLUE:
         iButton = wParam;
         CheckRadioButton( hDlg, DLGNEW_RED, DLGNEW_BLUE, iButton );
         if ( HIWORD( lParam ) == BN_DOUBLECLICKED )
         {
            SendMessage( hDlg, WM_COMMAND, IDOK, 0L );
         }
         break;

      case IDOK:
         EndDialog( hDlg, iButton );
         break;

      case IDCANCEL:
         EndDialog( hDlg, 0 );
         break;
      }
      break;
   }
   return iReturn;
}

/* Handle the ABOUT dialog box.*/

int FAR PASCAL MainDlgAbout(
   HWND        hDlg,
   unsigned    message,
   WORD        wParam,
   LONG        lParam )
{
   int         iReturn = FALSE;  /* Return value */

   switch( message )
   {
   case WM_INITDIALOG:
      iReturn = TRUE;
      break;

   case WM_COMMAND:
      EndDialog( hDlg, TRUE );
      break;
   }
   return iReturn;

}


Planning and Writing a Multithreaded OS/2 Program with Microsoft C

 Richard Hale Shaw

From a programmer's perspective,  OS/2 systems are a lot like a new
programming language. In order to become fluent you have to begin learning
the idiom by writing programs with it. In this article, we'll cover the
highlights of setting up and installing the Microsoft C Version 5.1 compiler
for OS/2 development and briefly look at the header files included with it.
Then we'll take a closer look at the OS/2 Application Programming Interface
(API) with the object of writing our first OS/2 program. Last, we'll begin
to explore the world of multithreaded programming and produce a
multithreaded version of the C programmer's much beloved HELLO.C program.

Compiler Setup

The first concern for installing Microsoft C 5.1 is to ensure that you have
enough disk space. The minimum space needed for the protected mode version
of the compiler is approximately 3.5Mb (which includes two libraries). If
you install the full protected mode compiler, you'll need closer to 4.5Mb.
If you're using the compiler to produce applications for both DOS and OS/2,
you'll probably need over 6Mb. The Setup program can be instructed to
install the portions of the compiler that you want.

You must also decide which directory structure the compiler will use. We
discussed the preferred OS/2 directory structure, shown in Figure 1, in  the
first article of this series. You may recall that the \OS2\PBIN directory
contains only protected mode executables; \OS2\RBIN holds only real mode
programs; and \OS2\BIN contains bound executables, that is, programs that
can operate under DOS and OS/2.

If you install the compiler while you are running OS/2, you'll find that the
Setup program recommends that you incorporate the traditional C compiler
directories into the same structure, as shown in Figure 2. The \OS2\LIB
directory will contain all compiler libraries, \OS2\INCLUDE will contain the
header files, and \OS2\SOURCE will be used for ancillary documentation and
source files. Note that the compiler will also install an alternative set of
header files under the \OS2\INCLUDE\MT directory. These are headers for the
multithreaded version of the library, which we'll discuss later in this
article.

Microsoft C 5.1 includes facilities for producing traditional DOS programs,
OS/2 programs, and programs designed to execute under both environments
(bound applications). To this end, one of the more significant features
available when you install the compiler under OS/2 are combined libraries.

Previous versions of the C compiler already include a plethora of libraries,
including three floating-point libraries and a graphics library. When you
add to these libraries the need to keep versions of them for each memory
model used, plus libraries for OS/2 development, you will find yourself
running out of disk space fast. You can, however, combine the basic library
for each memory model with   the graphics library (if selected) and
appropriate floating-point library. Combining the libraries allows you to
keep only one library per memory model. The Installation/Setup program will
do this automatically for you and will delete the leftover component
libraries for you if you elect to do so.

Another important option is the ability to create a dual-mode or bound
compiler. This is a version of the compiler that will run under both OS/2
and DOS. The Setup program will leave a copy of BINDC.CMD (BINDC.BAT if you
installed the compiler while running DOS), which can be run to produce the
bound version. We'll discuss the production of bound applications later, but
for now keep in mind that a bound application is one that calls only the
subset of API services known as Family Application Programming Interface
(FAPI) functions. These are functions that are available under both DOS
and OS/2. Although limiting yourself to these functions will restrict you
from using OS/2 multitasking services (after all, DOS does not offer these
services), it does ensure that a program will run without recompilation
under both environments.

BINDC will leave a bound version of the compiler in the directory of your
choice when it finishes. Since it's a bound executable, it is recommended
that you have BINDC place its output in the \OS2\BIN directory (where
dual-mode programs are kept) and delete the original copies of the compiler.
You won't need them, and they'll just take up disk space. The dual-mode
version will be bigger than the original, but you'll find it simpler to use
only the one version.

The setup program produces two files: NEW-VARS.CMD and NEW-CONF.SYS. The
NEW-VARS.CMD file contains the environment variable settings that should be
in place when you run the compiler, based on the directory selections you
made during installation. You can include these either in STARTUP.CMD or in
the CMD file used for setting up your C development session through the OS/2
Program Starter [see "Using the OS/2 Environment to Develop DOS and OS/2
Applications," MSJ (Vol. 4, No. 1)]. NEW-CONF.SYS contains the additions
that ought to be added to CONFIG.SYS (CONFIG.OS2 in older, dual-boot
environments).

New Compiler Options

There are three compiler command-line options that are pertinent to OS/2
programming. The /Lr option designates the compilation of a conventional
real mode executable (the default in earlier versions of the compiler). The
/Lp option, however, signals the compiler to compile and link for the
protected mode. When you use this switch, the protected mode version of a
given library will be used. For instance, if a conventional, real mode,
small mem-ory model compilation used SLIBCE.LIB (the small model combined
library with floating-point emulation), adding the /Lp option instructs the
linker to use SLIBCEP.LIB (the protected mode version of the library).

The third option, /Fb, directs the linker to run BIND.EXE after linking is
complete. BIND is a utility that converts protected mode programs into
dual-mode applications, which can be run in either real or protected mode.
With these options in mind, and using the directory structure discussed
earlier, we can construct a generic MAKE file for which we use MAKE to build
our first OS/2 programs (shown in Figure 3).

This MAKE file, which is called HELLO (since it will be used to compile
HELLO.C), forms the basis for the MAKE files we'll use to build other
programs. It sets up the environment variables INCLUDE and LIB for the
compiler and passes a number of options to CL. Note that the MAKE file uses
both /Lp and /Fb to instruct the compiler to produce a protected mode bound
version of the program.

The /W3 option forces the compiler to use warning level 3, the most severe
warning level available from the compiler. This option is very useful to
ensure strict type checking of different objects and arguments against
OS/2-type definitions. The /Zpe option combines two options: /Zp and /Ze.
The former instructs the compiler to produce packed, or byte-aligned (as
opposed to word-aligned), structures. The latter allows extensions to C,
particularly the far, near, and pascal keywords, which OS/2 programs will
reference frequently. The /Ox option turns on maximum optimization, and the
/I option specifies the INCLUDE directory, which the compiler will use.

The /G2 option forces the production of code for the 80286, which is a handy
option, since 286 instructions are generally more efficient than pure 8086
instructions. Although this option restricts the program to run on either an
80286 or above, it's not a problem in the OS/2 environment, since OS/2 will
only run on a machine with an 80286 or 80386 (the 80386 also executes 80286
code). However, it could cause problems if the bound executable is run under
DOS on an 8088/8086.

Several additional points should be made about this MAKE file. First, note
that for debugging purposes we will want to include symbolic debugging
information with the /Zi option and turn off optimization with /Od. Thus,
I've included the additional line of options for debugging. You can enable
these options and disable the others by removing the # in front of one line
and inserting a # in front of the other line of options (# is the comment
marker in MAKE files). Also, note the use of the /NOE option that CL passes
to the linker, which prevents the linker's extended library search. This
option will ensure that the correct versions of the library functions are
linked to the program.

Figure 3:

#file: hello
#generic OS/2 MAKE file for producing 'bound' executables
#
#Currently set for making HELLO.C
#Usage: C>make hello

INCLUDE=\os2\include
LIB=\os2\lib
COPT=/Lp /Fb /W3 /Zpe /G2 /Ox /I$(INCLUDE)
#COPT=/Lp /Fb /W3 /Zpie /G2 /Od /I$(INCLUDE)

hello.exe: hello.c hello
    cl $(COPT) hello.c /link /noe

#end

OS/2 System Calls

OS/2 system services are available to application programs through the OS/2
API. Unlike the interrupt-based interface of DOS, the API makes all services
available through system calls (also known as call-gates). While the DOS
interface was limited to 256 functions (via Int 21h), there is no limit on
the number of services that could be added to OS/2 in the future. And
whereas DOS services required the use of registers to receive or return
values, OS/2 system services pass these on the stack. Thus, all OS/2
services use the same format: if a system service is capable of returning
error values, it will return a zero when successful and an unsigned nonzero
integer on error. In addition, as stated earlier, the subset of API
functions known as Family API (FAPI) functions will operate under both OS/2
and DOS, allowing you to write bound executable programs that operate in
both environments.

When you include a call to an API function in your OS/2 program, the linker
does not add the code for the function to the executable program as it would
for a DOS program. Instead, it will add instructions for loading the
function's code from the appropriate OS/2 dynamic-link library (DLL). When
the program executes, OS/2 will load the necessary DLLs (if it hasn't
already loaded them for some other application program). Thus, the functions
will be available to the program at run time.

Only one instance of an OS/2 API function will be loaded into memory, even
if more than one application program is using it. Note that for FAPI
functions in a bound executable program, the linker will include both the
DLL calling code and the real mode executable code for the function. The
former will be used when the program is operating in the OS/2 protected
mode, and the latter will invoke Int 21h when in MS-DOS real mode.

Since OS/2 loads API routines from a DLL, they are located in a different
segment from that of the calling routine and must be reached with a FAR
call. There are four general types of parameters that can be passed to an
API system service: byte (or char), word (or unsigned), double word
(unsigned long), and address (far pointer). If the service requires only the
value of the parameter, then a copy of it is passed (call by value). If the
service requires a pointer to a parameter, however, the address is passed to
the service (call by reference). Because the calls are FAR calls, addresses
passed must also be FAR. To ensure that you are passing a far address
correctly to a system service, you should declare the object as far, use a
cast, or compile with the large data model. Meticulous use of the OS/2
object definitions found in the new header files (described below) will
eliminate most problems.

You might also note that segment values that are found in far addresses are
really selectors. Although they have the same segment:offset form found in
far addresses under MS-DOS real mode, OS/2's protected mode requires a
selector value. A selector is actually an index into a table of segment
addresses and has no correspondence to a physical segment. Selectors must be
used since OS/2's virtual memory management may change the physical segment
location as it is moved around in memory or swapped to disk. The layer of
abstraction they provide allows you to address a far object without having
to know its real physical location in memory--or whether it's in memory
at all (taken care of for you by OS/2).

API functions use the Pascal calling convention. This convention specifies
that there be a fixed number of arguments to the function and that the
arguments are pushed on the stack left to right (the order in which you
specify them in your source code). In addition, Pascal calls do not require
the function being called to clean up the stack. All these requirements
result in smaller, faster function calls. This scheme is the reverse of the
traditional C calling convention, which pushes arguments right to left
(allowing a variable number of arguments); requires the caller to clean up
the stack; and generally produces slower, larger code. The OS/2 header files
discussed below specify API functions as far pascal calls, so most of the
time you will not have to take any steps beyond including the appropriate
header files in your program. The far pascal convention is defined in the
OS/2 header files as APIENTRY.

Additionally, API functions use the Microsoft(R) Windows convention of
two- or three-phrase descriptive names with both uppercase and lowercase
letters. Keyboard subsystem services are provided by functions whose names
begin with Kbd; Mouse and Video subsystem services names begin with Mou and
Vio respectively. The names of all remaining operating system services (OS/2
kernel calls) begin with Dos. Some brief examples of these include DosRead,
DosCreateThread, KbdCharIn, and VioWrtTTY.

The documentation for the OS/2 API functions can be found in the OS/2
Programmer's Reference. This manual is a part of the Microsoft OS/2
Programmer's Toolkit and the Microsoft OS/2 Software Development Kit.

Header Files

Microsoft C 5.1 provides six new header files, some or all of which need to
be included in programs that take advantage of the OS/2 API. These header
files (listed in Figure 4) provide the API declarations, function
prototypes, macros, constants, and type definitions needed by an OS/2 C
program. As a routine part of getting started programming under OS/2, you
should become intimately familiar with their contents. You'll find that most
of the type definitions and structures use a naming convention similar to
that used by the system services described earlier. Also, since many system
services will place return values in the structures defined in these header
files, OS/2 programming will be easier later if you study their contents
now.

The OS/2 header files are hierarchically nested, so that you need only
include OS2.H in your program most of the time. The remaining header files
and definitions can be included by using various combinations of control
macros (listed in Figure 5), which should be defined in your program before
including OS2.H. This is particularly helpful when you are using only a
small subset of API functions in your program, since the headers are large
and compilation will proceed more quickly if you include only what the
program requires.

OS2.H itself includes two header files. The first file, OS2DEF.H, contains
most of the commonly used definitions, typedefs, macros, constants, and
structures. The second, BSE.H, indirectly contains the base definitions for
the various OS/2 subsystems (Keyboard, Video, Mouse, and Dos), plus
error-handling macros by optionally including three additional header files:
BSEDOS.H, BSESUB.H, and BSEERR.H.

BSEDOS.H contains definitions required for using the OS/2 kernel system
service, and can be included by defining INCL_DOS before the #include for
OS2.H in your program. BSESUB.H contains all the definitions required for
using any of the OS/2 subsystems (Kbd, Mou or Vio) and is included by
defining INCL_SUB. BSEERR.H contains all error-related macros and is
included through INCL_DOSERRORS. If necessary, you can force the blanket
inclusion of all API declarations and definitions by defining INCL_BASE in
your program prior to the #include for OS2.H:

#define    INCL_BASE
#include   <os2.h>

Note that many commonly used components will be defined by default unless
you define INCL_NOCOMMON in your program. You can also include specific
kernel services components by defining the macros listed in Figure 5.

Figure 4:

OS2.H     - Always #included in your program
OS2DEF.H     - Common definitions
BSE.H     - Base definitions, #includes the following:

BSEDOS.H    - Kernel services definitions
BSESUB.H     - Kbd, Vio, Mou definitions
BSEERR.H     - Error macros

Figure 5:

Define this macro:    To include definitions/declarations for:

INCL_BASE    All services
INCL_DOS    Kernel services
INCL_SUB    Subsystem (Kbd, Vio, Mou)
INCL_DOSERRORS    Error macros
INCL_DOSPROCESS    Processes and threads calls
INCL_DOSINFOSEG    Information segment calls
INCL_DOSFILEMGR    File management calls
INCL_DOSMEMMGR    Memory management calls
INCL_DOSSEMAPHORES    Semaphore functions
INCL_DOSDATETIME    Date/Time and Timer calls
INCL_DOSMODULEMGR    Module management services
INCL_DOSNLS    National language services
INCL_DOSSIGNALS    Signal functions
INCL_DOSMONITORS    Monitor services
INCL_DOSSESMGR    Session management calls
INCL_DOSDEVICES    Device and IOPL services
INCL_DOSQUEUES    Queue functions
INCL_RESOURCES    Resource-support functions


A First OS/2 Program

Once you have successfully installed the compiler, there is nothing to keep
you from writing your first OS/2 program. Figure 6 lists the code for an
OS/2 version of Dennis Ritchie's famous HELLO.C.

At first glance, an OS/2 program such as this version of HELLO.C might
appear extremely bizarre. It certainly does not resemble the Kernighan and
Ritchie version that we've all come to know and love. Nevertheless, it
accomplishes many of the same purposes. It allows us to begin to explore the
OS/2 API; introduces us to a real use of some of the header files, defines,
and functions; and most of all, gets us started writing our first real OS/2
program.

A second glance will reveal the familiar structure so often used to write
maintainable, efficient C programs. You'll note the use of INCL_SUB and
INCL_DOS to include function prototypes for the single call to the Vio
subsystem and the single kernel call in the program. The printf call has
been replaced with a call to VioWrtTTY, which prints a string on the logical
screen used by our program (all video access in OS/2 is done through a
logical screen group). Note that this function requires both the address of
the string and the string length and that it appropriately handles the
C escape sequences \r and \n, to generate a carriage-return/line-feed
combination. Also note that all Vio calls require a zero for their last
parameter.

After printing the string, a kernel call to DosExit terminates the entire
program. The first parameter specifies whether the entire process or the
current thread should be terminated (more on this in the next section). The
second parameter is the exit code, which is passed to the parent process and
is identical to the one that is passed in exit, the traditional C
termination function.

Finally, note that this program can be compiled, linked, and bound to create
a dual-mode application. Thus, the same executable program will run,
unmodified, under either OS/2 or DOS. Note that the MAKE file for this
program can be found in Figure 3. If you haven't already compiled and run a
C program using API calls, I suggest you type in HELLO.C, compile it, and
run it. It will certainly help make OS/2's magic more real and prepare you
for our next step: the world of multithreaded programs.

Figure 6:

/* hello.c RHS 10/14/88
 *
 * 1988 OS/2 version of    K&R's hello.c
 */

#define    INCL_SUB        /* for Vio calls used */
#define    INCL_DOS        /* for DosExit call used */
#include   <stdio.h>
#include   <string.h>
#include   <os2.h>

void main(void);        /* function prototype */

void main(void)
    {
    char *hello_str = "Hello, world!\r\n";
    int    len = strlen(hello_str);

    VioWrtTTy(hello_str,len,0);    /* print the message */
    DosExit(EXIT_PROCESS,0);       /* exit the program */
    }

Multiple Threads

The single most significant difference between OS/2 and DOS are the former's
facilities for multitasking. Multitasking will dramatically increase the
efficiency of most applications that are designed to take advantage of it.
However, not only do OS/2's facilities allow for the simultaneous execution
of more than one program, but what's more they permit OS/2 to execute
different parts of the same program at the same time.

The OS/2 multitasking model, shown in Figure 7, is built of threads,
processes, and screen groups. A thread is the smallest unit of execution, a
piece of code dispatched by the system. Threads are organized into a
process, or the portion of a program that controls the ownership of
resources, such as files, memory, and threads. A process is composed of at
least one thread and may consist of as many as 255 separate threads. A
process's main thread is the one in which execution begins. Note that though
a process may use a thread to manage a resource, a thread by itself does not
own any resources. A thread inherits the environment (open files, and so on)
of which it is a part and shares the same code and data segments as its
parent process. Collectively, the processes that share the same logical
keyboard and screen are a part of the same screen group.

The model allows for multitasking at all three levels (screen group,
process, and thread). In this article, however, we're primarily concerned
with the simultaneous execution of threads within the same process. Programs
that execute in such a manner are called multithreaded programs.

Multiple Thread Execution

OS/2 lets concurrently executing threads share a single
computer's CPU through the use of a preemptive, priority-based task
scheduler (later editions of OS/2 will take advantage of multiple-CPU
architectures). In reality, only one thread at a time is executing, but the
CPU's attention turns so quickly from one thread to another that the threads
appear to be executing at the same time--as long as the applications
that use multiple threads do not abuse system resources and CPU time in
their multithreaded code.

The task scheduler controls which thread gets slices of CPU time and how
much time is doled out to the thread. A thread's priority controls its
access to the CPU (if it has a higher priority, it will get more CPU time
relative to other threads). Thus, a previously idle, higher priority thread
that is ready to run can preempt CPU time from a lower priority thread that
is currently executing. On the other hand, a lower priority thread must wait
until all higher priority threads are idle before the scheduler will give it
CPU time.

The OS/2 task scheduler employs three categories of priorities for
scheduling tasks. The highest priority is time-critical, which should be
used by tasks that must respond to some type of regularly occurring event
(like a communications stream or keyboard input). The second category,
regular, is the default priority for a new thread and should be used by most
normal threads. The last category, idle, is for threads that should execute
when there are no higher priority tasks that are ready or able to execute
(such as a print spooler). Note that there are 32 levels of priorities in
each category and that the default priority for foreground processes is
regular, level 0, whereas processes that run in the background are also
regular but have a lower priority level. In general, foreground tasks are
given a higher priority than background tasks.

The task scheduler always runs the highest priority thread that is capable
of executing. If two or more threads with the same priority are ready to
run, the scheduler will evenly grant them CPU time on a round-robin basis.
If a thread is blocked--that is, it is waiting until some event
occurs--OS/2 will suspend it and run another thread. Note that the
TIMESLICE= statement in CONFIG.SYS controls the minimum and the maximum time
slice values used by OS/2.

As mentioned above, a thread is blocked when it is waiting for an event to
occur. A thread is running when it is being given CPU time slices. If a
thread is no longer blocked but hasn't yet been given CPU time slices, then
it is said to be ready to run.

Planning a Multithreaded Program

As mentioned above, a program or process can have more than one thread of
execution. Using multiple threads lets it manipulate and control machine
resources more efficiently than would a single-threaded application. For
instance, printing, communications file transfers, and database sorting all
are tasks that can be performed simultaneously by separate threads of
execution while the main thread of an application program continues to serve
the end user. Furthermore, in the multitasking environment of OS/2, a
multiple-thread architecture is essential to help ensure that no single task
will hog machine resources. Gordon Letwin, OS/2 architect, first identified
this libertarian approach to sharing resources: programs must obey the rules
in order to work together. This approach makes it obvious who the violator
is when a program abuses the environment, and permits the system to operate
in the most efficient manner possible.

Thus, planning a multithreaded application presupposes that the tasks that
the program will perform can best be implemented by using multiple threads
of execution. There are a number of caveats that help in planning such a
program, which can be summarized as: never assume that OS/2 will execute a
multitasking program or routine in a specific way. Corollaries of this rule
include:

■    Never assume that one routine will execute before another.

■    Never assume that a given routine will execute for a given number of
milliseconds.

■    Never assume that future versions of OS/2 will schedule tasks the same
way the current one does.

■    Never assume that different threads will always be competing for CPU
time. Future versions of OS/2 will run on parallel processors, allowing
different threads to execute simultaneously. While you and I both know that
they don't really execute concurrently at this point, treat them as if they
already do.

■    Never assume any direct correlations between CPU time slices and CPU
cycles.

■    Never assume that OS/2 can guarantee which thread will execute first at
any point during the course of your program. Although the main thread is
always the first thread to execute in the program, there are no guarantees
on execution order once the second thread has begun.

Obviously, the last caveat doesn't mean that there aren't any controls
available for manipulating events or serializing access to resources among
multiple threads: that's where semaphores and priority levels come in. We'll
discuss these later, but for now remember that when writing a multithreaded
application, never assume!

A Multithread Program

The process of creating an additional thread is fairly simple in itself. For
instance, suppose you wanted an application to run uninterrupted, ignoring
all keyboard input until the user presses the Esc key, at which point the
application would terminate. In a DOS application, there are two possible
solutions: you could design the program to occasionally poll the keyboard,
which is cumbersome in a complex application, or your program could trap
the BIOS keyboard interrupt with code that would signal the main program if
the Esc key is pressed.

Under OS/2 these approaches are neither necessary nor relevant. Instead, you
can start a thread that blocks on keyboard input (that is, it waits until
there is keyboard activity). When the user presses a key, the thread
examines the key. If the key is any key other than Esc, it will continue to
block. If the key is Esc, the thread will terminate the entire process.

A brief examination of the program shown in Figure 8 will make this
procedure clearer. The main thread begins where all C programs begin, with
the call to main. The main thread creates the keyboard thread with the call
to the API kernel function, DosCreateThread.

Note that DosCreateThread takes several parameters, as shown by the function
prototype in Figure 9. The first parameter is the address of a function that
contains the code for the thread. Here, the function is innocently named
keyboard_thread. The second parameter is the address of a variable into
which OS/2 will place the thread's identifier once it has successfully
created the thread. The final parameter is the address of the top of the
stack allocated for the thread, which should be at least 512 bytes in size.
Note that in order to pass the address of the top of the stack area,
keythreadstack, we must pass the address of the last byte of keythreadstack
in the manner shown.

As mentioned earlier, all API functions return nonzero on failure, so we
know that a new thread was successfully created if DosCreateThread returns a
zero value. The newly created thread will immediately begin execution of the
code found in keyboard_thread. Although the call to DosCreateThread could
have been placed almost anywhere, calling it early in the program ensures
that the program will terminate immediately if the user presses the Escape
key. Note that the while(TRUE) statement can be replaced with whatever code
you would use for processing in the main thread.

Now let's take a look at the keyboard_thread function. The code for a thread
should always be contained in a single function. You cannot incorporate the
code for this function into main or any other function, nor can you call a
thread function directly. Thus, thread functions are really an OS/2
extension to C.

Note that keyboard_thread begins executing immediately after creation and
then goes into the loop to call the Kbd subsystem function, KbdCharIn. This
function's first parameter is the address of an OS/2 KBDKEYINFO structure,
which will contain information about the keys pressed upon return (this
structure will be discussed in detail in an upcoming installment of this
series). The chChar member of the structure will contain the ASCII value of
the key pressed. The second parameter specifies how long the function should
wait until the user presses a key. Finally, passing IO_WAIT will cause the
function to wait for  the key indefinitely, in turn causing OS/2 to suspend
the calling thread until that thread's screen group receives a key from the
user.

Once the user presses a key, OS/2 will wake up the thread and return from
the KbdCharIn call. The thread then examines the key and breaks out of the
loop if it is the Esc key, calling DosExit to terminate the entire process.
You might note that the thought behind the function is similar to
object-oriented programming: the keyboard_thread function completely
encapsulates the keyboard control and program termination sequence. The main
program doesn't have to know how it works or what it does--that's taken
care of for it by the thread itself.

A program's main thread must be kept alive until all other thread activity
has finished or can be terminated. When the main thread dies, the other
threads die. Thus, if you insert code in the main thread that will exit the
process, the keyboard thread (and any other threads) will be destroyed with
the main thread. If a secondary thread like the keyboard thread shown here
reaches the end of its code without calling a termination routine, it will
die, but it will not affect other threads. Threads can explicitly terminate
themselves by means of a call to DosExit:

DosExit(EXIT_THREAD,
term_code);

or a thread can terminate the entire process via:

DosExit(EXIT_PROCESS,
term_code);

The MAKE file for this example can be found in Figure 10. The /Gs or /G2s
options are used to turn off standard run-time stack checking, since the
run-time stack checks will report false stack overflow errors in code for
the thread. If, however, you wish to isolate this to the thread code itself,
you can insert a #pragma:

#pragma check_stack(off)
/* thread function is
placed here */
#pragma check_stack(on)

which will turn off run-time stack checking for the code inserted between
the #pragmas.

If the main thread needs to close files or shut down other processes or
resources before terminating, the thread function could set a semaphore (as
a flag) that could be checked occasionally by the main thread, instead of
terminating outright. Another alternative would be to use DosExitList, shown
in Figure 11.

Figure 8:

/*
 * Simple keyboard thread example
 *
 * This program illustrates how a process might start
 * a keyboard thread which will terminate the process when
 * the user presses the Esc key.
 *
 * The program starts a keyboard thread which blocks on keyboard
 * input and terminates the entire program when the user presses
 * the Esc key.  All other keys are ignored and thrown away.
 */

#define    INCL_DOS
#define    INCL_SUB

#include   <os2.h>
#include   <mt\stdio.h>
#include   <mt\process.h>

#define    ESC    0x1b
#define    TRUE    1
#define    THREADSTACK    512

char keythreadstack[THREADSTACK];

void keyboard_thread(void);
void main(void);

void main(void)
    {
    TID    threadid;

    if(DosCreateThread(keyboard_thread, &threadid,
            &keythreadstack[THREADSTACK-1]))
        exit(-1);

    while(TRUE)    /* replace this with
          ;           code for main program */
    }


void keyboard_thread(void) /* keyboard thread code */
    {
    KBDKEYINFO keyinfo;

    while(TRUE)
        {
        KbdCharIn(&keyinfo,IO_WAIT,0);  /* wait for keystroke */
        if(keyinfo.chChar == ESC)     /* if ESC pressed, break */
            break;
        }
    DosExit(EXIT_PROCESS,0);          /* terminate the process */
    }

Figure 9:

unsigned DosCreateThread(void (far *) functionptr(void),
                         TID *threadidptr, void *stack);


Reentrance Issues

There is one important constraint to consider when writing multiple-thread
programs. It is the problem encountered when more than one thread tries to
simultaneously execute code that is being used by another thread. This
problem does not arise with the OS/2 API functions: their code has been
written for a multitasking environment. The standard C library and your own
functions, however, are another matter.

Consider a scenario like the following: The standard library routine printf
uses an internal buffer to format the characters that it will write to the
standard output. Suppose one thread is in the middle of executing printf,
with half of the buffer formatted, when another thread begins to execute
printf's code, overwriting the buffer with its own characters--and
resulting in chaos. This behavior is, of course, intolerable, and unless
you're going to implement your own set of semaphores to control the use of
every library routine (which is not a viable solution), you're going to end
up with a mess on your hands.

There are, fortunately, two solutions to this problem. First, when writing
multithread programs, refrain from using any standard library routines in
any but the main thread. This guarantees that only one thread will be using
the standard library routines at a time. With the exception of a few
reentrant routines, the standard library routines are not reentrant and are
designed for single-threaded execution (see Figure 12). If you must control
access to a specific routine from the standard library (or one of your own
routines), there are two ways you can do it:

■    Use the DosEnterCritSec API call to temporarily freeze the other
threads. Although this approach does work, it isn't the best solution and is
mentioned here for informational purposes; there are too many things that
can go wrong.

■    Use a semaphore to control access to a function. This solution is more
practicable, since only the threads that  are trying to access the shared
code will be affected--the rest will continue to execute
(DosEnterCritSec, on the other hand, will freeze all other threads).

If you find it impossible to live without the standard library functions,
there is one additional alternative: the multithreaded standard library.
While earlier versions of the C compiler required that you distinguish
between reentrant and nonreentrant functions, Microsoft now supports a
version of the standard library, LLIBCMT.LIB, that is completely reentrant
and supports multiple threads. If you write programs that use this library,
you must use the new _beginthread and _endthread functions that are
contained in the library (instead of using DosCreateThread and DosExit).
(For more information, see the sidebar "Using the Multithreaded Library:
DosCreateThread vs. _beginthread.")

Finally, you can write your own functions to accommodate multiple threads.
If you choose to do so, there are at least three key guidelines to follow.
First, multithreaded functions cannot disable interrupts or issue an INT
instruction. Second, they should not alter the contents of a segment
register or perform segment manipulations. Last, there must be strict
controls on access to global or static data by functions that can be called
by multiple threads (alluded to earlier in the discussion of the standard
library printf routine). The preferred mechanism for incorporating these
controls into your program is OS/2 semaphores.

Thread Control

A detailed discussion of the use of OS/2 semaphores can be found in "Using
OS/2 Semaphores to Coordinate Concurrent Threads of Execution," MSJ (Vol. 3,
No. 3), but we will briefly reiterate some of the points made in that
article that are pertinent here.

Although OS/2 offers several facilities for interprocess communication
(pipes, queues, signals, and shared memory), semaphores are the preferred
method of coordinating multiple threads. You can use them to serialize
access to pieces of code or resources that cannot be shared. Alternatively,
you can use semaphores when you need to have one thread signal to another
that an event has occurred. Of the several types of semaphores OS/2 offers,
RAM semaphores (used by threads in the same process) are the simplest to
implement and are the easiest to deal with in terms of our first
multithreaded program.

The MS-DOS operating system is a single-tasking environment: only one thread
operates at a time. A program executing under DOS has considerable control
over a resource, including the ability to disable interrupts and ensure
uninterrupted access to it. Signaling between processes in the DOS
environment is easy, since a global variable or flag can be used to
coordinate different pieces of code. You can have one process wait while the
flag is set, that is, until another process clears the flag. Once the flag
has been cleared, the process continues, setting the flag for itself, safe
in the knowledge that it alone has access to a particular resource,
including the flag variable used for signaling.

Under OS/2, this type of signaling is not possible, since there is no way
(without controls) to guarantee the order in which threads will execute.
Further, one thread may be reading a flag while another may be setting or
clearing it, or a second thread might end up setting the flag between the
moment that the first thread stopped waiting and began to set the flag
itself. Therefore, more than one thread might end up with access to the same
resources. The outcome of such a situation is predictably disastrous.
Furthermore, continually checking the value of such a flag uses the CPU
unnecessarily, lowering the efficiency of the system.

Semaphores provide an elegant alternative solution to these problems in the
context of the OS/2 multitasking environment. In one uninterruptible step a
semaphore kernel call can test and set a semaphore. Thus, the semaphore
controls access to a shared resource and allows one task to signal another
that an event has occurred.

To demonstrate a simple use of semaphores, suppose we added a facility to
the keyboard thread program shown in Figure 8. This facility causes the
keyboard thread to increment a counter every time the user presses the Esc
key (instead of terminating the program). The main thread looks at the
counter periodically, and as soon as the counter is greater than a certain
value (say, 3), the main thread will terminate the program.

The problem in OS/2's multithreaded environment concerns the serialization
of a resource, specifically the counter variable that more than one thread
may be sharing. Here's where the semaphore comes in: by using a semaphore,
we can serialize the access to the counter variable, so that only one thread
at a time actually reads or writes it.

The revised listing, shown in Figure 13 (it uses the same MAKE file
mentioned earlier and shown in Figure 10) illustrates the solution. It
creates a semaphore variable, CountSem, which the main thread clears with
the call to DosSemClear. The while loop has been expanded to handle the
reading of the counter variable. The main thread sleeps for 100 milliseconds
(about three 32-millisecond time slices), then calls DosSemRequest to gain
access to CountSem. The -1L parameter causes the function to block the
calling thread until the semaphore has been cleared--that way it will
not be able to access the counter variable if the keyboard thread has
already gained control.

Next, the main thread evaluates the counter thread, breaks out of the loop,
and terminates the program if the counter is greater than 3. Otherwise, it
clears the semaphore (giving up ownership of the resource, the counter
variable) and returns to the top of the loop. Note that the call to DosSleep
suspends the current thread, allowing OS/2 to give CPU time to threads of
the same or greater priority. Without the call to DosSleep, the thread would
attempt to run in a continual loop, unnecessarily burning CPU time. Note,
too, that the keyboard thread does not require DosSleep: the IO_WAIT
parameter to KbdCharIn blocks that thread until there is keyboard input
available.

The keyboard thread code also uses the CountSem semaphore to gain access to
the counter variable. Every time the user presses the Esc key, the keyboard
thread requests access to the semaphore, blocking until the semaphore has
been cleared. Then it increments the counter variable and clears the
semaphore. Again, this mechanism prevents it from accessing the counter
variable at the same time as the main thread. Thus, access to the counter
variable has been serialized and the activity of the two threads has been
synchronized. From this simple example, we can now move to something a
little more complex: a multithreaded version of HELLO.C.

Figure 10:

#
# make file for key.c example found in Figure 8
#

INCLUDE=\os2\include\mt
LIB=\os2\lib
COPT=/Lp /W3 /Zp /Zie /Zl /G2s /I$(INCLUDE)  /Alfw

key.exe: key.c key
    cl $(COPT) key.c /link /co llibcmt


A Multithreaded HELLO.C

The multithreaded version of HELLO.C is designed to help illustrate the use
of threads and semaphores for serializing access to and controlling
resources. Whereas the original HELLO.C simply printed a message on the
screen and terminated, HELLO0.C (shown in the listing in Figure 14),
logically subdivides the screen into frames and prints the message in each
frame. The program assigns each frame a thread that is responsible for
writing and clearing the message. The program's threads continue to write
and clear their messages until the user presses the Esc key. This
frame-based format will be the basis for other example programs as we
explore the OS/2 subsystems and other facilities in later articles in this
series.

Each frame's thread receives a pointer to the frame's data structure, which
contains the information the thread will use during the course of the
program. This structure includes the frame's row/column coordinates, the
thread's ID (returned from _beginthread), a semaphore that the main program
thread will use to activate the thread, and the thread's stack. Exactly how
many frames will appear on the screen depends on the screen mode when you
run the program (25, 43, or 50 lines).

HELLO0.C can take one optional command-line argument: the number of
milliseconds that the main thread should sleep between each activation of a
frame thread. This argument allows you to slow down the visual activity
deliberately so that you can see what's happening a little more clearly. The
command-line parameter, stored in a variable called sleeptime, defaults to 1
millisecond, which the DosSleep kernel function will round up to 32
milliseconds--the minimum OS/2 time slice. It also prevents HELLO0.C
from hogging too much CPU time. You might try running the program with a
parameter of 50, 100, 500, or 1000 (the equivalent of 1 second) to get a
better idea of what's happening.

The program begins by checking the command-line parameter and then calls
_beginthread to spin off a keyboard thread, which blocks until keyboard
input is received and terminates the program when the user presses the Esc
key. The code for this thread is lifted directly from the earlier keyboard
thread example, shown in Figure 8. Next the main thread goes through several
housekeeping and preparatory steps:  it obtains the video mode through a
call to VioGetMode, sets up for the number of lines on the screen, and
calculates the maximum number of frames to display. Then it allocates and
initializes the frame structures (FRAME data types) and randomly chooses a
FRAME for each screen frame, assigning the appropriate row/column
coordinates. Consequently, the frames will seemingly appear and disappear in
a random order, although the order remains static throughout the program.

The real fun in HELLO0.C begins when the main thread calls DosSemSet for
each FRAME, followed by a call to _beginthread to start the FRAME's thread
(which will block until the semaphore clears). Finally, the main thread
enters a loop where it stays for the remainder of the program. In this loop,
it activates the thread for each FRAME by clearing the FRAME semaphore. The
call to DosSleep suspends the thread, forcing it to give up some CPU time
before activating the next thread. The main thread will do this for every
FRAME and then repeat the process. Adding calls to DosSleep when a thread is
in a loop will make the program more efficient, since it eliminates a
thread's ability to waste CPU time.

What does each FRAME thread do? The code for each FRAME thread is contained
in the hello_thread function, which takes one parameter: the address of the
thread's FRAME, which is passed to it by means   of _beginthread. The FRAME
thread code is structured around a loop, at the top of which is a call to
the DosSemRequest kernel function. By passing the address of a semaphore and
a -1 to this function, the calling thread blocks until the semaphore
clears. Thus, each thread is idle until the main thread clears its
semaphore.

Once activated, a FRAME thread will either clear or write its message,
depending on a variable that is toggled every time the thread is active.
Note that the VIO subsystem function, VioWrtCharStr, performs all video
output and writes a specific number of characters from a string at a
specific row/column coordinate on screen. An example of a FRAME thread's
output is shown in Figure 15.

As mentioned earlier, the program will continue until the user presses the
Esc key. At that time, the keyboard thread will wake up (it's been blocked
in the absence of keyboard input) and terminate the program.

HELLO0.C is probably multithreaded overkill (how many times will you need to
run as many as 25 threads in an application?), but it should get you off to
a strong start and clarify the multithread issues addressed in the article.
With this foundation, you'll be able to write some rather complex programs
that use multiple threads. Indeed, program development will become even more
interesting in the next issue, when we explore the VIO subsystem.

Figure 11: Using DOSEXITLIST

OS/2 lets a process establish a set of routines that will always be called
when the process terminates.Typically these routines are functions that free
the process's resources (such as closing open files). Regardless of how and
when the process terminates, OS/2 will execute the functions upon
termination. An application can "register" functions that OS/2 will execute,
thus ensuring an orderly shutdown and disposal of the process's resources in
spite of the unexpected termination of the process.

The kernel function, DosExitList, registers the termination functions with
OS/2, and terminates the functions themselves. The function prototype for
DosExitList is:

unsigned DosExitList(unsigned code,void far
    *fptr(unsigned));

When registering the functions with OS/2, the first parameter can be either
EXLST_ADD or EXLST_REMOVE, which add or remove a function from the list,
respectively. By allowing a process to dynamically add and remove functions
from the termination list, a process can control the destiny of its
resources after its death (in a way, not unlike a human will). A process can
remove the functions from the list prior to normal termination if they are
no longer needed. The second parameter is, obviously, a pointer to the
termination function being registered.

When OS/2 begins execution of the termination functions, the process and all
of its threads have been destroyed, with the exception of the thread
executing the DosExitList functions. OS/2 will transfer control to each
function registered, but in no particular order. Once OS/2 has executed all
the registered functions, the process ends.

The termination functions registered with DosExitList must be defined in the
process (that is, in its code segment) and should be as short and fail-safe
as possible. A termination function may call any OS/2 system function with
the exception of DosCreateThread and DosExecPgm.

A skeleton definition of a termination function follows:

void far termfunc(unsigned code)
    {
    if(code != TC_EXIT)
        {
        .
        /* do cleanup here  */
        .
        }
    DosExitList(EXLST_EXIT,0);
    }

Note that a termination function has one parameter and no return value. The
parameter will always be one of the following:

TC_EXIT    - normal exit
  TC_HARDERROR    - hard-error abort
  TC_TRAP    - trap operation
  TC_KILLPROCESS    - unintercepted DosKillProcess

This allows the termination function to detect whether a normal termination
of the process has occurred. It can also determine what actions the function
should take.

Termination functions must terminate themselves by calling
DosExitList(EXLST_EXIT,0). They cannot execute a 'return' (explicitly or
implicity, by falling past the curly brace), or the process will hang and
never terminate. The EXLST_EXIT code tells OS/2 that the termination
processing is complete, and that it should call the next function on the
termination list.

Figure 12:

abs

atoi

atol

bsearch

chdir

getpid

halloc

hfree

itoa

labs

lfind

lsearch

memccpy

memchr

memcmp

memcpy

memicmp

memmove

memset

mkdir

movedata

putch

rmdir

segread

strcat

strchr

strcmp

strcmpi

strcpy

stricmp

strlen

strlwr

strncat

strncmp

strnicmp

strncpy

strnset

strrchr

strrev

strset

strstr

strupr

swab

tolower

toupper

Figure 13:

#define    INCL_DOS
#define    INCL_SUB
#include<stdio.h>
#include<process.h>
#include<os2.h>

#define    ESC    0x1b
#define    TRUE 1

void keyboard_thread(void);
void main(void);

#define    THREADSTACK    512

char keythreadstack[THREADSTACK];
long CountSem =    0L;
unsigned count = 0;

void main(void)
    {
    TID    threadid;

    DosSemClear(&CountSem);

    if(DosCreateThread(keyboard_thread,&threadid,
            &keythreadstack[THREADSTACK-1]))
        exit(-1);

    while(TRUE)    /* insert code for main program here */
        {
        DosSleep(100L);
        DosSemRequest(&CountSem,-1L);
        if(count > 3)
            break;
        DosSemClear(&CountSem);
        }
    }

void keyboard_thread(void)  /* keyboard thread code */
    {
    KBDKEYINFO keyinfo;

    while(TRUE)
        {
        KbdCharIn(&keyinfo,IO_WAIT,0);  /* wait for keystroke */
        if(keyinfo.chChar == ESC)     /* if ESC pressed, break */
            {
            DosSemRequest(&CountSem,-1L);
            count++;
            DosSemClear(&CountSem);
            }
        }
    DosExit(EXIT_PROCESS,0);          /* terminate the process */
    }

Figure 14:

/* os2hello.c by RHS, 10-14-88
 *
 * OS/2 and 1988 version of K&R's hello.c
 * demonstrates multiple threads
 */

/*

This program provides an introduction to the use of threads and semaphores
under OS/2. It divides the screen up into a series of logical frames. Each
frame is a portion of the screen that is managed (written to) by a single
thread. The exact number of frames will depend on the current screen length
(25, 43 and 50 lines). Each thread has its own data from which it knows
where the frame can be found on screen. This includes a semaphore which
signals the thread when to proceed. These elements can be found in the FRAME
data type.

Upon receiving a signal from its semaphore (i.e., the semaphore has been
cleared), the thread either draws a message on the frame or clears the
frame, and reverses the flag that determines this. Then it again blocks
until its semaphore has been cleared again.

The main program thread starts by setting up the frame information: checking
the screen size, determining the number and size of the frames. It also
"randomly" selects the order in which the frames will appear.

Then it sets each thread's semaphore and initiates each thread (remember the
threads will block until their semaphores are cleared.

Finally, the main program goes into an infinite loop, clearing each thread's
semaphore, sleeping for at least 1 millisecond, and then continuing to the
next thread. Thus the threads asynchronously call the VIO subsystem to draw
or clear each frame, while the main program thread continues.

An optional parameter can be passed to set the number of milliseconds passed
to DosSleep, allowing the operator to more accurately "see" the order in
which the frames appear/erase. This value must always be at least 1 to allow
the main program thread to give time to the CPU scheduler.

A call to _beginthread() early in main() sets up a thread to monitor
keyboard input. This thread blocks until a key is pressed, then examines the
key, and if the key is the Escape Key (27 decimal or 1bH), the thread calls
DosExit to kill the whole process.

*/

#define  INCL_SUB
#define  INCL_DOSPROCESS

#include <os2.h>
#include <mt\stdio.h>
#include <mt\string.h>
#include <mt\assert.h>
#include <mt\stdlib.h>
#include <mt\process.h>

#if !defined(TRUE)
#define TRUE    1
#endif

#if !defined(FALSE)
#define FALSE   0
#endif

#define LINES25     4        /* height in lines of frames*/
#define LINES43     6
#define LINES50     7


#define MAXFRAMES    28      /* limited to max frames possible */
#define RAND()       (rand() % maxframes);
#define THREADSTACK  400     /* size of stack each thread*/
#define IDCOL        15
#define ESC          0x1b

char *blank_str = "                    "; /* string for
                                             blanking frame */


    /* frame data */

char *hello_str25[LINES25+1] =
    {
    "                    ",
    "    Hello, world!   ",
    "  from Thread #     ",
    "                    ",
    "\0"
    };

char *hello_str43[LINES43+1] =
    {
    "                    ",
    "                    ",
    "    Hello, world!   ",
    "  from Thread #     ",
    "                    ",
    "                    ",
    "\0"
    };

char *hello_str50[LINES50+1] =
    {
    "                    ",
    "                    ",
    "    Hello, world!   ",
    "                    ",
    "  from Thread #     ",
    "                    ",
    "                    ",
    "\0"
    };


char **helloptr;
int numlines;

typedef struct _frame   /* frame structure */
    {
    unsigned    frame_cleared;
    unsigned    row;
    unsigned    col;
    unsigned    threadid;
    long        startsem;
    char        threadstack[THREADSTACK];
    } FRAME;

FRAME far *frames[MAXFRAMES];     /* pointers to frames */
unsigned maxframes;

ULONG sleeptime = 1L;             /* minim sleep time */
char keythreadstack[THREADSTACK];


    /* function prototypes */

void hello_thread(FRAME far *frameptr);
void keyboard_thread(void);
void main(int argc, char **argv);


void main(int argc, char **argv)
    {
    int row, col, maxrows, maxcols, len, i, loops = 0;
    VIOMODEINFO viomodeinfo;

    if(argc > 1)
        sleeptime = atol(argv[1]);
    if(sleeptime < 1L)
        sleeptime = 1L;

    /* start keyboard thread */
    if(_beginthread(keyboard_thread,keythreadstack,
                    THREADSTACK,NULL) == -1)
        exit(-1);

    viomodeinfo.cb = sizeof(viomodeinfo);
    VioGetMode(&viomodeinfo,0);            /* get video info */

    maxrows = viomodeinfo.row;
    maxcols = viomodeinfo.col;

    switch(maxrows)
        {
        case 25:
            helloptr = hello_str25;
            numlines = LINES25;
            break;
        case 43:
            helloptr = hello_str43;
            numlines = LINES43;
            break;
        case 50:
            helloptr = hello_str50;
            numlines = LINES50;
            break;
        default:                 /* fail if not 25,43,50 lines */
            assert(0);
            exit(-1);
        }

    len = strlen(*helloptr);

    maxframes = (maxrows / numlines) * (maxcols / len);

    assert(maxframes <= MAXFRAMES);

    for( i = 0; i < maxframes; i++)   /* initialize structures */
        {
        if(!(frames[i] = malloc(sizeof(FRAME))))
            exit(0);
        frames[i]->frame_cleared = FALSE;
        frames[i]->startsem = 0L;
        memset(frames[i]->threadstack,0xff,
               sizeof(frames[i]->threadstack));
        }

    i = RAND();     /* get first random frame */


    /* set up random appearance */

    for(row = col = 0; loops < maxframes ; )    /* set row/col
                                                   each frame */
        {
        if(!frames[i]->frame_cleared)
            {
            frames[i]->frame_cleared = TRUE; /* set for empty
                                                 frame */
            frames[i]->row = row;         /* frame upper row */
            frames[i]->col = col;         /* frame left column */

            col += len;                   /* next column on
                                             this row */
            if(col >= maxcols)            /* go to next row? */
                {
                col = 0;                  /* reset for start
                                             column */
                row += numlines;          /* set for next row */
                }

            i = RAND();                   /* get next random
                                             frame */
            }
        else
            ++i;

        if(i >= maxframes)
            {
            i -= maxframes;
            loops++;                       /* keep track of #
                                              of frames*/
            }
        }


    for( i = 0 ; i < maxframes; i++)       /* start a thread
                                              for each */
        {
        DosSemSet(&frames[i]->startsem);   /* initially set
                                              each sem. */


        /* start each thread */

        if((frames[i]->threadid = _beginthread(
                (void far *)hello_thread,
                (void far *)frames[i]->threadstack,
                THREADSTACK,
                (void far *)frames[i])) == -1)
            {
            maxframes = i;      /* reset maxframes on failure */
            break;
            }
        }

    while(TRUE)                 /* main loop */
        {


        /* swing thru frames, signalling to threads */

        for( i = 0; i < maxframes; i++)
            {
            DosSemClear(&frames[i]->startsem);   /* clear: thread
                                                    can go */
            DosSleep(sleeptime);             /* sleep a little */
            }
        }
    }


void hello_thread(FRAME far *frameptr)    /* frame thread
                                             function */

    {
    register char **p;
    register int line;
    int len = strlen(*helloptr);
    unsigned row, col = frameptr->col;
    char idstr[20];

    while(TRUE)
        {
        DosSemRequest(&frameptr->startsem,-1L); /* block until
                                                   cleared */
        itoa(frameptr->threadid,idstr,10);      /* init idstr */

        row = frameptr->row;         /* reset row */

        if(!frameptr->frame_cleared) /* if frame in use, erase */
            for( line = 0; line < numlines; line++, row++)
                VioWrtCharStr(blank_str,len,row,col,0);
        else                   /* else frame not in use */
            {
            p = helloptr;      /* print message */
            for( line = 0; **p; line++, row++, p++)
                VioWrtCharStr(*p,len,row,col,0);


            /* write id # in frame */

                VioWrtCharStr(idstr,3,
                              row-(numlines/2),
                              IDCOL+col,0);
            }


        /* toggle use flag */

        frameptr->frame_cleared = !frameptr->frame_cleared;
        }
    }

void keyboard_thread(void)
    {
    KBDKEYINFO keyinfo;

    while(TRUE)
        {
        KbdCharIn(&keyinfo,IO_WAIT,0); /* wait for keystroke */
        if(keyinfo.chChar == ESC)      /* break if ESC pressed */
            break;
        }
    DosExit(EXIT_PROCESS,0);           /* terminate process */
    }


/* end of hello0.c */

Using The Multithreaded Library:

DosCreateThread vs. _beginthread

The OS/2 API interface for creating and terminating threads works through
the kernel functions DosCreateThread and DosExit. Unfortunately, although
the code for a thread must be contained within a function, you cannot pass
parameters to it by using DosCreateThread. So if you prefer more of a C-like
interface to write multithreaded programs, or you're going to use the
multithreaded standard library, LLIBCMT.LIB, you should be familiar with an
alternative interface via _beginthread and _endthread. If you plan on
calling standard library functions from within your thread function, it's
imperative that you use LLIBCMT.LIB.

The Case of printf

The discussion of printf in the main text illustrates the need for this
interface. A function like printf uses a large internal buffer for
formatting its output string. Although this buffer is adequate if a single
thread is executing printf's code, the outcome is unpredictable if more than
one thread is trying to execute printf at the same time. There are two ways
that printf can be written to resolve this: you can use a semaphore to
permit only one thread at a time to execute the code for printf, or you can
use a set of semaphores to allow a finite number of threads to access printf
simultaneously.

Let's take a closer look at these two solutions. With the first method
(sketched in Figure A), a semaphore is set at the beginning of printf and
cleared at the end. This approach serializes the code for printf so that
only one thread can execute it at a time. Unfortunately, that means that any
time a thread calls printf, it will block if some other thread is executing
the printf code and will remain suspended until the thread using printf
clears the semaphore. Additionally, there is no guarantee that the next
thread will be allowed to execute printf, nor can the scheduler ensure which
thread that will be. OS/2 cannot guarantee that the next thread you want to
call printf will be the one allowed to execute it with this approach. Thus,
a scenario might develop where a thread of lesser priority might constantly
be preempted by higher priority threads executing printf--and that can
cause undesirable visual results.

Alternatively, the second method limits the number of threads that can
simultaneously execute the code for a function like printf. However, it
offers no possibility of collision between threads competing for access to
printf's code. With this approach (illustrated in Figure B), printf is
structured to provide a fixed set of formatting buffers, with access to each
controlled by a different semaphore. Consequently, every thread is given its
own private buffer while executing the code. The catch is the limit on the
number of buffers and therefore the limited number of threads that can gain
access.

This limit is one reason _beginthread is provided. The function offers more
of a C-like approach to creating a new thread by allowing you to pass
parameters to the thread and returning the thread ID when successful. But
more specifically, it restricts the calling process to 32 threads, far less
than the 255 threads that can be created by DosCreateThread. For many
applications, however, 32 threads will be more than enough, allowing the
multithreaded library, LLIBCMT.LIB, to operate on the assumption that no
process will consist of more than 32 threads at a time. For this reason,
_beginthread is available only when you use this library, and you should use
it instead of DosCreateThread when writing a program that uses this library.

Figure A:

void printf(char *fmt,...)
    {
static long printfSem = 0L;
static char formatbuffer[BUFSIZ];

DosSemRequest(&printfSem,-1L);

■
■
■

DosSemClear(&printfSem);
}

Figure B:

#define    MAXTHREADS    32
void printf(char *fmt,...)
{
static long printfSems[MAXTHREADS] =
    {0L,0L,0L,0L,0L,0L,0L,0L,0L,0L,
     0L,0L,0L,0L,0L,0L,0L,0L,0L,0L,
     0L,0L,0L,0L,0L,0L,0L,0L,0L,0L,
     0L,0L};

char formatbuffers[MAXTHREADS][BUFSIZ];
int semno;

for(semno = 0; semno < MAXTHREADS; semno++)
    if(!DosSemRequest(&printfSems[semno],0L))
        break;
assert(semno < MAXTHREADS);

■
■
■

DosSemClear(&printfSem[semno]);

}


Using _beginthread

How does _beginthread work? From the function prototype shown in Figure C,
you can see that, like DosCreateThread, it requires the address of a
function that contains the code for the thread. Unlike DosCreateThread,
however, _beginthread takes not the address of the top of the stack, but the
address of a stack area as you would declare it in your C program (that is,
the bottom of the stack). Therefore you must provide it with the stack size
(the third parameter). Last of all, _beginthread takes a parameter that
makes it more valuable than DosCreateThread in some cases: an argument
parameter for passing arguments to the thread function itself. An example of
this use of _beginthread can be found in the multithreaded HELLO0.C program.
Finally, you'll notice that _beginthread itself returns -1 on error or
the thread ID of the new thread.

Obviously, _beginthread must at some point call DosCreateThread. In fact, it
even calls DosCreateThread when you have exceeded the number of threads
allowed by the multithreaded library. The only way it can limit the number
of threads to a process is to get the thread ID returned from its own call
to DosCreateThread and return -1 if the ID is greater than 32.
However, this reveals the use of two undocumented assumptions about OS/2:
that DosCreateThread will always return the lowest available thread ID and
that OS/2 will reuse thread IDs of previously terminated threads.

Figure C:

#include<mt\process.h>
#include<mt\stddef.h>

int cdecl far _beginthread(
          void (cdecl far *start_address) (void far *),
          void far *stack_end,
          unsigned stack_size,
          void far *arglist);

■
■
■
void far cdecl _endthread(void)

Linking LLIBCMT.LIB

To use _beginthread and its counterpart to DosExit, _endthread (also shown
in Figure C), make sure that the LLIBCMT.LIB and DOSCALLS.LIB libraries are
available in the current directory or in the directory pointed to by the LIB
variable (in the environment or in your MAKE file--see the MAKE file
for HELLO0.C as an example). The DOSCALLS.LIB library is required, since
_beginthread, _endthread, and some of the other library functions will make
calls to OS/2 API functions. LLIBCMT.LIB should be used in place of any
other run-time libraries.

In addition, you may want to change your INCLUDE variable (again in the
environment or in your MAKE file) to point to the MT directory that the
compiler installed beneath the standard INCLUDE directory. This directory
contains copies of the standard header files and should be used for creating
multithreaded programs. You can set the INCLUDE variable on the compiler
command line with the /I option as an alternative. Incidentally, the
prototypes for _beginthread and _endthread can be found in PROCESS.H.

Limitations

Multithreaded code must make several assumptions as it executes. First, all
code and data addresses are expected to be far. In addition, the code must
assume that the data segment is fixed but should not assume that the stack
and data segments are the same. In addition, conventional run-time stack
checking must be turned off, since it is taken care of for each thread in a
multithreaded program. You can conveniently use compiler switches to take
care of these concerns by employing the /Alfw and /G2s options. Also, either
the /Zl compiler option or the /NOD linker option should be used to prevent
the default library search by the linker. The MAKE file for HELLO0.C
illustrates this usage and can easily be adapted to compile and link your
own multithreaded programs.

Finally, remember that multithreaded programs cannot be bound into dual-mode
applications since there is no facility in MS-DOS for simultaneous execution
of multiple threads of code.

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

Volume 4 - Number 3

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


A Technical Study of Dynamic Data Exchange Under Presentation Manager

 Susan Franklin and Tony Peters

The IBM Corporation and Microsoft recently shipped an updated version of the
OS/2 operating system, which contains some important enhancements over the
initial release of the OS/2 systems. The major enhancement to the OS/2
Version 1.1 release is the inclusion of the Presentation Manager (referred
to herein as PM) as a standard component. The OS/2 Presentation Manager is
based on Microsoft(R) Windows and provides the same benefits Windows
provided to DOS: a windowed, graphical user interface and support for a
variety of input and output devices.

An important component of Microsoft Windows that has been implemented in
OS/2 PM is the Dynamic Data Exchange (DDE) protocol. The Windows DDE version
was described in "Inter-Program Communication Using Windows' Dynamic Data
Exchange," MSJ (Vol. 3, No. 6). DDE is a published message protocol for the
exchange of data between participating programs and has gained wide
acceptance among Windows applications as the standard messaging protocol for
data exchange. The evolution of DDE from DOS to the OS/2 environment has
required some enhancements to address limitations associated with the
original Windows DDE specification. This article describes that evolution
and provides a graphical data exchange program as an example.

Protocol Modifications

In the Windows environment, DDE provided a consistent, flexible method for
communication between applications. When moving DDE to the multitasking,
protected memory environment of the OS/2 PM, however, certain changes to the
protocol were needed. These changes had to address the new concepts
introduced by the OS/2 environment without significantly changing the DDE
model, which has proved successful in the Windows environment.

An early approach to the migration of the DDE protocol to OS/2 and
Presentation Manager was a simple remapping of the message parameters. The
primary change necessary was the parameter used when actually passing the
data to another application, which requires crossing OS/2 process
boundaries. Whereas a handle to the data is sufficient in the Windows
environment, a memory selector is required to pass data between separate
OS/2 processes. String data could still be passed in the global atom table,
although applications were necessary to explicitly request access to the
atom table. This approach solved the problems introduced by protected memory
into the DDE message set. But several other problems and limitations still
existed. They could be solved by further modification of the protocol.

DDE suffered from the two-parameter limit inherent in PM messages. Following
the Windows DDE model, the first parameter in any DDE message is the handle
of the sending window. This left just one 32-bit parameter to pass all other
conversation parameters and references to data. Although the necessary
parameters and selectors could fit into the remaining long parameter, there
was no room for any future expansion to the protocol.

Communicating with other machines on a local area network (LAN) or with
other types of computers proved difficult given the parameter limits. This
limitation was also a problem if and when the operating system's addressable
space increased. Even in the current environment, any expansion to the DDE
model or conversation parameters would not be possible given the lack of
parameter space. Clearly, the protocol had to be modified such that it
permitted expandability and application freedom to include additional
parameters as necessary.

In order to expand the parameter space for DDE messages, the PM version of
DDE uses the second DDE message parameter as a 32-bit pointer to one of two
available DDE data structures. These structures contain all necessary DDE
conversation parameters as well as the actual data when necessary. As the
requirements for DDE change, this structure can be expanded without changing
the parameters of the DDE messages. The long address of the structure
permits compatibility to future system software and other machines. By
packaging all parameters into one structure, the message parameters become
more consistent since the variant parameters are contained in the structure
itself.

All parameters are packaged into a single structure, so the use of the atom
table is no longer necessary. Adding string parameters to the atom table
just introduces a second data access method that complicates the protocol.
Instead string data is included in the DDE structures.

Since the majority of DDE messages will be sent or posted to windows in a
separate process, the memory containing the DDE structure had to be made
accessible to the receiving window. Rather than require applications using
DDE to grant this access, the PM DDE protocol provides new Application
Program Interfaces (APIs) for sending and posting DDE messages. Applications
do not use WinPostMsg or WinSendMsg to transmit DDE messages. Instead, the
message and parameters are passed to a system API, which grants the
receiving window access to the DDE structure and sends or posts the message
on behalf of the calling application. These APIs ensure that the access to
the structure is granted consistently and correctly, while simplifying the
programming efforts of an application implementing DDE.

These enhancements to DDE for OS/2 have provided a standard framework for
applications to communicate without having to design a new protocol that
would be suitable for DDE within a multitasking operating system.
Additionally, using OS/2 DDE provides a concise method of implementing
ever-increasingly complex graphical data exchange in an efficient manner.

Single Client/Server

In the simplest case, DDE is used when one application, called the client
application, requires data from another independent application, called the
server application. The classic example for this model is a charting program
receiving updates of data from a spreadsheet and reflecting those changes by
redrawing the chart. In that case, the charting program is the client and
the spreadsheet is the server.

We will begin by following the logic for the simplest of cases, the single
client/single server model. In this example, the purpose of the DDE
conversation is the exchange of graphical data between programs. The server
application is any application wishing to display a picture in the client
application's window. The graphical data is packaged and sent to the client
application each time the server application wishes to change the appearance
of its picture.

Figure 1 shows the general logic flow for the single client/single server
type of application. The client application initiates the conversation. Once
the server acknowledges the initiate, the client requests the data and the
server sends it inside the appropriate structure.

A single client/single server DDE conversation begins when the client
application broadcasts a WM_DDE_INITIATE message to all other top-level
windows in the system. The client specifies the string name of the
application expected to reply, as well as a string name identifying the
topic of the proposed conversation. Either of these string names may be NULL
to indicate that the desired server application or the topic name is not
specific and any application implementing DDE may participate. The
WM_DDE_INITIATE message is not broadcast directly by the application.
Instead, the client application uses the WinDdeInitiate call to send the
message. Figure 2 illustrates the procedure for initiation.

When a server application gets the WM_DDE_INITIATE message, it checks the
application and topic names to determine whether it will participate in the
conversation. Sample code for initiate processing appears in Figure 3. Note
that the application and string pointers that were input to the
WinDdeInitiate call do not surface as explicit message parameters in the
WM_DDE_INITIATE message. Instead, they are included in the DDEINIT
structure, which is referenced by the second parameter. In all DDE messages,
the first parameter contains the window handle of the window that originated
the DDE message.

If the server decides to participate in the conversation,  then the
WinDdeRespond call   is used in order to send the WM_DDE_INITIATEACK message
back to the client application. Again, the strings referenced in the
parameters of this call will be copied to the DDEINIT structure, and the
pointer to the structure will be passed as the second parameter in
WM_DDE_INITIATEACK.

Once the conversation link has been established, the actual data transfer
may take place. This exchange may be a one-time data transfer, an ongoing
data transfer, or a transfer of remote commands.

One-time data transfer is accomplished by using the WM_DDE_REQUEST message
when data is flowing from server to client and the WM_DDE_POKE message when
data is flowing from client to server. In either case, a DDESTRUCT is
allocated and filled by the client application. The structure contains
status flags describing the format of the data that is being requested (or
poked) as well as the status and the size of the data. Once   the structure
is filled, the WM_DDE_REQUEST or WM_DDE_POKE message is posted to the server
application using the WinDdePostMsg API. This is a call that is used for all
messages   except   for WM_DDE_INITIATE and WM_DDE_INITIATEACK. The call
ensures that the receiving window handle gets access to the memory allocated
for the DDESTRUCT.

When the data is being poked, the WM_DDE_POKE message is the only one
required to complete the transfer, since the transfer is unsolicited. When
the data is requested, the server application responds with the WM_DDE_DATA
message. If the server cannot supply the data in the requested format, it
responds with a negative WM_DDE_ACK message. Figure 4 illustrates the code
required in the server application to handle both one-time data transfer
methods.

Perhaps the most common type of data transfer in DDE is the establishment of
a permanent data link between applications. In that case, the client
application posts a WM_DDE_ADVISE. The DDESTRUCT is filled in the same
manner as it is for the WM_DDE_REQUEST message. This message informs the
server application that the client would like to receive WM_DDE_DATA
messages as the data change. As in the case of the WM_DDE_REQUEST message,
the server responds with a negative WM_DDE_ACK message if the data cannot be
supplied in the requested format. If the data can be supplied, the client
will continue to receive WM_DDE_DATA messages until either the conversation
terminates or the permanent link is terminated. Figure 5 shows the server
handling a request for a permanent data link.

When the client terminates the permanent link, it posts the WM_DDE_UNADVISE
message. This message does not end the DDE conversation; rather, WM_DDE_DATA
messages will no longer be posted when data changes.

Another requirement during a DDE conversation is the execution of commands
by a server application on behalf of the client. In this particular case,
DDESTRUCT contains a string of commands to be executed. A positive or a
negative WM_DDE_ACK message, depending on the outcome of the execution, is
posted to the client. A code fragment for remote execution of commands is
illustrated in Figure 6.

Termination of a DDE conversation may be instigated by either the client or
the server application. Typically, the WM_DDE_TERMINATE message is posted
when the user has requested to close the application, although this isn't
always the case. Whatever the reason for the termination, posting of the
WM_DDE_TERMINATE indicates that no further DDE messages will be sent. The
application that is posting the WM_DDE_TERMINATE may not shut down until the
other application has responded with another WM_DDE_TERMINATE. There are no
parameters used with the terminate message.

Figure 2

case WM_CREATE:    /* broadcast the initiate message  */
   WinDdeInitiate(hwnd, "App_Name", "Graphics_Exchange");

break;

Figure 3:

case WM_DDE_INITIATE:  /* respond if app and topic strings match */
 pDDEInit = (PDDEINIT)lParam2;
 if ((HWND)lParam1 != hwnd) {
    if((!strcmp("App_Name", pDDEInit->pszAppName)) &&
        (!strcmp("Graphics_Exchange", pDDEInit->pszTopic))) {
           WinDdeRespond(lParam1, hwnd, "Client",
           "Graphics_Exchange");
    }
 }
 DosFreeSeg(PDDEITOSEL(pDDEInit));
break;

Figure 4:

case WM_DDE_REQUEST: /* allocate DDESTRUCT and send data */
 pDDEStruct = (PDDESTRUCT)lParam2;
 strcpy(szTemp, "Text_Data");
 if((pDDEStruct->usFormat == DDEFMT_TEXT) &&
     (!strcmp(szTemp, DDES_PSZITEMNAME(pDDEStruct)))) {
         nNumBytes = (strlen(szTemp) + 2 + strlen(szData));
         pDDEStruct = st_DDE_Alloc((sizeof(DDESTRUCT) + nNumBytes),
                                   "DDEFMT_TEXT");
         pDDEStruct->cbData = strlen(szData) + 1;
         pDDEStruct->offszItemName = (USHORT)sizeof(DDESTRUCT);
         pDDEStruct->offabData = (USHORT)((sizeof(DDESTRUCT) +
                                       strlen(szTemp)) + 1);
         memcpy(DDES_PSZITEMNAME(pDDEStruct), szTemp, (strlen(szTemp)
                + 1));
         memcpy(DDES_PABDATA(pDDEStruct), szData, (strlen(szData)
                + 1));
         pDDEStruct->fsStatus |= DDE_FRESPONSE;

WinDdePostMsg((HWND)lParam1, hwnd, WM_DDE_DATA, pDDEStruct,

TRUE);
         DosFreeSeg(PDDESTOSEL(lParam2));
 }
 else {  /* send negative ACK using their DDESTRUCT */
      pDDEStruct->fsStatus &= (~DDE_FACK);
      WinDdePostMsg((HWND)lParam1, hwnd, WM_DDE_ACK, pDDEStruct,
                    TRUE);
 }
break;

case WM_DDE_POKE: /* unsolicited data from client */
   pDDEStruct = (PDDESTRUCT)lParam2;
   strcpy(szTemp, "Text_Data");
   if((pDDEStruct->usFormat == DDEFMT_TEXT) &&
      (!strcmp(szTemp, DDES_PSZITEMNAME(pDDEStruct)))) {
           strcpy(szData, DDES_PABDATA(pDDEStruct));
           DosFreeSeg(PDDESTOSEL(lParam2));
 }
 else {  /* send negative ACK using their DDESTRUCT */
      pDDEStruct->fsStatus &= (~DDE_FACK);
      WinDdePostMsg((HWND)lParam1, hwnd, WM_DDE_ACK, pDDEStruct,
                    TRUE);
 }

break;

Figure 5:

case WM_DDE_ADVISE:  /* set ADVISE bit in window data and ACK */
 pDDEStruct = (PDDESTRUCT)lParam2;
 if(pDDEStruct->usFormat == DDEFMT_TEXT) {
    pWWVar->bAdvise = TRUE;
    if((pDDEStruct->fsStatus & DDE_FACKREQ) == DDE_FACKREQ) {
       pDDEStruct->fsStatus |= DDE_FACK;
       WinDdePostMsg((HWND)lParam1, hwnd, WM_DDE_ACK, pDDEStruct,
                     TRUE);
    }
    else {
        DosFreeSeg(PDDESTOSEL(lParam2));
    }
 else {    /* Send a negative ACK using their DDESTRUCT */
    pDDEStruct->fsStatus &= (~DDE_FACK);

WinDdePostMsg((HWND)lParam1, hwnd, WM_DDE_ACK, pDDEStruct, TRUE);

}
break;

Figure 6:

case WM_DDE_EXECUTE:                   /* execute a command */
   pDDEStruct = (PDDESTRUCT)lParam2;
   strcpy(szCommand, DDES_PABDATA(pDDEStruct);
   if(!Dde_Cmd_Processor(szCommand)) {   /* parse and execute
                                            the command */
          pDDEStruct->fsStatus &= (~DDE_FACK);
          WinDdePostMsg((HWND)lParam1, hwnd, WM_DDE_ACK,
                        pDDEStruct, TRUE);
 }
 else {
    DosFreeSeg(PDDESTOSEL(lParam2));
 }

break;



User-Defined Formats

It is possible for an application to create its own DDE data format if an
appropriate, system-provided data format is not available. The system
provides one predefined format for interchanging text strings, DDEFMT_TEXT.
Before an application uses an application-defined data format, however, it
must establish a convention for obtaining a unique ID and registering that
format so other applications can associate the format ID in the DDESTRUCT
with their specialized data format. Although appearing similar to clipboard
data formats, DDE formats are not to be confused with clipboard formats.
Clipboard formats identify handle types, whereas DDE formats identify the
actual layout of the data in the DDESTRUCT block. We have chosen to register
DDE formats using the system atom table. The prefix of DDE formats is
DDEFMT_. Using the atom manager to register DDE formats guarantees unique
IDs among all applications that use this method to register DDE formats.

Figure 7 represents a function, Register_DDEFMT, which illustrates how to
register a user-defined DDE data format with the system. Register_DDEFMT
returns the DDE data format for either an existing or a newly created data
format. The first time Register_DDEFMT is invoked for a particular DDE data
format, that format is registered in the system atom table. Every call that
is sent to Register_DDEFMT for a particular DDE format string identifier
results in the format being retrieved and returned to the caller. Looking
ahead, Figure 9 shows an example of a function call to Register_DDEFMT.

The user DDE data format for the DDE graphics exchange sample program in
this article is shown in Figure 8. The format consists of a graphics
descriptor control block in the abData area. This graphics descriptor
control block contains, among other information, an offset pointer to the
graphics data that is passed in the same shared memory block following the
GDE data in abData.

Figure 7:

USHORT Register_DDEFMT(pszFormat)
PSZ pszFormat;
{
HATOMTBL hAtomtbl;
USHORT   retn;

    hAtomtbl = WinQuerySystemAtomTable();
    if (retn = WinFindAtom(hAtomtbl,pszFormat))
       return retn;
    else
       return (WinAddAtom(hAtomtbl,pszFormat));

}

Using Shared Memory

DDE uses shared memory for all communications. Two different types of memory
objects are allocated for DDE transactions--the DDEINIT and the
DDESTRUCT structures. The way these objects are allocated may differ, but
both result in shared memory made available to the recipient. These shared
memory objects need to be freed properly for OS/2 shared memory management
to be effective. The data areas containing szAppName and szTopicName in the
WinDdeInitiate and WinDdeRespond calls may be in private or shared memory.
When the WinDdeInitiate and WinDdeRespond calls are executed, the system
will copy those strings into DDEINIT and make DDEINIT available to the
recipient.

The data area that contains   the DDESTRUCT structure must be allocated
using the SEG_GIVEABLE flag in the DosAllocSeg and DosAllocHuge calls. The
WinDdePostMsg API makes the DDESTRUCT memory object available to the message
recipient and frees the object from the sender.

Any pointers that are part of the abData field must point to shared memory.
It is the application's responsibility to manage the allocation and sharing
synchronization of these shared data areas. The application may use either
base OS/2 memory management API calls or higher level memory subsetting
common services to achieve this, as long as the memory is managed properly.

The message recipient is responsible for freeing all memory objects after
retrieving the information. DosFreeSeg is used to release the memory.
DDEINIT and DDESTRUCT must both be released by the message recipient.

Figures 9 and 10 show the allocation and deallocation of the shared memory
objects necessary for processing of the WinDdePostMsg API. The first part of
Figure 9 is a call to a local function, which allocates and initializes the
DDE shared memory object. The accompanying function, DDE_Alloc(), does the
actual shared memory allocation by making a call to DosAllocSeg() and clears
the entire shared memory object. The DDE shared memory block is initialized
with the user-defined data DDE format by setting usFormat field in
DDESTRUCT. This is accomplished by calling the function Register_DDEFMT(),
which we previously discussed. By allocating and initializing the DDE shared
memory objects in this manner, we can readily obtain ready-to-use shared
memory objects for each of our DDE transactions.

Figure 10 shows how deallocation of the DDESTRUCT, in this case during the
processing of a WM_DDE_DATA message, is accomplished by calling DosFreeSeg
with the selector of the DDE memory object as its parameter.

Figure 9:

/* send a request for data */

       /* allocate memory  */
       DDEstrptr = DDE_Alloc(sizeof(DDESTRUCT), IDS_GDE);

WinDdePostMsg((HWND)lParam1, DDEtoHWND, (ULONG)WM_DDE_REQUEST,

DDEstrptr, TRUE);

.
.
.

PDDESTRUCT DDE_Alloc(size, format)
int size;
char *format;

/************************************************************
 *  1. Allocate a block of size. bytes
 *     of giveable, shared memory for DDE call.
 *  2. Fill in DDE data format by
 *     calling Register_DDEFMT((PSZ)format);.
 ************************************************************/
{
SEL     ddepsel;
USHORT  dasret;
PDDESTRUCT DDEstrptr;
   if ((dasret = DosAllocSeg(size, &ddepsel, SEG_GIVEABLE)) == 0) {
       DDEstrptr = (PDDESTRUCT)SELTOPDDES(ddepsel);
       memset(DDEstrptr, (BYTE)NULL, size); /*  set allocated memory
                                                to nulls  */
       /* fill in DDE data format */
       DDEstrptr->usFormat = Register_DDEFMT((PSZ)format);

   } else {   /*  error  */
       return((PDDESTRUCT)NULL);
     }

}

Figure 10:

MRESULT APIENTRY MasterDDEWndProc(hwnd,message,lParam1,lParam2)

■
■
■

    DDEstrptr = (PDDESTRUCT)lParam2;

    case WM_DDE_DATA:

■
■
■

DosFreeSeg(PDDESTOSEL(DDEstrptr));

DDEINIT and DDESTRUCT

Presentation Manager processes the data and allocates the memory for the
WinDdeInitiate and WinDdeRespond API calls as shown in Figure 11. The system
takes the strings passed in the WinDdeInitiate call and copies the strings
into multiple DDEINIT blocks that are broadcast to all applications running
on the system. Participating DDE server applications process the
WinDdeInitiate call by sending a WM_DDE_INITIATEACK message with the
WinDdeRespond call to the client and freeing the DDEINIT memory object from
the WinDdeInitiate call. Non-DDE applications don't respond to the
WM_DDE_INITIATE message and the shared memory object is released by the
system default winproc.

Figure 11 also illustrates how the DDESTRUCT is allocated, passed, and
released by the client and server applications. Figure 12 is a diagrammatic
view of DDEINIT processing.

Multiclient/Server Model

Earlier we discussed the single client/single server DDE conversation model.
It may be possible, and in a multitasking environment such as OS/2 PM it is
likely, that 1) server applications may have to support multiple clients and
2) multiple clients can receive data simultaneously from multiple servers.
Likewise, there may be little or no distinction between whether the client
or server application is initiated first. In a multiclient/multiserver
application relationship, multiple clients and server applications can be
invoked in any conceivable order, with virtually any number of permutations.
The real power of DDE to manage conversations efficiently becomes apparent
in implementing multiclient/multiserver relationships.

In managing an N-way conversation, the client, the server, or both
applications may be involved in a one-to-many conversation. That is, a
single server may be supplying data to multiple client applications, a
client application may be receiving data from several servers, or both of
these things may be happening.

The DDE convention for managing one-to-many conversations is for the
managing application to open a window--the conversational DDE
window--for each conversation and to process the messages in a generic
winproc for each conversation based on the conversational DDE window handle.
There are several benefits associated with managing the conversations based
on a window handle assigned to the conversation.

First, the individual conversation window handles serve as an ID for the
conversation, which is guaranteed by the operating system to be unique.
Second, conversations can easily be maintained and manipulated by using PM
API calls (for example, WinEnumerateWindow) without having to develop
specific data structures, such as linked lists, to keep track of
conversations. Third, it is possible to easily store and retrieve
conversational specific data in window words created with each
conversational DDE window. We have created one additional window in each DDE
application to facilitate DDE processing by our applications.

Each of our DDE applications creates a DDE anchor  window, which imposes an
artificial one-layer window hierarchy on the application window structure,
isolating all the DDE windows under a single parent window. This lets us
search through the DDE conversations directly using WinEnumerateWindow
without having to search all application children windows for DDE
conversations. The DDE conversation windows are normally traversed to locate
information specific to a single conversation or during termination
processing when all the links of a particular application are being
terminated. Figure 13 illustrates the parent/child relationship of DDE
windows in our applications.

When either the server or the client can initiate the conversation, as can
be done in a multiclient/multiserver application, a client/server
relationship that is consistent with the single client/single server DDE
conversation model should be maintained. An example of this situation is a
newly invoked server application participating in an existing client/server
conversation.

When a server is invoked during execution of a client, the server must
signal the client that it is willing to participate in a conversation. For
our signal, we have chosen to send the WM_DDE_INITIATE message with a
predefined application name. The client responds to the server's initiate
message with an initiate message of its own, causing the server to respond
with a WinDdeRespond message, as would be done under the single
client/single server model.

Graphics Exchange Program

DDE extensions for the multiclient/multiserver model are implemented in the
sample graphics exchange program. For sample purposes, the client
application exists merely to display graphical pictures representing the
running server applications. Server applications may differ greatly in the
function provided, but they all use DDE to transfer their graphics data to
the client. In our model, the client always establishes a permanent data
link with the server to receive updates as the state of the server
application warrants a change in the picture's appearance. We have chosen
Graphics_Exchange as the topic name  for this example.

The server provided in the example is simulating a phone messaging service.
As phone messages arrive, they are listed in the main window of the server
application. Each time a message is added, the picture is updated so that it
reflects the current number of phone messages, and the client is posted a
WM_DDE_DATA message. In order to limit the complexity of the server code
presented, phone messages are generated through the use of the timer. At
regular intervals, phone messages are added or deleted from the list. This
lets us focus our attention on the DDE implementation in the program rather
than on the function behind the server application.

In Figure 14 the server application has been invoked and is generating phone
messages. When the client application is invoked, it broadcasts the
WM_DDE_INITIATE message and the server responds. Note that the application
string is zero length to indicate that the client will talk to any
application that will respond to the topic of Graphics_Exchange. Figure 15
shows the screen after the conversation has been linked and the first
picture has been transferred to the client application.

Upon invocation of a second instance of the server application, the server
must signal the client that it has begun execution. In our case, the server
broadcasts the WM_DDE_INITIATE message using the same topic but specifying a
target application of Client. This indicates that the initiate is a special
case in which the server is signaling its invocation to any client
applications that may want to subsequently initiate a conversation. Upon
receiving this message, the client application broadcasts another
WM_DDE_INITIATE message. If the application string name were zero length,
however, the client would inadvertently establish a second link with the
original server application. To prevent this occurrence, the client
specifies an application name, which is simply the handle of the new server
converted to a string name. The newly invoked server checks the application
name and responds because the application name matches its handle. Figure 16
illustrates the message flow in establishing the second link.

This signaling convention establishes several rules for initiation of a
Graphics_Exchange conversation and the subsequent response. The client
application must broadcast a   WM_DDE_INITIATE upon invocation with the null
application string. It must also be prepared to receive a WM_DDE_INITIATE
message as a signal of a newly invoked server. If the application name
Client is present in this message, then the client must again broadcast a
WM_DDE_INITIATE, this time with an application string containing the handle
of the new participant. Figure 17 contains the initiate processing for the
Graphics_Exchange client application.

The server must respond to a WM_DDE_INITIATE of topic Graphics_Exchange if
and only if the application string name is zero  length or if it contains
the string representation of the server's window handle. Any other
application string name should be ignored. If the server responds to the
conversation, it creates a window to handle all future processing of the new
link and responds to the client using this window handle. Note that a window
word containing the conversation link count is incremented at this time.
This counter will be used later by the server to determine when shutdown may
occur. Figure 18 shows the response processing in the Graphics_Exchange
server application.

Upon receipt of the WM_DDE_INITIATEACK message, the client application
creates a window to process the link. Just as the server did, the client
increments a conversation link count stored in its window word. This will be
used during TERMINATE processing. The WM_DDE_REQUEST and WM_DDE_ADVISE
messages are posted to the server on behalf of the newly created
conversation window. Figure 19  illustrates this processing.

The primary activity of the server window is the packaging of the data and
posting of the WM_DDE_DATA message. In the sample program, this activity
always occurs during WM_DDE_REQUEST processing. That is, even when the main
server application determines that data should be transferred as a result of
an ongoing ADVISE, the result is the posting of a WM_DDE_REQUEST by the main
application to all of the conversation windows on behalf of the applications
being advised. The format of the data message is a user-defined data format
that was registered as discussed previously.

This data format is actually a data structure, GDEDATA, which is used as the
input structure for a control that manipulates the graphics for the client
application. Upon receiving a WM_DDE_REQUEST, the server allocates memory
for the DDESTRUCT and the underlying data structure and fills in the
necessary data fields required by the client and its control. The actual
graphics may be deposited in either bitmap or GPI drawing orders format and
included in the memory object. In the sample, they are always deposited as
drawing orders. The whole data package is then transmitted via the
WinDdePostMsg call.  The complete processing of the WM_DDE_REQUEST message
is shown in Figure 20.

Figure 21 shows how the main server application generates WM_DDE_REQUEST
messages on behalf of all advised clients when the data changes. This is
done by enumerating all child windows of the anchor window handle and
posting the message to those windows that are advising a client application.
The advise status of a server application is stored in the window word of
the conversation window procedure. Figure 22 illustrates the setting of this
status field upon receiving either the WM_DDE_ADVISE or the WM_DDE_UNADVISE
message.

The data format for a Graphics_Exchange conversation is simply the input
structure to a control, therefore the WM_DDE_DATA processing by the client
is no more than an insertion or replacement of the data structure into the
control. If the DDE_FRESPONSE bit is set, the client knows that the
WM_DDE_DATA message is the result of the initial WM_DDE_REQUEST that was
made after the conversation was linked. Since this is the first transmittal
of data from the server, the picture must be inserted into the graphics
control. If the DDE_FRESPONSE bit is not set, then the WM_DDE_DATA message
is the result of an ongoing advise and the client must replace the picture
in the control with the new data.

The control messages themselves are quite simple. The first parameter
identifies the ID of the picture in question, and the second points to the
structure describing and containing the picture. The internal details of the
control are not relevant; the control manages the appropriate sizing and
placement of the graphics data. The complete WM_DDE_DATA processing is shown
in Figure 23.

During execution of the participating applications, the WM_DDE_DATA messages
will be sent each time the timer triggers the delivery or removal of a phone
message. The DDE conversation will continue until either application
terminates the conversation. In our example, the conversation is only
terminated when the user attempts to close either application. The WM_CLOSE
processing as well as the subsequent WM_DDE_TERMINATE processing follow the
same algorithm in both the client and server applications.

When a WM_CLOSE message is received, the application enumerates all DDE
conversation windows (by enumerating on the anchor window). For each
conversation window, a window word is set to indicate that the user has
requested a shutdown of the application. At the same time, another window
word is queried to determine the handle of the conversation window with
which the enumerated window is exchanging data. That window is posted a
WM_DDE_TERMINATE message by the main application on behalf of the
conversation window. No further shutdown processing will take place until
all conversation links have terminated. Figure 24 shows the WM_CLOSE
processing by the client application. Note that the server code is very
similar.

When it receives the WM_DDE_TERMINATE message, a conversation window checks
its window word to determine if the close was initiated by its own
application or by its conversation partner. If the close was initiated by
the conversation partner, the window posts a corresponding WM_DDE_TERMINATE
to the sender. If the close was not initiated by the conversation partner,
the message is simply an acknowledgment by the partner that terminate
processing may continue. In either case, the conversation window may now
destroy itself, since its conversation link is ending. At this time, it
posts an application-defined message, called APPM_CONV_CLOSE, to indicate to
the main window that the link has successfully terminated. Figure 25 shows
the WM_DDE_TERMINATE processing by the client. Again, the algorithm is the
same in the server application.

The APPM_CONV_CLOSE message serves as the signal that final shutdown may
occur. As each APPM_CONV_CLOSE message is received, the link counter is
decremented. When the link counter reaches zero, the main application checks
its window word to determine whether a close was requested for the
application. If close was requested and all links have successfully
terminated, the application may complete its shutdown and terminate (see
Figure 26).

Graphics exchange provides an extremely visual working example of the use of
DDE to establish permanent links between separate PM applications. Support
for multiple conversation management is added simply by defining one
additional message and making extensive use of the window word and window
enumeration facilities. Of course, these applications may be expanded to
exchange other data in addition to the graphics representations exchanged in
this program.

By using our example as a guide, you should be able to generalize the
implementation presented in order to develop your own multitasking
interprogram data exchange. We have found DDE to be a powerful and flexible
element of Presentation Manager, providing an added dimension to developing
interacting applications in the OS/2 multitasking environment. As the
programming environment has evolved from Windows to the extensive
capabilities of the OS/2 Presentation Manager, the changes made to the DDE
protocol have kept pace with this new function. Moreover, it has provided a
framework for further expansion as the environment continues to evolve.

Figure 17

case WM_CREATE:  /* broadcast the initiate message  */
     WinDdeInitiate(hwnd, "", "Graphics_Exchange");
     break;

case WM_DDE_INITIATE: /* reply with an initiate to specific server */
   if (hwnd != lParam1) {
       if ((!strcmp("Client", DDEInitPtr->pszAppName)) &&
           (!strcmp("Graphics_Exchange",DDEInitPtr->pszTopic))) {
           itoa((int)lParam1, itoa_buf, 10);
           WinDdeInitiate(hwnd, itoa_buf, "Graphics_Exchange");
       }
   }
   DosFreeSeg(PDDEITOSEL(DDEInitPtr));

break;

Figure 18

case WM_DDE_INITIATE:  /* respond if app and topic strings match */
     pDDEInit = (PDDEINIT)lParam2;

   /* check for null strings or Graphics_Exchange */
   if ((HWND)lParam1 != hwnd) {
       itoa((int)hwnd, szTemp, 10);
       if(((!strlen(pDDEInit->pszAppName)) &&
           (!strcmp("Graphics_Exchange", pDDEInit->pszTopic))) ||
           ((!strcmp(szTemp, pDDEInit->pszAppName)) &&
           (!strcmp("Graphics_Exchange", pDDEInit->pszTopic))) ||
           ((!strlen(pDDEInit->pszTopic)) &&
           (!strlen(pDDEInit->pszAppName))))    {

           /* create conversation window and respond */
           hwndConv = WinCreateWindow(hwndDDE,
                                      (PSZ)"DDEConversation",
                                      (PSZ)NULL, WS_VISIBLE,
                                      0,0,0,0, hwnd, HWND_TOP,
                                      ++nConvID, (PVOID)NULL,
                                      (PVOID)NULL);
           pWWVar->ConvCnt++;
           WinDdeRespond(lParam1, hwndConv, "Client",
                         "Graphics_Exchange");
        }
     }
     DosFreeSeg(PDDEITOSEL(pDDEInit));

break;

Figure 19

case WM_DDE_INITIATEACK:  /* establish conversation with server */
     if( ((!strcmp("Client", DDEInitPtr->pszAppName)) &&
          (!strcmp("Graphics_Exchange", DDEInitPtr->pszTopic))) ||
        ((!strlen(DDEInitPtr->pszAppName)) &&
          (!strcmp("Graphics_Exchange",DDEInitPtr->pszTopic)))) {

         /* create a window for the conversation  -
            child of DDEanchorHWND  */

         DDEconversationHWND = WinCreateWindow(hwnd, (PSZ)"DDE_Win",
                                (PSZ)NULL, WS_VISIBLE, 0, 0, 0, 0,
                                WinQueryWindow(hwnd,QW_PARENT,FALSE),
                                HWND_TOP, ++winDDEid, (PVOID)NULL,
                                (PVOID)NULL);
         WinSetWindowULong(DDEconversationHWND, WW_CONV_HWND,
                           (ULONG)lParam1);
         WinSetWindowULong(WinQueryWindow(hwnd, QW_PARENT, FALSE),
                           WW_CONVCOUNT,
                           WinQueryWindowULong(
                               WinQueryWindow(
                                   hwnd,QW_PARENT,
                                   FALSE),
                           WW_CONVCOUNT)+1);

         /* send a request for initial data */
         DDEstrptr = st_DDE_Alloc(sizeof(DDESTRUCT) +
                       strlen("Graphics")+1,"DDEFMT_graphics_data");
         DDEstrptr->offszItemName = (USHORT)sizeof(DDESTRUCT);
         strcpy(DDES_PSZITEMNAME(DDEstrptr), "Graphics");
         WinDdePostMsg((HWND)lParam1, DDEconversationHWND,
                       (ULONG)WM_DDE_REQUEST, DDEstrptr, TRUE);

         /* send an advise to subscribe to
            receive future data updates */

         DDEstrptr = st_DDE_Alloc(sizeof(DDESTRUCT) +
                       strlen("Graphics")+1,"DDEFMT_graphics_data");
         DDEstrptr->offszItemName = (USHORT)sizeof(DDESTRUCT);
         strcpy(DDES_PSZITEMNAME(DDEstrptr), "Graphics");
         WinDdePostMsg((HWND)lParam1, DDEconversationHWND,
                       (ULONG)WM_DDE_ADVISE, DDEstrptr, TRUE);
     }
     /* free the memory */
     DosFreeSeg(PDDEITOSEL(DDEInitPtr));

break;

Figure 20

case WM_DDE_REQUEST: /* allocate DDESTRUCT and dump graphics data */
     pDDEStruct = (PDDESTRUCT)lParam2;
     strcpy(szTemp, "Graphics");
     if((pDDEStruct->usFormat == pWWVar->usFormat) &&
         (!strcmp(szTemp, DDES_PSZITEMNAME(pDDEStruct)))) {
          if(!pWWVar->bNoData) {
             nNumBytes = (strlen(szTemp) + 1 + sizeof(GDEDATA) +
                          LOUSHORT(lPhFigCnt));
             pDDEStruct = st_DDE_Alloc((sizeof(DDESTRUCT) +
                           nNumBytes), "DDEFMT_graphics_data");
             pDDEStruct->cbData = sizeof(GDEDATA) + lPhFigCnt;
             pDDEStruct->offszItemName = (USHORT)sizeof(DDESTRUCT);
             pDDEStruct->offabData = (USHORT)((sizeof(DDESTRUCT) +
                           strlen(szTemp)) + 1);
             pGDEData = (PGDEDATA)DDES_PABDATA(pDDEStruct);
             st_Init_GDEData(pGDEData);
             pGDEData->cBytes = lPhFigCnt;
             strcpy(pGDEData->szItem, "phone");
             pGDEData->pGpi = (unsigned char far *)((LONG)pGDEData +
                               sizeof(GDEDATA));
             GpiGetData(hpsGraphics, (LONG)IDSEG_PHONE,
                        (PLONG)&lOffset, DFORM_NOCONV,
                        (LONG)lPhFigCnt, (PBYTE)pGDEData->pGpi);
             memcpy(DDES_PSZITEMNAME(pDDEStruct), szTemp,
                    (strlen(szTemp) + 1));
             if(lParam1 != WinQueryWindow(hwnd, QW_OWNER, FALSE)) {
                pDDEStruct->fsStatus |= DDE_FRESPONSE;
                pWWVar->hwndLink = (HWND)lParam1;
             }
             WinDdePostMsg(pWWVar->hwndLink, hwnd, WM_DDE_DATA,
                           pDDEStruct, TRUE);
          }
          else {
             WinDdePostMsg(pWWVar->hwndLink, hwnd, WM_DDE_DATA,
                           NULL, TRUE);
          }
          DosFreeSeg(PDDESTOSEL(lParam2));
     }
     else {  /* post negative ACK using their DDESTRUCT */
          pDDEStruct->fsStatus &= (~DDE_FACK);

WinDdePostMsg(lParam1, hwnd, WM_DDE_ACK, pDDEStruct, TRUE);

}

break;

Figure 21

case WM_TIMER:  /* insert phone message into listbox,
                   update picture, and generate new
                   REQUESTS for all ADVISING windows */
     .
     .  /* listbox and picture have been updated */
     .
     hEnum = WinBeginEnumWindows(hwndDDE);
     while((hwndEnum = WinGetNextWindow(hEnum))) {

pWWChild = (PWWVARS)WinQueryWindowULong(hwndEnum, QWL_USER);

if (pWWChild->bAdvise) {
            pDDEStruct = st_DDE_Alloc(sizeof(DDESTRUCT) +
                          strlen("Graphics")+1,
                          "DDEFMT_graphics_data");
            pDDEStruct->offszItemName = (USHORT)sizeof(DDESTRUCT);
            strcpy(DDES_PSZITEMNAME(pDDEStruct), "Graphics");
            WinDdePostMsg(hwndEnum, hwnd, WM_DDE_REQUEST,
                          pDDEStruct, TRUE);
        }
        WinLockWindow(hwndEnum, FALSE);
     }
     WinEndEnumWindows(hEnum);

break;



Figure 22
case WM_DDE_ADVISE:  /* set ADVISE bit in window data and ACK */
     pDDEStruct = (PDDESTRUCT)lParam2;
     if(pDDEStruct->usFormat == pWWVar->usFormat) {
        pWWVar->hwndLink = (HWND)lParam1;
        pWWVar->bAdvise = TRUE;
        if(pDDEStruct->fsStatus & DDE_FNODATA) {
          pWWVar->bNoData = TRUE;
        }
        if(pDDEStruct->fsStatus & DDE_FACKREQ) {
           pDDEStruct->fsStatus |= DDE_FACK;
           WinDdePostMsg(pWWVar->hwndLink, hwnd, WM_DDE_ACK,
                         pDDEStruct, TRUE);
        }
        else {
           DosFreeSeg(PDDESTOSEL(lParam2));
        }
     }
     else {    /* Send a negative ACK using their DDEStruct */
        pDDEStruct->fsStatus &= (~DDE_FACK);
        WinDdePostMsg(lParam1, hwnd, WM_DDE_ACK, pDDEStruct, TRUE);
     }
     break;

case WM_DDE_UNADVISE:/* turn off ADVISE bit in window data and ACK */

pDDEStruct = (PDDESTRUCT)lParam2;
   if((lParam1 == pWWVar->hwndLink) &&
       (pDDEStruct->usFormat == pWWVar->usFormat)) {
        pWWVar->bAdvise       = FALSE;
        pWWVar->bNoData       = TRUE;
        pDDEStruct->fsStatus |= DDE_FACK;
        WinDdePostMsg(lParam1, hwnd, WM_DDE_ACK, pDDEStruct, TRUE);
     }
   else {
        pDDEStruct->fsStatus &= (~DDE_FACK);
        WinDdePostMsg(lParam1, hwnd, WM_DDE_ACK, pDDEStruct, TRUE);
   }

break;



Figure 23

DDEstrptr = (PDDESTRUCT)lParam2;

switch (message){

   case WM_DDE_DATA:   /* process incoming picture */
       gde_ptr = (PGDEDATA)DDES_PABDATA(DDEstrptr);
       gde_ptr->hwnd_idItem = LOUSHORT(hwnd);

       /*  add if request  * /
       if (DDEstrptr->fsStatus && DDE_FRESPONSE) {
           WinSendMsg(WinWindowFromID(
                      WinQueryWindow(hwnd, QW_OWNER, FALSE),
                      ID_GRAPHICS1),IC_INSERTITEM,
                      MPFROMSHORT(ICM_END), (MPARAM)gde_ptr);
       } else {  /* replace if advise */
           WinSendMsg(WinWindowFromID(
                      WinQueryWindow(hwnd,QW_OWNER,FALSE),
                      ID_GRAPHICS1),IC_SETITEMSTRUCT,
                      MPFROMSHORT(gde_ptr->hwnd_idItem),
                      (MPARAM)gde_ptr);
       }

       if (DDEstrptr->fsStatus && DDE_FACKREQ) {
           DDEstrPtrAck = st_DDE_Alloc(sizeof(DDESTRUCT),
                           "DDEFMT_graphics_data");
           WinDdePostMsg((HWND)lParam1, hwnd, (ULONG)WM_DDE_ACK,
                           DDEstrPtrAck, TRUE);
       }

       DosFreeSeg(PDDESTOSEL(DDEstrptr));

break;

Figure 24

/* send WM_DDE_UNADVISE, shutdown all conversations
   for this application, then quit */
case WM_CLOSE:
   if (WinQueryWindowULong(hwnd, WW_CONVCOUNT)) {
       WinSetWindowULong(hwnd, WW_CLOSE,
                         WinQueryWindowULong(
                          hwnd, WW_CLOSE) | WIN_CLOSING_FLAG);
       henum = WinBeginEnumWindows(DDEanchorHWND);
       while (hwndenum = WinGetNextWindow(henum)) {
           WinSetWindowULong(hwndenum, WW_CONV_FLAGS,
                     WinQueryWindowULong(hwndenum, WW_CONV_FLAGS) |
                     WIN_TERM_FLAG);

tohwnd = (HWND)WinQueryWindowULong(hwndenum,WW_CONV_HWND);

DDEptr = st_DDE_Alloc(sizeof(DDESTRUCT) +
                       strlen("Graphics")+1,"DDEFMT_graphics_data");
           DDEptr->offszItemName = (USHORT)sizeof(DDESTRUCT);
           strcpy(DDES_PSZITEMNAME(DDEptr), "Graphics");
           WinDdePostMsg(tohwnd, hwndenum, WM_DDE_UNADVISE,
                         DDEptr, TRUE);
           WinDdePostMsg(tohwnd, hwndenum, WM_DDE_TERMINATE,
                         NULL, TRUE);
           WinLockWindow(hwndenum, FALSE);
         }
         WinEndEnumWindows(henum);
     }
     else {
         WinPostMsg(hwnd,WM_QUIT,0L,0L); /* quit if no
                                            conversations open  */
     }

break;

Figure 25

/* post terminate to server, tell client, and die */
case WM_DDE_TERMINATE:

if (!WinQueryWindowULong(WinQueryWindow(hwnd, QW_OWNER, FALSE),

WW_CLOSE)) {
       WinDdePostMsg((HWND)lParam1, hwnd, WM_DDE_TERMINATE,
                     NULL, TRUE);
   }
   WinPostMsg(WinQueryWindow(hwnd,QW_OWNER,FALSE),
              APPM_CONV_CLOSE,MPFROMLONG(hwnd),(MPARAM)NULL);
   WinDestroyWindow(hwnd);

break;

Figure 26

/* decrement conversation count and delete picture */
case APPM_CONV_CLOSE:
   WinSetWindowULong(hwnd,WW_CONVCOUNT,
                     WinQueryWindowULong(hwnd, WW_CONVCOUNT) - 1);
   WinSendMsg(WinWindowFromID(hwnd,ID_GRAPHICS1),IC_DELETEITEM,
                              (MPARAM)LOUSHORT(lParam1),
                              (MPARAM)NULL);
   if (WinQueryWindowULong(hwnd,WW_CLOSE) &&
       !WinQueryWindowULong(hwnd, WW_CONVCOUNT)){
           WinPostMsg(hwnd,WM_QUIT,0L,0L);
   }

break;




Creating a Virtual Memory Manager to Handle More Data in Your Applications

 Marc Adler

The amount of memory that is available to a program under the Microsoft(R)
OS/2 operating system is beginning to spoil many programmers. For example,
when Magma's ME Programmer's Text Editor (not to be confused with the
Microsoft Editor) was ported to OS/2, one of the advantages was the ability
to easily edit files larger than the available memory. Going back to the DOS
version of the editor, with its limited file size, became very difficult. To
satisfy the desire to edit very large files under DOS, the DOS version of ME
had to be enhanced. The logical way to do that was to design and build a
virtual memory manager (VMM) that could handle the demand. Figure 1 lists
the APIs for the VMM.

The motivation for writing a virtual memory manager was enhanced by a desire
to overcome the shortcomings of malloc, the staple function of the C
run-time library. Different compiler manufacturers have implemented the C
memory allocation functions in different ways, each implementation being
equally mysterious to the average programmer.

From a programming perspective, it is advantageous to take the mysteries out
of malloc and to put the inner workings of a memory management function at
programmers' fingertips, to be tinkered with in special situations and to be
traced meaningfully with the Microsoft CodeView(R) debugger. Being able to
do such tracing and tinkering might be very useful, for example, if one felt
that a program had corrupted a chain of allocated blocks of memory.

Finally, there was an overriding desire to ensure that ME would never again
be limited by the amount of free memory left in a given system.
Unbelievably, there are still people who are using 256Kb machines. The
amount of memory that DOS itself takes up combined with both the memory used
by terminate-and-stay-resident (TSR) programs and the huge size of the EXE
files that make up today's major applications, severely limits the amount of
space that can be allocated, even with 640Kb of memory.

One specific goal in designing the virtual memory manager for the ME text
editor was to keep it simple enough and general enough so that it could be
used in any other application that needed memory management beyond what
malloc offers. Since many people have installed additional memory boards in
their systems, it was also desirable to be able to exploit the capabilities
of expanded memory in the virtual memory manager. (Even though ME's virtual
memory manager supports EMS, its implementation won't be covered here. Such
an implementation, however, would be useful as an exercise for the reader.)

The VMM must be compiled in large model. The reason is that we must remain
consistent with the far pointer that we use and the pointers that some of
the C library routines need (such as memset). A little work needs to be done
if you would like to use the VMM under a different memory model. (The actual
VMM source code listings, VM.H and VM.C, are available on MSJ's bulletin
boards and are not included here due to space constraints--Ed.)

Interspersed throughout the text will be comments about possible extensions
you could implement to make the VMM more powerful. If you decide to
implement any of the suggestions, I'd be interested in receiving changes,
along with your comments about how they affected the performance of the VMM.
They should be forwarded to the Technical Editor of Microsoft Systems
Journal. (Interesting additions and comments may be published by MSJ in a
future issue.--Ed.)

Figure 1

 VMInit

Initializes the VMM. Must be called at the beginning of the application
before any memory is requested.

  VMTerminate

Shuts down the VMM. Call it at the end of your application.

  char far *MemDeref(HANDLE h)

Dereferences the object pointed to by handle h and returns the memory
address of that object.

  HANDLE MyAlloc(unsigned size)

Allocates size bytes from the VMM and returns a handle to that block. The
block is also filled with zeros.

  MyFree(HANDLE h)

Frees the memory block pointed to by handle h.

  SetVMPageSize(int kbytes)

Sets the default page size to kbytes kilobytes. For instance,
SetVMPageSize(16) sets the default page size to 16Kb.

  MakePageDirty(HANDLE h)

Sets the dirty bit of the page that contains the memory block referenced by
handle h.

Initializing the VMM

The first thing every program that uses the virtual memory manager must do
is to initialize it. This is done simply by calling the function VMInit. The
main job of VMInit is to create the swap file that is used when blocks of
memory must be paged out to disk. VMInit will first check to see if the user
defined an environment variable called METEMP. METEMP should contain the
path name of where the swap file should go. If the METEMP variable is not
defined, the swap file will be created in the current directory. The use of
a user-definable destination for the swap file allows the user to take
advantage of any RAMdisks that might be available. Swapping to a RAMdisk, of
course, is significantly faster than swapping to a hard disk.

You can set the METEMP variable with a line in your AUTOEXEC.BAT file that
looks like this :

set METEMP=<swap path>

To create the name of the swap file, you use the mktemp function, which is
part of the C run-time library. This function takes a single parameter, a
string representing a file name template, and creates a unique file name
from that template. In this case, the template that is used is VMXXXXXX. The
mktemp function will replace the uppercase Xs with characters that would
make the file name unique in the current directory. For instance, mktemp
might return the string VM065291 to VMInit, and we would use that as the
name of our swap file.

At this point, I must confess that the virtual memory manager has one major
limitation; the swap space is bounded by the available space on the swap
disk (plus the amount of expanded memory that is free). A possible extension
would be to allow the VMM to use multiple volumes when swapping, including
swapping over a network to a totally different computer (such as a remote
file server). If you do this, beware of the DOS limitation on the number of
files that an application can have open simultaneously.

Terminating the VMM

Before terminating, an application must call VMTerminate to do some cleanup
work. VMTerminate simply closes the swap file and deletes it. If you added
routines to do performance analysis or to swap to multiple volumes, you
might need to do some cleanup work at this point.

Obtaining Memory

The function that obtains memory from the VMM and returns it to the caller
is MyAlloc. It replaces the standard call to malloc. Since the memory
returned is zeroed out, MyAlloc can replace calloc as well. A single
argument is passed to MyAlloc--the number of bytes needed--and a
handle is returned.

The word handle is becoming an increasingly popular term, mainly because of
its frequent usage in Microsoft Windows. A handle is simply an identifier
that is associated with a block of memory; it is used by the internal
routines to identify an object. In the true spirit of data hiding, the value
of a handle (also referred to as a magic cookie) will typically have no
meaning to the application that uses the VMM.

In the case of our VMM, a handle is an unsigned long quantity comprised of
two parts. The upper 16 bits is the page number in which the block of memory
is found, and the lower 16 bits constitutes the zero-based offset from the
beginning of that page. For example, a handle whose value is 00030400H
signifies that the memory block is located 400H bytes away from the
beginning of the block allocated for page 3. Figure 2 illustrates this
example.

This design allows us to have at most 64Kb pages, each page containing as
many as 64Kb of memory. Thus our VMM can access a total of more than 4Gb,
well over the maximum size of the largest hard disk.

The Structure of a Page

As I stated earlier, MyAlloc expects a single argument that represents the
amount of memory we want to allocate from the VMM. If we cannot find a page
with enough free memory, a new page will be allocated. At this point, let's
see what is in the PAGE data structure.

You'll find the declaration of the PAGE data structure (shown in Figure 3)
in the listing of VM.H. Since a block can reside anywhere in RAM, we need a
field to hold its far address. A block can reside on disk, so we also need a
field to hold the offset from the beginning of the swap file where the block
is found. These two fields are called memaddr and diskaddr.

In addition to these two fields, we have fields that contain the ID of the
page (a unique integer), the size of the page (in case we modify the VMM to
deal with different sized pages), a bit mask to represent the status of the
page (if it's in memory, on disk, or both, and whether the page is dirty),
the offset to the first free byte in the page (used for implementing a
linked list of free blocks within the page), and the clock for the least
recently used (LRU) swapping algorithm. We also have fields for the number
of free bytes within the page and the size of the largest free block of
contiguous memory. These two fields can be used in conjunction, in the event
we modify the VMM to do true compaction of free blocks.

The amount of space allocated for a page should be a power of 2. With
careful experimentation, you might optimize the performance for your
application. A routine known as SetVMPageSize is provided that allows the
application to set the page size from within your application. It should be
called directly after VMInit. If you decide to alter the default size of a
page, then you must call SetVMPageSize only once within your application,
and the call should be made before any pages are actually allocated. The
reason for this is that the swapping algorithm thinks that each page is the
same size. We use a default size of 4Kb for each page; however, in the
version that uses expanded memory, the page size is increased to 16Kb to
match the size of an expanded memory page.

Within each page, a linked list of the free blocks within that page is
maintained. This list is defined by the FREEINFO structure. Each free block
(and allocated block) has a header that records the number of bytes in the
block and the offset to the next free block in the page. A block that has
the value of FFFFH in its offset field is the last block in the chain. This
linked list is a simple one with no implicit ordering of blocks. We will
talk about enhancing this list when we discuss the freeing of blocks. Figure
4 shows an example of a typical chain of free blocks.

When MyAlloc is called, we search the page list for the first page with the
necessary amount of contiguous free bytes. The function that handles this
search is FindNContigBytesFree. Precedence is given to pages that are
already in memory, but if there are no pages in memory that contain the
needed bytes, we look for the first disk-based page that has the free space.
If there are no pages either on disk or in memory that have a sufficient
number of contiguous bytes free, we allocate a new page and return its
address. The AllocPage routine is responsible for allocating space for a new
page header and for the associated buffer.

Back in MyAlloc, we have a pointer to a page with the necessary number of
bytes free, and we are assured that the page is in conventional memory. We
then traverse the list of free blocks within that page and stop when we find
the first block with the necessary free bytes. The block is zeroed out and
the handle is returned to the calling routine.

Figure 3

typedef struct page
{
  struct page *next;            /* chain to next free page in list */
  char far *memaddr;            /* memory address of the page block */
  unsigned long diskaddr;       /* disk address of the page block */
  PAGEID   id;                  /* page identifier */
  unsigned long LRUcount;       /* least-recently-used count      */
  unsigned pagesize;            /* how many bytes is this page    */
  unsigned freebyte;            /* index of 1st free byte in page */
  unsigned bytesfree;           /* # of bytes free in this page   */
  unsigned maxcontigfree;       /* max # of contiguous free bytes */

  unsigned flags;
#define PAGE_IN_MEM     0x0001
#define PAGE_ON_DISK    0x0002
#define IS_DIRTY        0x0004
#define SET_PAGE_DIRTY(p)  ((p)->flags |= IS_DIRTY)
#define NON_SWAPPABLE   0x0008
#define PAGE_IN_EMM     0x0010
} PAGE;



Swapping Pages

In this version of the VMM, a call is made to the Microsoft C library
routine, _dos_allocmem, to allocate memory for a page. This routine is
really a front end for the main DOS memory allocation service (Int 21H,
function 48H). Using the DOS memory functions lets us totally bypass the
malloc family found in the C run-time library. (Actually, each page header
is allocated using malloc, but we can choose to use DOS memory for this if
we want.) We continue to use _dos_allocmem to grab space for a page until we
run out of memory. At this point, we have a page header allocated for the
new page but no block of memory allocated for its data. What we need to do
is borrow the memory used by a previously allocated page. But before that
can be done, we must save that page's data somewhere. Then the new page can
use that page's block of memory to store its own data in. This process is
called swapping or paging.

How do we know just where in the swap file the old page's contents should be
stored? The array VMFile.slottable contains a map of which sectors of the
swap file are used by which pages. A NULL entry for a sector means that the
sector is free. When we swap a page to disk for the first time, we search
the slot table for the first NULL entry and then write the page to the
corresponding sector.

The remaining question is, How do we determine which page to swap out to
disk? If we have a bad algorithm for choosing the swappable page, we can run
into a hideous phenomenon know as thrashing. If a VMM thrashes, it is
spending an inordinate amount of time swapping pages between disk and
memory. That can happen if we choose to swap out a frequently referenced
page.

For our swapping algorithm, we use the old, time-tested least recently used
algorithm. In order to implement the algorithm, we must keep a clock which
is incremented every time a page is accessed. Each page has a variable which
records the time when it was last accessed. To find the LRU page, we just
scan the page list for the page with the minimum clock time and return a
pointer to that page.

A possible enhancement to the VMM is to implement a means of making certain
important pages nonswappable. For instance, instead of using malloc to
allocate the headers for each page as we do now, we could allocate memory
from the VMM for them. However, it would be disastrous if the page headers
were swapped out to disk! In this case, we might use a nonswappable page to
hold the headers and other important system information. The LRU algorithm
would require a simple modification that would tell it not to consider pages
marked as nonswappable.

Another possible enhancement is to keep the list of pages in a doubly linked
list rather than a singly linked one and to maintain a pointer to the tail
of the list. Since we move a page to the head of the page list whenever we
access that page, we will be guaranteed that the least recently used page
will be at the tail of the list. Using this method, we find the LRU page
merely by looking at the tail; we don't need to implement a clock for the
LRU algorithm, and we don't have to allocate a counter for each page in the
system.

A Perfect Fit?

The method of allocation followed here is called the first fit approach.
It's given that name because we stop our search at the first block that fits
the criterion, that is, the first block that has enough contiguous bytes
free. Another popular approach is called best fit; we traverse the entire
free list in search of a block with the smallest size that satisfies our
criterion. A third method is called next fit; we remember the position the
last block that was allocated came from, and the next time we search for a
free block, we start at that spot. The first fit method has the advantage of
taking less time to find the proper memory block, and best fit has the
advantage of reducing fragmentation (if you use one of the methods of
reducing fragmentation discussed below).

Fragmentation of memory is a major concern when designing a memory manager.
It is caused when you allocate part of a memory block that has more free
bytes than you really need: part of that block will be allocated and the
remainder will be put back onto the free list; however, if the remaining
block is too small for any subsequent allocation request, it may never be
allocated. For example, let's say that I need a block of 24 free bytes, and
after searching the chain of free blocks, I come to a block that has 32
bytes free. I will allocate 24 bytes out of that block and leave a block of
8 bytes on the free list. This free block is probably too small to satisfy
any future allocation request, so it will remain forever on the free list.
That may not seem so bad, except that the memory manager will still have to
examine the block whenever it searches the free list; multiply this example
by the thousands of memory requests that a typical application might make
and you'll see why fragmentation is a severe problem. Figure 5 illustrates a
graphic representation of fragmentation.

A simple way of solving fragmentation problems is to round off an allocation
request to a higher number; the excess bytes will be included in the free
block that we return to the application. For the example above, I might use
the simple heuristic of rounding off all allocations to the nearest power of
2. If I ask for 24 bytes, and I come upon a block of 32 bytes, then I will
return the entire block to the application. Although 8 bytes may never be
used, I will not have a small block floating about in the free list.

Another solution is to perform periodic garbage collection and periodic
compaction on the memory blocks, an approach we will also consider.

Dereferencing Handles

You will recall that the handle to a memory block is merely an identifier
that tells the VMM how to reference that block; the application does not
really know what memory address the handle points to. Once an application
obtains a handle to a memory object, it has to go through a dereferencing
step in order to use the memory block that the handle refers to.

Remember, too, that a handle is an unsigned long value that comprises the
page identifier and the offset within that page. The function MemDeref must
be called whenever you need to transform a handle into a memory address. For
example, if we wanted to copy the string XYZ into an allocated memory block,
we must write the following code:

#include "vm.h"

■
■
■

HANDLE h;
char far *s;

■
■
■


/* Note--this assumes
   large memory model */
if ((h = MyAlloc(4)) ==
    (HANDLE) NULL)
/* perform error
   processing */

■
■
■


if ((s = MemDeref(h)) !=
        NULL)
strcpy(s, "XYZ");

MemDeref simply breaks the handle into its constituent parts, searches for
the page with the ID contained in the handle, swaps the page into memory if
necessary, adds the base memory address of the page with the offset of the
block that the handle specifies, and returns a pointer to the memory
address. One nice thing that MemDeref also does is to move the referenced
page to the front of the linked list of pages that are currently in use. The
VMM assumes a locality of reference within an application--that is,
once a page is referenced, it will continue to be referenced in the
application's surrounding code. Moving the newly referenced page to the
front of the page list reduces the time needed to traverse the page list for
subsequent searches for that page.

Touching a Page

If you look carefully at the code for the WritePage function, you'll notice
that a page will get written to disk only if it's currently in memory and if
the page has been modified since it was allocated or last written to disk.
If the same image of a page exists both in memory and in the swap file,
there is no need to perform the actual write to disk.

The VMM includes a routine that sets the dirty bit of the page that contains
a referenced memory block. The MakePageDirty function takes a single
argument, the handle of a memory block, and searches for the page
corresponding to that block. When that page is located, its dirty bit is
set. This scheme ensures that when we modify a certain portion of memory,
the associated page will be swapped properly when memory begins to run low.

Referring to the example above, if we wanted to change the bytes pointed to
by handle h to the string ABC, we would need to do the following :

s = MemDeref(h);
strcpy(s, "ABC");
MakePageDirty(h);

Freeing a Memory Block

When a memory block is no longer needed, we call the MyFree routine to
release it back into the memory pool. MyFree takes a single
argument--the handle to the memory block that we will release. To
release a memory block, we simply place it on the page's linked list of free
blocks and increase the number of free bytes within that page. What makes
the operation a little more complicated is the fact that we would like to
examine the blocks adjacent to the newly freed block, and if they are free,
coalesce them with the newly freed block. When coalescing is done, our most
important statistic, the number of contiguous free bytes, also increases.

To increase how quickly we traverse the linked list of free blocks and to
help us examine adjacent blocks, we keep the list sorted by the blocks'
offsets. When we free a block, we traverse the list until we find a free
block whose offset is greater than that of the newly freed block. Then we
insert the newly freed block in the list before this block. Figure 6
illustrates a typical coalescing action.

The astute reader will notice that although the user can theoretically
request up to 64Kb of memory (the maximum value of the argument to MyAlloc),
the amount of memory requested is limited to the size of the page block. To
accommodate this limit, we can modify the VMM to dynamically modify the
basic page block size if a request comes in that is too large.

The Double Indirection Method

Earlier, we alluded to the fact that periodic compaction of the memory
blocks would reduce fragmentation in our VMM. Before compaction, the free
blocks are intermingled with the allocated blocks, and the free list must be
traversed in order to locate a block with the desired amount of contiguous
bytes free. After compaction, all the allocated blocks are pushed to one
side of the page, and a single large free block is created out of the
remaining space. When we look for a free block to allocate, there is only
one block to examine in the page.

Compaction presents one major problem, however. If we move an allocated
block to another position in memory, we must modify all our application's
variables that point to that block to point to the new place in memory. But
how does the memory manager know which variables point to that memory block?

To solve this problem, we use a scheme known as double indirection. Double
indirection has been made popular by the memory managers of both the
Macintosh(R) and of Microsoft Windows. When an application requests a block
of memory, we will return not a pointer to that block, but a pointer to a
pointer to that block. This is illustrated in Figure 7.

Figure 8 illustrates what happens when we compact a number of memory blocks.
Even though the blocks shift in memory, the pointers to the blocks remain
stationary. Since all our application knows about are the indirect pointers,
and since the position of those pointers doesn't change, the memory manager
does not have to alert the application when the compaction operation occurs.
Everything is totally invisible to the application. (Actually, in Microsoft
Windows Version 2.x, an application can choose to be alerted when allocated
blocks are moved by specifying the GMEM_NOTIFY flag in the call to
GlobalAlloc.)

As you can see from the diagrams in Figures 7 and 8, the double indirection
method requires one extra memory reference in order to dereference a memory
handle. However, with the speed of modern day CPUs increasing yearly, the
extra memory reference is not as much of a problem as it used to be. If you
consider the reduction of time involved in searching  the free list, and
also take into account the elimination of fragmentation, you'll see why the
designers of Windows chose the doubly indirect way of doing these things.

The McBride Allocator

Let's take a brief look at another virtual memory management package, VMEM
by Blake McBride. VMEM (see Figure 9), which also comes with full source
code, uses the double indirection method to achieve high performance.
Although it does not support expanded memory at this time, it compensates
for this deficiency by allowing compaction of the swap file. It also uses
one of the enhancements that was mentioned above--it maintains the
virtual memory objects on a doubly linked list so that there is always a
pointer to the least recently used object.

The user has a choice of two methods of swap file compression. Using the
first method, all objects are moved to the beginning of the swap file, so
that one large hole remains at the end. The second method uses two swap
files; all allocated objects are copied from the first swap file to a newly
created second swap file, then the original swap file is deleted. Although
both methods produce the same results, each method has an important
advantage. With the single file method, even though the allocated blocks
have been moved, the size of the swap file will never decrease. With the
dual-file method, you need twice the disk storage during the compression
operation.

VMEM also has the ability to save and restore entire virtual memory images
to and from disk. This capability is ideal if, for example, you'd like to
save the entire state of the virtual memory system, release the memory back
to DOS, shell out a large program (like a compiler), and restore the state
of the system when the shelled program is completed.

A study of the VMEM API will reveal that you have a bit more control over
the operating parameters with VMEM than you do with our simple memory
allocator. The API lets you choose whether you want to clear the memory when
you allocate, set the dirty bit of a block when you deference that block,
and directly change some of the important operating parameters of VMEM.

Using a VMM as a replacement for heap-based allocation gives an application
more flexibility in dealing with huge amounts of data. I believe that
virtual memory is best done at the operating system level (for example,
using Microsoft OS/2) and kept invisible from all applications in the
system. But since a large part of the market is, and will remain for some
time to come, firmly entrenched in the DOS world, virtual memory managers
are still needed. In the future, all operating systems will have built-in
virtual memory and will make use of the dedicated memory management chips
produced by semiconductor vendors like Intel and Motorola. Let's hope that
that day is not too far away.

Figure 9

char far *VM_addr(VMPTR_TYPE voh, int dirty, int fFreeze)

Dereferences the memory handle voh. The variable dirty should not be zero if
you are going to change the contents of the memory block. fFreeze is not
zero if the block's position in memory should be frozen.

VMPTR_TYPE VM_alloc(long size, int fClear)

Allocates size bytes from the memory pool. If fClear is not zero, the memory
block will be cleared to zeros.

VM_dcmps

Initiates compression of the swap file. The compression method may be
selected with VM_parm.

 int VM_dump(char *filename)

Dumps the entire contents of the VM system to the disk file filename.
Returns 0 if the dump was successful, nonzero if not.

void VM_end

Terminates VMEM. The swap file is deleted, but real memory is not released
back to the operating system.

void VM_fcore

Same as VM_end, except that all real memory is returned to the operating
system.

void VM_free(VMPTR_TYPE voh)

Frees the object referenced by handle voh.

int VM_rest(char *filename)

int VM_frest(char *filename)

Restores the VMM to a previous state that was saved by VM_dump. The main
difference between VM_rest and VM_frest is that the VM_rest will read in the
memory blocks only as they are needed and is therefore much faster than
VM_frest.

 int VM_init

Initializes VMEM.

void VM_parm(long rmmax, long rmasize, double rmcompf, long dmmfree, int
dmmfblks, int dmctype)

Sets various VMEM parameters. Used to fine-tune the system. The arguments
are the following:

rmmax    --    maximum amount of real memory that VMEM will request

rmasize    --    minimum amount of real memory that VMEM will request

rmcompf    --    real memory compression factor

dmmfree    --    determines when automatic swap file compression occurs

dmmfblks    --    another method used to determine when swap file

        compression occurs

dmmctype    --    type of swap file compression used

VMPTR_TYPE VM_realloc(VMPTR_TYPE voh, long newsize)

Reallocates the memory block pointed to by voh and returns a handle to the
new block.

long *VM_stat

Used to obtain various statistics about the current state of VMEM.


Using the OS/2 Video I/O Subsystem to Create Appealing Visual Interfaces

 Richard Hale Shaw

The most noticeable attribute of an application is the way it visually
interacts with the user. If the application looks snappy and smart, the user
will gain confidence when running it for the first time. If the application
appears slow and dull, however, the user will become apprehensive or, worse
yet, bored. Thus, from the user's perspective, the screen is the most
essential mechanism of the application. To an OS/2 application developer,
this makes the video I/O (VIO) the most important of the three subsystems
available to OS/2 character-based applications.

An Overview of VIO

The MS-DOS(R) operating environment provided such limited and inefficient
video services, that application developers looked for other means to
improve the video throughput of their programs. A great number of DOS
applications took advantage of the PC ROM BIOS video services (Int 10h) or
wrote to and directly manipulated the video hardware and display buffer.
This approach hindered portability, but it was not a problem under the real
mode of the DOS operating environment, since only one application at a time
was able to access the video hardware.

In contrast, OS/2 systems are endowed with a robust highly efficient set of
video services, which comprise the VIO subsystem. The subsystem consists of
a set of character-oriented display services of the type that are generally
required by the current generation of character-based applications. Think of
VIO as a superset of the PC ROM BIOS services (Int 10h) found in real mode
under DOS, with the difference being that VIO uses calls instead of an
interrupt.

The efficiency and effectiveness of the VIO services are such that there is
little need for an application to manipulate the video hardware directly.
Perhaps the only reason an OS/2 application would need to access the video
hardware while using VIO would be to generate graphics images. VIO has only
limited graphics support, but it does allow an application to move to and
from graphics mode (this will be discussed in greater detail below). In the
event that an application does need to access the video hardware, OS/2 can
serialize such accesses and provide a means for applications to do so that's
compatible with its protected mode environment.

Although it's essential that a real operating system like OS/2 offer
efficient video services (where DOS does not), there is another, more
profound reason for the existence of VIO. The protected mode of the OS/2
operating environment requires that system facilities be virtualized and
shared among processes, including access to the video screen and video
hardware. An application should not be hindered from performing screen
updates while running in the background, but a foreground application should
not have its screen trashed by a program running in the background. By using
VIO, the user is assured that the video output of one application will not
interfere with that of another application. In addition, most VIO calls can
be used in bound programs that must run under both OS/2 and DOS. Like the
ROM BIOS calls or direct video hardware control code under DOS, VIO calls
under OS/2 make a program less portable to the DOS environment and more OS/2
specific. Of course, this is true of all non-FAPI OS/2 calls.

VIO is implemented as a dynamic-link library (found in VIOCALLS.DLL), so a
program's references to VIO services are bound at execution time, not at
link time. Thus, you can replace VIO functions at any time without
recompiling or relinking the client application. This is how the OS/2
Presentation Manager (PM) handles VIO calls. By providing its own version of
VIOCALLS.DLL, PM can allow non-PM OS/2 programs to run under it--even
in the PM screen group (that is, in a PM window).

VIO calls are efficient, but they do not offer the complex formatting
facilities of the printf standard library function nor do they handle output
redirection. A subsystem like VIO must be fast, so it's necessary that it
avoid the encumbrances of the file system and I/O redirection. In those
instances in which redirection is a consideration, however, you can use
DosRead and DosWrite. These two kernel services use VIO to handle the screen
component of their output. Note that when STDOUT points to a screen device,
OS/2 routes it through VIO.

If generic screen output is a consideration, you'll find it simpler and
easier to use the standard C library routines, since a typical application
will probably use a combination of these and VIO calls. The VIO services do,
however, allow considerable flexibility in the way text is written to the
screen, including control over color and cursor positioning.

The Logical Video Buffer

As mentioned earlier, OS/2 considers the screen a resource that is able to
be simultaneously shared by more than one process. When a session is
started, OS/2 creates a logical video buffer (LVB) for it. OS/2 retains
control of the physical video buffer (PVB). If a program makes a VIO call,
VIO will update the LVB of the program's session and notify OS/2 that the
PVB must be updated. While a session is in the foreground, OS/2 will
duplicate VIO updates of the LVB in the PVB. Thus, VIO calls always update
the two buffers while a session is in the foreground. When you move the
session into the background, OS/2 assumes that its screen contents are
completely updated in the LVB. Therefore, it doesn't have to save the screen
and can quickly update the PVB from the new foreground session's LVB. This
makes screen switches very fast and allows OS/2 to virtualize screen access
among processes.

While the session is in the background, VIO services will continue to write
to the session's LVB. OS/2 ignores calls from VIO to update the PVB, since
the foreground session is using the PVB. When the session is brought into
the foreground again, OS/2 will copy the contents of the LVB to the PVB and
resume updating the PVB from the LVB. Thus, as long as the application uses
VIO services to do its video updates, the LVB will always accurately depict
an application's visual state in character mode. This remains true whether
the application is in the foreground or background, and it makes VIO a safe
means of updating the video screen.

Each LVB is organized in 2-byte pairs, called cells (shown in Figure 1). The
number of cells in the LVB will vary with the current video mode (for
example, a 43-line mode will require more cells than a 25-line mode). Each
cell corresponds to one character on the screen, with the first byte of each
cell containing the character itself and the second byte holding the
attribute value (which controls foreground and background color, intensity,
and blinking). You can use different VIO services to write characters,
attributes, or cells to the video screen.

The VIO subsystem supports the full range of PC-based video adapters and
displays, including monochrome, CGA, EGA, and VGA.  The 24 different flavors
of text and graphics modes that are available are shown in Figure 2. The
smallest user-addressable unit of the screen is a character in text mode or
a pixel or pel in graphics mode. PC display hardware is memory mapped, so
whatever is currently stored in video memory (the PVB mentioned above) is
displayed on the screen. Other than the description of the video buffers
given above, however, it's not essential that you know how video memory is
organized or where it's physically located, unless you're going to access
the video hardware directly. Also, note that unlike DOS, OS/2 doesn't use
mode numbers to specify a video mode. When getting or setting the video
mode, you deal directly with the video characteristics themselves.

As previously mentioned, the attribute byte of each pair or cell controls
the colors, intensity, and blinking characteristics of a character displayed
on screen. Of the 8 bits in an attribute byte, the lower 3 bits control the
foreground color, a value of 0-7. Bit 3 toggles intensity on or off,
bits 4-6 control the background color, and bit 7 toggles the blinking
characteristic. These are shown in Figures 3 and 4.

VIO and PM

As you are probably well aware, an OS/2 system offers a high-powered
graphical user interface called the Presentation Manager. PM offers a
graphical presentation space, windowing, scroll bars, icons, and interfaces
for both a mouse and the keyboard. It runs in its own screen group under
OS/2 and can run other programs in PM windows in the same screen group.
These capabilities differ from those of VIO applications under OS/2 Version
1.0, where a background process remains in its own screen group and is not
visible until a user brings it into the foreground screen group. PM can also
run ill-behaved programs (that is, those programs that do not run in a PM
window or that write directly to the PVB), but they will run in their own
screen group like any other OS/2 program.

Although the Presentation Manager can do all of the things mentioned, there
is still a need for VIO. First, PM applications are complex. They take more
effort to write since simple visual ideas can be complex to express in PM
code. Second, some applications (a command-line utility, for instance) do
not require PM or wouldn't benefit very much from PM's graphical user
interface. Other applications will have to be redesigned or completely
rethought before they can take advantage of PM. These are problems that VIO
can easily solve. In addition, VIO offers a path to OS/2 that requires
minimal changes to existing DOS applications. Thus, it's considerably easier
to adapt a DOS application to VIO than to PM.

VIO-based programs will run under a PM text window. That's because the
versions of VIO (and, for that matter, KBD and MOU) offered with PM (OS/2
Version 1.1) are different from those supplied with OS/2 Version 1.0. The
VIO DLL supplied with PM is windows aware and can map each character and its
attributes into the appropriate pattern of pixels inside a PM window, while
clipping the output of each VIO call to fit the application's window size.
The entire process is invisible to both the programmer and the compiled
program.

If you use VIO in your OS/2 applications today, you won't pay an extensive
penalty later. As OS/2 is ported to other processor families, VIO calls will
remain the same and should not require changes to those portions of an
application that use VIO. Future releases of OS/2 for the 80286 family of
processors won't require a recompile, since new versions of VIO will be
supplied with each release. This is why VIO is implemented as a dynamic-link
package.

As this series continues, I'll include practical tips that will make it
easier for you to produce efficient, high-quality programs that take
advantage of OS/2 facilities. If you refer back to the second article in the
series, "Planning and Writing a Multithreaded OS/2 Program With Microsoft(R)
C," MSJ (Vol. 4, No. 2), there is a discussion of some of the ways a
multithreaded program can use up too many CPU cycles, making it difficult
for other processes to run (particularly those in the background). You'll
see another example of this in the program listing for this article. From
the perspective of using OS/2's video system, however, keep in mind that a
well-behaved OS/2 program is one that confines itself to the use of VIO
(most standard library functions call VIO) and does not access the PVB.

VIO Data Structures

Earlier in this series, I briefly discussed the new OS/2 header files,
structures, and type definitions. Because of the variety of objects used to
manipulate the screen, there are several data structures designed strictly
for VIO programming. These are displayed in Figure 5, which you can refer to
as you read about the sample program listing.

The VIO data structures provide all of the information used by the old
IBM(R) PC ROM BIOS calls, and more. But instead of passing information in
registers (the convention in the DOS environment), OS/2 places the
information in a structure whose address is passed to the appropriate
function. Not only does this make retrieving and changing the video
environment rather easy, it places a machine-independent interface on the
entire mechanism. Moreover, in keeping with other OS/2 functions, it allows
the function to return a single unsigned integer as an error return status.

For example, the program listing contains a function (Cursor) that can save
or restore the cursor. Notice that it uses the VIOCURSORINFO object type,
which includes the starting and ending cursor scan lines, the cursor width
(in columns or pixels depending on the current screen mode), and the cursor
character attribute. Information in the VIOCURSORINFO object is easily
retrieved and/or modified via the VioGetCurType and VioSetCurType functions,
as shown in the listing.

The VIO Pop-up Facility

VIO offers a means by which a background process can temporarily pop up in
the foreground or a foreground process can temporarily switch to a blank
text screen. If you note the stress on temporary, you'll begin to see that
the need for pop-ups is not the same as with DOS. Under DOS, a
terminate-and-stay-resident (TSR) application could take control of the
processor and pop up over the current application for an indefinite period
of time. Since DOS is a single-tasking environment, this became an
acceptable manner of communicating with a TSR program. And since the current
application was frozen until the TSR gave up control of the processor, the
TSR could stay popped up for as long as  you wished.

Under OS/2, the characteristics of a TSR program in the DOS environment are
no longer needed since you can run an application in another screen group
and switch that screen group into the foreground whenever, as often, and for
however long you like. From the perspective of accessing a TSR program,
every program is a TSR under OS/2. It's always there, running in its own
private screen group, although you can still issue the commands to the
application for it to terminate. This is even more obvious under PM, since
PM and well-behaved VIO programs that run in the PM screen group can
simultaneously display and run in overlapping windows.

Remember that a process can be detached and run in a special screen group,
where it does not have to have regular keyboard input or show screen
displays. Detached processes are good candidates for pop-up programs under
OS/2. You'll doubtless come up with other ideas.

Pop ups are exceptions to the way that OS/2 typically handles the console.
Only one pop-up program can be activated at a time: if another process
issues a pop-up call, it will be blocked until the first one relinquishes
the screen. While a pop-up program is active, the user cannot switch to
another process or screen group. In this sense, the pop-up program gains
temporary ownership of the foreground screen.

The pop-up facility of OS/2 should only be used by a process that needs to
gain temporary control of the physical console (that is, the screen, the
keyboard, or the mouse). Since other processes cannot be switched into the
foreground during a pop up, the pop-up program must be efficient, do its
job, and end the pop-up session quickly. A good example of how the OS/2
pop-up facility is used is the OS/2 Hard Error handler. You can observe its
use of the pop-up facility by issuing a DIR command on a disk drive while
the drive door is open.

When a pop-up program begins, the screen is automatically placed in 80 by 25
text mode. The screen is restored to the previous mode when the pop-up
program ends. The VIO service routines for activating a pop-up program are
VioPopUp and VioEndPopUp. The function prototypes for these two routines are
shown in Figure 6. Note that only a subset of VIO services are available
during the pop-up call (shown in Figure 7). An example of code that
activates a pop-up program can be found in the graphics function in the
sample program listing.

Using the VIO API

Any discussion of the VIO Application Programming Interface (API) ought to
begin with a reference to Ray Duncan's article, "Character-Oriented Display
Services Using OS/2's VIO Subsystem," MSJ (Vol. 2, No. 4). It contains an
excellent overview of the API and discusses most of the services that VIO
provides. This article will now pick up where Duncan's article left off and,
in doing so, will walk through the sample program listing presented here.
The sample is a modified and enhanced version of HELLO0.C, which was
presented in the preceding article in this series. This discussion will
provide insight into the practical application of VIO services.

A New HELLO.C

Recall that HELLO0.C was a multithreaded version of HELLO.C. It was designed
to illustrate the use of threads and semaphores to control and serialize
access to resources. Although technically sound, it was visually boring, so
adding a variety of colors to it is a good idea. In addition, the current
version of HELLO1.C (The actual code listing is available on MSJ's bulletin
boards and is not included here due to space constraints--Ed.) has been
restructured and modularized to allow different sample functions to be
plugged in with ease.

Probably the first thing you'll notice in HELLO1.C is that the macro
INCL_SUB has been defined at the beginning of the program listing. As such,
the program automatically includes the function prototypes, macros, and type
definitions that will be needed to use VIO functions via the system of
header files and defines that were discussed in the earlier article.

Next, note that every VIO function requires a device handle as its last
argument. In non-PM versions of OS/2, this handle is always zero. A special
set of VIO calls under PM (called advanced VIO, or AVIO) use this argument.
The argument is represented by a macro, VIOHDL, in the listings here, making
it easy to search for and replace references to it later.

Finally, in fitting with the convention used by all OS/2 API functions,
notice that VIO services that return an error code will return a nonzero
unsigned value when an error occurs. You can always assume that a VIO
service was successful when it returns a zero value. Also note that one of
the most commonly used VIO routines, VioWrtTTy, is now called VioWrtTTY in
OS/2 Version 1.1, which has been released since this series was initiated.

The Startup Function

As you see from the listing, main has been simplified to include only a
handful of functions. The first of these, startup, processes the optional
command-line arguments. As in HELLO0.C, these arguments are the number of
milliseconds used in calls to DosSleep, which allow you to vary the sleep
time between thread events. If you set these values too low, the program
will hog CPU time and radically slow down OS/2 and other programs. These
arguments are discussed in more detail below.

Startup performs several other services for the program.  It initiates a
keyboard thread (discussed below). Then it calls the Screen function to save
the current video display for restoration when the program terminates.
Screen takes a single argument, which determines if the screen should be
saved or restored. It assumes that the calling program's screen group is in
text mode and calls VioGetBuf to retrieve the address of the screen group's
LVB and the size of the buffer. Thus Screen can allocate storage to hold the
LVB contents and copy the contents of the LVB to the new storage. To restore
the display when the program terminates, Screen copies the contents of the
storage buffer back to the LVB and calls VioShowBuf to update the physical
display. This process is further discussed in the sidebar, "Direct Screen
I/O and Graphics under VIO".

Startup also calls the Cursor function to hide the cursor and save the
cursor position. Like Screen, the Cursor function takes a single argument,
whose bits determine whether Cursor should hide, save, or restore the cursor
(that is, position it and make it visible). It uses VioGetCurType to
retrieve the cursor character attribute and VioSetCurType to change the
attribute (and hide the cursor). It calls VioGetCurPos to save the current
cursor position and VioSetCurPos to reposition the cursor. Note that the
Screen and Cursor functions rely on the VIOCURSORINFO data type.

Startup also resets the screen rows to the highest number possible. It does
this by calling VioGetMode to get the current video mode information (into a
VIOMODEINFO object type). Then it sets the row parameter to either 50, 43,
or 25 lines and calls VioSetMode until it is successful. This lets the
program run at 50 lines on a VGA monitor, 43 lines on an EGA monitor, and 25
lines otherwise. The original row count is saved so the termination function
can restore it before the program exits.

After startup, main calls the initialization function shown in the listing.
This code is virtually identical to the code used by HELLO0.C in the
preceding article and initializes the FRAME structures.

Flickering Frames

The flickering frames of HELLO0.C that greeted you with "Hello, World from
thread" are now encapsulated in the flicker_frames function. The code for
this function is very similar to that in the original program. The function
will create about two dozen threads; the exact number will vary with the
number of screen rows. Each thread carries the responsibility of displaying
or clearing its frame on cue from its semaphore, which is stored in its
FRAME data type. And each thread shares the code found in the hello_thread
function.

Hello_thread now calls VioWrtCharStrAtt to do its screen I/O. This is a VIO
service routine that writes a string at a specified row and column location
with a specific attribute. Note that an attribute byte for each frame (and
thus each frame thread) has been added to the FRAME data type and is set and
manipulated by flicker_frames. Thus, whenever each frame appears it has a
different foreground and background color. The flicker_frames function masks
off the blinking bit in the attribute so that the frames do not blink. In
addition, the function increments the FRAME attribute byte whenever the
foreground and background colors match,  thereby ensuring that the
foreground and background contrast.

In order to accommodate the changes to the FRAME type, the program now
includes a filler byte. This ensures that the threadstack member is aligned
on an even-numbered address. The _beginthread library routine will not
successfully create a thread whose stack falls on an odd-numbered address.

The operator initiates the termination of the flicker_frames function by
pressing the Esc key. This activates the keyboard thread, which is contained
in the keyboard_thread function mentioned above. This thread blocks on
keyboard input and continues to block until the Esc key is pressed. Then it
blocks on a semaphore, doneSem, until flicker_frames clears the semaphore.
Clearing the semaphore allows keyboard_thread to begin the termination
sequence and flicker_frames to postpone the sequence until it has completed
a round of activating the frame threads.

Once the semaphore clears, keyboard_thread continues and sets the done flag,
thereby notifying flicker_frames that it can terminate the frame threads.
Flicker_frames will cause each thread to run one final time, clear its box
(if it is not already cleared), and terminate. As each frame thread
terminates, it clears its own frame semaphore to notify flicker_frames that
it is terminating. In the meantime, flicker_frames calls the
WaitForThreadDeath function, which blocks on each thread's semaphore, and
returns to the flicker_frames when all the threads have terminated. Then
flicker_frames returns to main.

Chasing Frames

New to HELLO1.C are chasing frames, which demonstrate an animated use of the
VIO scrolling routines. Encapsulated in the chasing_frames function, the
chasing frames are a series of Hello, World frames that are launched from
the upper-right-hand corner of the screen and traverse the screen perimeter
counterclockwise. Each successive circuit takes place one frame height and
width inside the last, so that the frames gradually make their way to the
center of the screen. Another frame follows after a period of time (which is
set by one of the command-line sleeptime arguments). The VIO scrolling func
tions, VioScrollLf, VioScrollDn, VioScrollRt, and VioScrollUp, not only
cause the frames to chase each other, they also leave a trail of each
frame's color behind. Each chasing frame is managed by its own thread,
thereby giving a visual confirmation to the reality of independent threads
of execution. The threads share the code found in the box_thread function.

Again the keyboard_thread function plays a role in terminating these
threads. After servicing flicker_frames, the keyboard thread goes into a
loop, blocking on keyboard input and ignoring all but the Esc key. When the
Esc key is finally pressed, the keyboard thread will again set the done flag
and terminate. This in turn notifies chasing_frames not to start any
additional frames. It also notifies those frame threads that have already
started to terminate themselves. If a frame thread is in the middle of
scrolling around the screen, its sleeptime will fall to zero and it will
race to the upper-right-hand corner of its current circuit. Then it will
clear its own semaphore and terminate. As flicker_frames did before it,
chasing_frames calls WaitForThreadDeath to wait until all the frame threads
have died; it then returns to main.

There is one catch to the way the keyboard thread works. The user must press
the Esc key before the last chasing frame completes its final circuit.
Otherwise, the keyboard thread will continue to block until the Esc key is
pressed. The next section of the program assumes that the keyboard is
available, so the keyboard thread must have terminated by that time.
Unfortunately, there is no kernel routine for one thread to kill another. In
any case, it would have made keyboard_thread even more complex to cause it
to terminate some other way.

The Video Configuration

The next function called from main is displayconfig. This function calls
get_user_name to prompt the user for his or her name. To do this,
get_user_name displays a multicolored prompt, and below it, a highlighted
input field delimited with two markers. It uses VioWrtCellStr, which writes
a series of character-attribute cell combinations from those stored in the
cellstr array to create the multicolored prompt. Then it calls VioWrtNCell,
which can replicate a single cell, to create the field and markers. Finally,
it restores the cursor, sets it to the beginning of the field, and calls the
KBD routine, KbdStringIn, to read the user's name. After hiding the cursor,
it returns the number of characters read by KbdStringIn.

When displayconfig returns from get_user_name, it knows how long the user's
name is, but it does not have the name itself. So it calls VioReadCharStr to
read the characters directly from the screen. Next, it retrieves the video
configuration with VioGetConfig, prints that information on the screen, and
returns to main.

Background Colors

The togglebackground function illustrates the use of two other VIO services:
VioReadCellStr and VioWrtNAttr. The former reads one or more cells from the
screen. The Background function, which is called by togglebackground, uses
it to retrieve the attribute of the character in the upper-left-hand corner
of the screen. It then increments or decrements the attribute value
(depending on the argument passed to it) and calls VioWrtNAttr to flood the
screen with this attribute. Thus, togglebackground can prompt the user to
use the arrow keys to increment or decrement the background color. Note that
exactly which color it uses the first time depends on the color of the
character in the upper-left-hand corner. This may have been set by one of
the chasing frames. The Esc key is used to terminate the togglebackground
function and return to main. An additional message (discussed below) is
printed, warning the user to press only the Esc key while in graphics mode.

Generating Graphics

The last facet of VIO that HELLO1.C illustrates is how to generate graphics
and create a pop-up program under OS/2. The graphics function changes the
video mode to CGA graphics with a 320 by 200 resolution and writes a
graphical Hi in magenta on a white field. The function creates a separate
thread of execution, which saves and restores the screen before and after a
screen switch (see the sidebar, "Direct Screen I/O and Graphics under VIO").
Finally, it waits for the user to press the Esc key to terminate. If,
however, the user presses any key (other than Alt-Esc or Ctrl-Esc), a pop up
will be generated that informs the user to press the Esc key.

The graphics function works by first retrieving the current video mode
information and changing the video mode parameters. It sets them to indicate
a video mode of non-monochrome graphics, with four colors, 40 by 25
characters, and 320 by 200 pixels. Then it calls VioSetMode to change the
video mode, followed by VioGetPhysBuf to retrieve a selector for the CGA
video buffer. It then locks the screen with a call to VioScrLock.

The function continues by creating a new thread that executes the
SaveRestoreScreen function discussed below. It calls clear_graphics_screen
to clear the screen and writes a large Hi in the middle of the screen. The
screen write uses two loops to traverse a bitmap of character 1s and 0s
(stored in the hi_bitmap array) and calls the putpixel function to write a
magenta pixel whenever a 1 is encountered in the bitmap. You can get an idea
of what this will look like by staring at the hi_bitmap array at the
beginning of the listing and unfocusing your eyes slightly.

Last of all, graphics calls VioScrUnLock to unlock the screen and goes into
a loop. In the loop it calls KbdCharIn to block on keyboard input. If the
Esc key is pressed, the function breaks out of the loop and returns to main.
Otherwise, it calls VioPopUp to create a blank 80 by 25 text mode screen,
prints a message, and waits for a single keystroke. Upon receiving the
keystroke, the function calls VioEndPopUp to end the pop up and loop back to
KbdCharIn.

The graphics function illustrates two important points: how to generate
graphics under OS/2 and how to save and restore the screen in graphics mode.
When you run the program, try some screen switches and press the wrong key
to generate the pop up. You will find that the warning message (at the end
of togglebackground) not to press anything but Esc during the graphics
display is a taunt to trick the user into generating the pop up. The
SaveRestoreScreen function is called by OS/2 each time the wrong key is
pressed to save or restore the graphics image and mode.

SaveRestoreScreen starts by calling VioGetMode to save the current video
mode information. Following that, it goes into a loop and immediately calls
VioSavRedrawWait. This call registers the thread with OS/2, causing the
thread to block until a screen switch of some kind occurs. At this point, it
doesn't matter whether the user switches sessions (using Alt-Esc), brings up
the Session Manager (with Ctrl-Esc), or presses an inappropriate key that
causes the graphics function to generate a pop up. If you want a screen
switch to takes place, OS/2 will activate the SaveRestoreScreen thread by
returning from VioSavRedrawWait.

Once SaveRestoreScreen returns from VioSavRedrawWait, it quickly saves the
screen by copying the contents of the video screen to a buffer. If a screen
restore has to take place, the function restores the screen by resetting the
video mode and copying the buffer contents back to the video screen.

The graphics function uses two support functions. The first,
clear_graphics_screen, simply floods the pixels in the video buffer with
FFH, turning them white. The second, putpixel, converts pixel row-column
coordinates into pixel offsets and stuffs the color bits passed into the
appropriate part of a byte into the video buffer.

Terminating the Program

The termination function completes HELLO1.C. It resets the screen rows to
the original number (restoring the original video mode at the same time),
calls Screen to restore the screen to its original state, and calls Cursor
to restore the cursor. Finally, it calls DosExit to terminate the program.

Here's a summary of how to run the program. The program goes through five
stages: flickering frames, chasing frames, the name prompt, the
configuration display, and graphics. The flickering frames will run
indefinitely until Esc is pressed. The chasing frames will continue until
the frames are exhausted, but for best results, terminate them before the
last frame makes its way to the center. Since a chasing frame normally
appears before its predecessor moves off the top line, it should be easy to
identify. The number of frames is set in the initialization function and
will vary on different video hardware.

Once you have entered your name at the name prompt, you can use the Up and
Down arrow keys to change the background color. You can continue
indefinitely changing the background until you press Esc. When you press
Esc, the graphics display will terminate. If you press any other keys, it
will generate the pop-up screen. Try recompiling the program with the
SaveRestoreScreen thread commented out. The graphics screen will be trashed
to some degree if you switch screens without this thread operating.

Finally, you may find it interesting to alter the sleeptime variables in the
program. Three of these can be altered on the command line. The command line
defaults are

HELLO1 1 1500 1

which places a minimum of 1 millisecond between the activation of each
flickering frame, 1500 milliseconds (1.5 seconds) between each chasing
frame, and a 1 millisecond pause when a chasing frame returns to the
upper-right-hand corner of its circuit. The most interesting results occur
when using 0 instead of 1 and a low number (like 100) instead of 1500. Keep
in mind, however, that 0 will cause the program to steal a lot of CPU time
from other processes. But it's fun to watch the results.

This concludes our examination of the OS/2 VIO subsystem. Additional
functions and services, which are not essential to a typical OS/2
application but should be mentioned, are listed and briefly discussed in
Figure 8.

You are now ready to tackle VIO. In fact, you can even write some
sophisticated applications. With a good feeling for VIO, you'll find the
rest of the OS/2 API no more difficult than gaining knowledge of how and
when to use the different services. This will become even more apparent in
the next article in this series, which switches from visual output to
keyboard and mouse control, and will explore the KBD and MOU subsystems.


Direct Screen I/O and Graphics Under VIO

The OS/2 VIO subsystem does not offer any facilities for a process to access
the physical screen directly. This can pose problems if you are porting DOS
applications that perform direct screen writes or generate graphics or if
you are writing an OS/2 application that generates graphics without the use
of Presentation Manager (PM). Fortunately, OS/2 offers two indirect means
for accessing the physical screen, depending on the needs of the
application.

Direct Screen Writes in Text Mode

OS/2 systems provide a logical video buffer (LVB) for each screen group, and
all VIO services update the LVB. When a screen group is in the background,
screen updates can take place without disrupting the screen of the
foreground process. When a screen group is in the foreground, OS/2
duplicates VIO updates of the LVB in the physical video buffer (PVB).

You can easily modify a DOS program that performs text mode direct screen
writes to run under OS/2. To do this, however, it's essential that you
change the code that updates the PC's video buffer to write to the LVB
instead.

First, you must call VioGetBuf to retrieve the address of the LVB of your
program's screen group. Since the format of the LVB is identical to a PC's
text mode video buffer, the direct screen write code remains largely
intact--only the destination changes. Note that there is no need to
include code that checks for horizontal retrace or that manipulates the
video hardware. OS/2 will take care of these details for you. When you are
finished updating the LVB, your program should call VioShowBuf to update the
PVB from the LVB. VioShowBuf can update all or a portion of the LVB to the
PVB.

This indirect screen write process remains the same whether your program is
in the foreground or background: OS/2 will only honor calls to VioShowBuf
when the program is in the foreground and will ignore such calls when a
program is in the background. OS/2 will, however, update the PVB from the
LVB once the program is switched to the foreground again. Note that although
your program only needs to call VioGetBuf once, when it begins, your program
can (and should) call VioShowBuf as often as is necessary. VioShowBuf will
do nothing while a process is in a background screen group. This is not a
problem since OS/2 will update the PVB upon a screen switch.

I should mention that VioShowBuf is very fast. I modified the Magma Systems
ME Editor to perform screen paging by writing to the LVB and occasionally
calling VioShowBuf. This improved the speed of screen page updates and made
the calls to VioShowBuf appear every bit as fast as direct screen writes
under DOS.

VIO function calls in a foreground process automatically update both
buffers, since they notify OS/2 that the PVB must be updated. You can see
this for yourself by writing a piece of code that first writes some text
directly to the LVB, then calls a VIO service routine to write additional
text, which overwrites part of the text written in the first step. This
updates the LVB and causes OS/2 to update the PVB. Finally, have the code
call VioShowBuf or manually switch the program into the background and back
again. Until the PVB is updated, only the output of step 2 will appear on
the screen. The text written in step 2 that overlays the text written in
step 1 will, however, survive the screen update after the call to
VioShowBuf. This also illustrates that, in text mode, the LVB always
contains the current visual state of an application.

Going Around VIO to Generate Graphics

Although it's relatively simple to perform direct screen writes in text
mode, it's not as easy to work with graphics. Part of the problem is that
you can't use the LVB, since it's only available for direct screen writes in
text mode. Therefore, if your program is going to generate graphic images,
you'll need to access the PVB directly. Fortunately, VIO has a mechanism
that allows an application to step around it and manipulate the PVB
directly. This makes it easier to port graphics applications from DOS and
enables you to write OS/2 graphics applications without the complexity of PM
programming.

There are a few caveats to be aware of when writing to the PVB. First, a
routine can only access one adapter and video mode at a time. To support
several adapters in a graphics routine, you'll have to write code that can
address each, making the code somewhat hardware dependent.

Next, images drawn in the PVB by an application are lost when a screen
switch takes place. As I said earlier, you can't use the LVB in graphics
mode. Further, there is no built-in mechanism for saving the contents of the
PVB when a screen switch takes place. Since it's not built in, you'll have
to provide this mechanism yourself. If your program manipulates any of the
video hardware, you'll also have to save that information when a screen
switch takes place.

Finally, an application that accesses the physical screen must run in its
own screen group, so it will never run in a PM window. When run in the PM
environment, PM gives the program its own full-screen window. If you want it
to run in a PM window, you'll have to rewrite the program to take advantage
of the PM graphic facilities.

With these warnings in mind, there are two problems to solve when writing a
program that generates graphics in a VIO context. How do you use the
mechanism for obtaining access and writing to the physical screen? And how
do you provide a facility to save and restore the screen before and after a
screen switch?

The Direct Screen Write Process

First, consider how to access and write to the physical screen buffer. Your
program should call VioSetMode to ensure that the display is in the correct
graphics mode. Then you must obtain the address of the PVB by calling
VioGetPhysBuf, which will return a selector that corresponds to the address
of the physical buffer. When you're ready to write to the buffer, you must
use VioScrLock to lock the screen so a background application will not
switch screens during the update. Next, your program should perform the
direct screen write itself. This should be fast, efficient code, since a
screen lock temporarily hangs the system and prevents a screen switch from
taking place. Finally, your program must call VioScrUnLock to unlock the
screen.

Note that once you've retrieved a selector for the physical buffer address,
the remaining steps must be taken every time your program is going to access
the display hardware and write to the screen. These steps are essential to
inhibit screen switches (which will disrupt the display) and to prevent
other processes from writing to the display at the same time.

VioScrLock is also a fast, effective way to determine whether or not your
program is in the foreground, since the caller can either choose to block if
the screen is not available or to return immediately. If the screen is
unavailable, the program can do other processing until it is. Probably the
most effective way to use VioScrLock is to place the direct screen write
code in a function that's executed by a separate thread. You can use a
semaphore to tell the thread when to perform a screen update, and the thread
can block on VioScrLock until the screen can be accessed. This leaves the
main thread free to continue with other work and lets you structure the code
so that a graphics-intensive I/O can be written quickly, in its entirety,
without a great deal of overhead.

VioScrLock is so effective in locking the screen that nothing can switch the
screen while it's locked. It protects the system from the application and
protects the application from the system. Even pop ups (including the hard
error handler) cannot be activated during a screen lock. As a safety valve,
OS/2 will cancel the lock and perform a screen switch after 30 seconds if a
program or the user has requested it. If your graphics application is still
drawing an image when the screen switch takes place, the picture will be
disrupted.

Saving/Restoring the Video Display and State

The second problem is to see how an application that manipulates the video
hardware can save and restore the entire video state. The contents of the
display buffer, the video mode, the palettes, the cursor, and so on, will
all be lost if a screen switch takes place. VIO cannot perform this service
for you, since a graphics bitmap might require a great deal of memory to
save and restore the display. Further, some video hardware adapters have
write-only registers that cannot be saved for later restoration--only
your program will know whether or not it changed a video register.

Thus, OS/2 can only notify a process when a screen switch is about to take
place. It becomes the application's job to save and restore the screen when
so notified. This is accomplished by creating a thread whose job it is to
save and restore the screen. You can register the thread function with OS/2;
then, just before the screen switch takes place, OS/2 will activate the
thread. This lets you construct a thread to do one job and do it
well--to save or restore the screen and video mode at the appropriate
time. Thus, the thread's structure, minus the details of saving/restoring
the display and video mode, is quite simple. The thread enters a loop and
immediately calls VioSaveRedrawWait. This function will cause the thread to
block until OS/2 indicates that a screen switch is about to take place. When
this happens OS/2 will cause VioSaveRedrawWait to return, at which time the
thread must perform the screen save or restoration. Returning to the top of
the loop, the thread again calls VioSaveRedrawWait, which notifies OS/2 that
the screen switch can continue. The thread will continue to block until the
next screen switch for this process occurs.

Note that upon its return, VioSaveRedrawWait will indicate whether a screen
save or restore should transpire. And, as with VioScrLock, the system is
vulnerable while the save/restore thread is active, so the thread function
must be as efficient as possible. Finally, note that a similar function,
VioModeWait, is provided if your program only needs to save the video mode
and not the display. VioModeWait is used in the same fashion as
VioSaveRedrawWait.

Please keep in mind that the discussion presented here pertains to graphics
applications that need to circumvent VIO or PM. It's not an issue with
ordinary text applications. There's nothing to stop you from using
VioGetPhysBuf to write to the screen in text mode, if you choose. The
efficiency you gain will, however, be offset by the code overhead needed to
save and restore the screen. That's why the VioGetBuf/VioShowBuf combination
is provided for text applications.


Investigating the Debugging Registers of the Intel 386 Microprocessor

 Marion Hansen and Nick Stuecklen

The more complex a program and the microprocessor executing it, the bigger
the task of debugging. In very complex systems, external debuggers simply
cannot do the whole job. Fortunately, debugging sophisticated software
created for the 386 hardware environment is made easier by the fact that the
Intel(R) 386 microprocessor (hereafter referred to as mp) itself provides a
substantial set of internal debugging support features. In this article we
will take a look at these 386 features and explore how today's
state-of-the-art debuggers use them.

In the Past

In the past, PC-based debuggers could only set code breakpoints, not data
access breakpoints, and setting those code breakpoints was restricted to
random access memory (RAM). Data monitoring utilities could be used, but
they merely attach themselves to the timer interrupt and periodically
examine and display the specified region of memory. Typically, a debugger
worked by overwriting the first byte of the specified instruction, which
preempted the ability to set breakpoints in read only memory (ROM). If you
wished to set a breakpoint at an instruction, an old-style debugger usually
saved the first byte of that instruction and then overwrote the byte with a
0CCH opcode (INT 3). When the microprocessor executed the 0CCH opcode, it
generated an INT 3, and the debugger, which was monitoring INT 3, was
invoked. The debugger displayed the breakpointed instruction and the
register state, then waited for the next command.

Today

Fortunately the introduction of the 386 mp changed all that. With its six
debug registers, the 386 mp provides built-in debugging support to let you
set breakpoint addresses and define when breakpoints will occur. The four
debug address registers are individually programmable and individually
enabled or disabled through the debug control register. The debug status
register maintains debug status information.

In addition to supporting the usual break on instructions, the 386 mp also
supports data access breakpoints. Data breakpoints are a very useful
debugging tool (besides being an exceptional capability for a
microprocessor). A data breakpoint occurs at the exact moment that data
residing at a particular address is read or written. Using data breakpoints,
you can immediately locate the instruction responsible for overwriting a
data structure. The 386 mp lets you set breakpoint addresses in ROM as well
as RAM. You can set a 386 debug exception when any of the following
conditions occur:

■    an execution of the breakpoint instruction

■    an execution of every instruction (single-stepping)

■    an execution of any instruction at a given address

■    a read or write of a byte, word, or doubleword at any specified address

■    an attempt to change a debug register

■    a task switch to a specific task (protected mode only)

Some Limitations

Even with all the above capabilities, the 386 debug registers can't always
do the job of hardware-assisted debuggers. For example, while many
in-circuit emulators (ICEs) can maintain breakpoints across processor
resets, a 386 mp reset clears the debug registers, effectively destroying
any previously set breakpoints. Another limitation is that many ICEs can set
breakpoints on I/O space accesses, but the 386 debug registers can't
distinguish between memory and I/O space accesses. All data breakpoints are
associated with the memory space only. You need an ICE to trap I/O space
accesses, or you can use an I/O permission bitmap in virtual 8086 mode.

You can use a hardware-assisted debugger such as an ICE to debug a debugger,
but the only way to debug a 386-based debugger using the 386 debug registers
is to ensure that the debugger uses only the INT 3 breakpoint. When
triggered, the 386 debug registers generate an INT 1; therefore, a debugger
relying only on INT 1 can debug a debugger relying only on INT 3. Further,
because you need stable hardware to use the 386 debug registers, debugging
in harsh, unknown system environments is easier with a full-scale ICE or
other hardware-assisted debugger. System developers usually don't have the
functional keyboard, display, disk, and operating system that is needed to
load and run a debugger relying only on the 386 debug registers; they must
rely on an ICE when debugging preliminary software and device drivers.

You do, however, need more than just the 386 debug registers to create an
acceptable software debugger. Programmers expect today's debuggers to
provide recognition of symbolics, fast disassemblies, and the ability to
handle multiple object module formats (OMFs). Another desirable feature is a
Boolean comparison layer above the debug exception handler.

Debuggers that take advantage of the 386 debug features are on the horizon.
Combining the 386 debug features with what is available on today's debuggers
will produce a powerful development tool. In the meantime, the example at
the end of this article is part of a working program that exploits the 386
debug features to create a useful debugging tool.

Built-in Features

The 386 mp provides several built-in debugging features:

■    Four breakpoints. You can specify four addresses that the CPU will
automatically monitor.

■    Arm or disarm the breakpoints. You can selectively enable and disable
various debug conditions that are associated with the four debug addresses.

■    Data  and  instruction breakpoints. You can set breakpoints on data
accesses as well as on instruction executions.

■    Singlestepping. You can step through the program one instruction at a
time.

Let's look at how the 386 debug features are implemented. Along the way, we
will develop some debugger software that will amplify these features.

Debug Registers

The 386 mp has six registers to control debug features. Figure 1 shows the
format of the debug registers. You can access them by using variants of the
MOV instruction. A debug register can be either the source or the
destination operand. In protected mode, the MOV instructions that access the
debug registers can only be executed at privilege level zero. Trying to read
or write the debug registers from any other privilege level causes a general
protection exception. Under a protected mode operating system (such as OS/2
or XENIX(R) systems), the debug registers are considered privileged
resources, and only privileged tasks can access them.

Debug address registers (DR0-DR3). You can set a breakpoint address in
each of the four debug address registers. Each register contains a linear
address used to identify a breakpoint. (In contrast, addresses are presented
as a segment/offset pair in real mode.)

Debug control register (DR7). The debug control register lets you define,
enable, and disable debug conditions. It specifies the conditions under
which the 386 recognizes a breakpoint, the breakpoint type (local or
global), and the breakpoint length field. The low order 8 bits of DR7 enable
or disable the four breakpoints. Each breakpoint has 2 enable bits. The L
bit enables local breakpoints, and the G bit enables global ones. The real
mode debugging program shown later in this article sets and clears both
bits.

For each address in register DR0 through DR3, the corresponding fields R/W0
through R/W3 (in register DR7) specify the type of action that can cause a
breakpoint. The 386 interprets these bits as follows:

 Bits    Meaning

00    break on instruction execution only

01    break on data writes only

10    undefined

11    break on data reads and writes but not on instruction fetches

The breakpoint length field (LENn) is associated primarily with data
breakpoints. The length of a data breakpoint can be a byte, a word, or a
doubleword. Specifying only the starting address of a data item is not
enough to match a breakpoint condition because data items can be of three
different lengths (8, 16, and 32 bits). The length field adds flexibility by
selecting a range of address accesses which can trigger a breakpoint. You
can specify the following lengths:

 Bits    Meaning

00    1-byte length

01    2-byte length (word)

10    undefined

11    4-byte length

    (doubleword)

Because instruction breakpoints should uniquely specify the byte-granular
starting address of the intended instruction, instruction breakpoints always
specify a length field of 1 byte. If RWn is 00--an instruction
execution breakpoint--LENn should also be 00 (1 byte). Any other length
is undefined.

When the LE (local) or GE (global) bit is set, the 386 mp slows execution so
data breakpoints can be reported on the precise instruction that caused
them. Keep in mind that while an instruction execution breakpoint occurs
before the specified instruction is executed, a data access (read or write)
breakpoint occurs after the specified data is read or written.

Debug status register (DR6). The debugger uses the debug status register to
determine what debug conditions have occurred. The 386 mp sets status bits,
and the debugger reads them. When the microprocessor detects a debug
exception, it sets one or more of the status register's 4 low-order bits, B0
through B3, before entering the debug exception handler. Bn is set if the
condition described in the address registers (DRn) and the control register
(LENn and R/Wn) occurs.

The BS bit, associated with the TF (trap flag) bit of the 386 EFLAGS
register, is set if the debug handler is entered, since a single-step
exception occurred. The single-step trap is the highest-priority debug
exception. When BS is set, any of the other debug status bits may also have
been set by the 386 mp. This means that a single-step trap can occur at the
same time an instruction or data breakpoint occurs. The BT and BS bits are
used only when the microprocessor is in protected mode.

The BT bit is associated with the T bit (debug trap bit) of the task state
segment, or TSS. The TSS is a data structure defined by the 386 system
architecture; it is available only in protected mode. Each task has its own
TSS, which holds the state of the task's virtual processor. The 386 mp sets
the BT bit before entering the debug handler if a task switch has occurred
and if the T bit of the new TSS is set. There is no corresponding bit in DR7
that enables and disables this trap; the T bit of the TSS is the only
enabling bit.

The ICE-386 is Intel's in-circuit emulator for the 386 mp. When the ICE-386
is attached, it has priority over the 386 debugger. The BD bit is set if the
next instruction will read or write one of the debug registers and ICE-386
is also using the debug registers.

The 386 mp only clears the bits of DR6 when the microprocessor is reset. To
avoid confusion in identifying the cause of the next debug exception, the
debug handler should move zeros to DR6 immediately before returning.

Setting Breakpoints

Each of the four breakpoints  is defined by its linear address (DRn) and its
length (LENn). The LEN field lets you specify a 1-, 2-, or 4-byte field. The
386 mp requires that 2-byte fields be aligned on word boundaries (addresses
that are multiples of two), and 4-byte fields must be aligned on doubleword
boundaries (addresses that are multiples of four). You will get unexpected
results if code or data breakpoint addresses are not properly aligned.

You can set a data breakpoint for a misaligned field longer than 1 byte by
using two or more debug address registers to hold the entire address. Each
entry must be properly aligned, and the entries must span the length of the
field. For example, when setting three breakpoints for a doubleword address
that starts on an odd-byte boundary, the first address identifies the first
byte of the doubleword, the second one identifies the next two bytes, and
the third identifies the last byte. Figure 2 shows you how to align
breakpoint addresses for the address of a doubleword starting on an odd-byte
boundary.

A memory access triggers a data read or write breakpoint when it occurs
within a defined breakpoint field (as determined by a breakpoint address
register and its corresponding LEN field). Figure 3 contains examples
showing when breakpoints will and will not occur.

Instruction  breakpoint addresses are always specified as 1 byte (LEN=00).
Other values for instruction breakpoint addresses are undefined. The 386
recognizes an instruction breakpoint only when the breakpoint address points
to the first byte of an instruction. Therefore, if the instruction has
prefixes, then the breakpoint address must point to the first prefix byte.

Debug Exceptions

Breakpoints set on instructions cause faults; all other debug conditions
cause traps. (Faults break before executing the instruction at the specified
address. Traps report a data access breakpoint after executing the
instruction that accesses the given memory item.) The debug exception can
report faults and traps at the same time. The following describes the four
classes of debug exceptions.

Instruction execution breakpoint. An instruction execution breakpoint is a
fault, so the 386 mp reports an instruction execution breakpoint before it
executes the instruction at the given address.

Data access breakpoint. A data access breakpoint exception is a trap. That
is, the processor reports a data access breakpoint after executing the
instruction that accesses the given memory item.

When using data breakpoints, you should set the DR7's LE bit, GE bit, or
both. If either LE or GE is set, any data breakpoint trap is reported
exactly after completion of the instruction that accessed the specified
memory item. This exact reporting is accomplished by forcing the 386
execution unit to wait for completion of data operand transfers before
beginning execution of the next instruction. If neither GE nor LE is set,
data breakpoints may not be reported until one instruction after the data is
accessed or they may not be reported at all. This is because the 386 mp
normally overlaps instruction execution with memory transfers to such a
degree that execution of the next instruction may begin before memory
transfers for the previous instruction are completed.

If a debugger is creating a data write breakpoint, it should save the
original data contents before setting the breakpoint. Because data
breakpoints are traps, a write into a breakpoint location is completed
before the trap condition is reported. The handler can report the saved
(original) value after the breakpoint has been triggered. The data in the
debug registers can be used to address the new value written by the
instruction that triggered the breakpoint.

Single-step trap. This debug condition occurs at the end of an instruction
if the trap flag (TF) of the flag's register held the value 1 at the
beginning of that instruction. Note that the exception does not occur at the
end of an instruction that sets TF. For example, if POPF is used to set TF,
a single-step trap does not occur until after the instruction that follows
POPF.

The interrupt priorities in hardware guarantee that if an external interrupt
occurs, single stepping stops. When an external and a single-step interrupt
occur together, the single-step interrupt is processed first. This clears
the TF bit. After saving the return address or the switch tasks, the
external interrupt input is examined before the first instruction of the
single-step handler executes. If the external interrupt is still pending, it
is then serviced. The external interrupt handler is not single stepped. In
order to single step an external interrupt handler, single step an INT n
instruction that refers to the interrupt handler.

Task switch breakpoint. In protected mode, a breakpoint occurs after
switching to a new task if the new TSS's T bit is set. The breakpoint occurs
after control passes to the new task but before the first instruction is
executed. The exception handler can detect a task switch by examining the BT
bit of the debug status register (DR6).

Interrupts

Both the 386 and earlier microprocessors have two interrupt vectors
dedicated to debugging. Interrupt 1 is reserved for the single-step
instruction trap, interrupt 3 for instruction breakpoints. In addition, the
386 mp generates an interrupt 1 on any debug register trigger.

Interrupt 1. The handler for interrupt 1 is usually a debugger or part of a
debugging system. Figure 4 shows the seven breakpoint conditions that can
cause an interrupt 1. The debugger can check flags in DR6 and DR7 to
determine what condition caused the exception and what other conditions
might have occurred.

Interrupt 3. This exception is caused by execution of the breakpoint
instruction INT 3. Typically, a pre-386 debugger prepared a breakpoint by
substituting the opcode of the 1-byte breakpoint instruction for the first
opcode byte of the instruction to be trapped.

Prior to the 386 machine, microprocessors used the breakpoint exception
extensively for trapping execution of specific instructions. The 386 mp
solves this need more conveniently by using the debug registers and
interrupt 1. However, the breakpoint exception is still useful for debugging
debuggers because the breakpoint exception can vector to an exception
handler that is different from that used by the debugger. The breakpoint
exception can also be useful when you need to set more breakpoints than
allowed by the four debug registers.

Sample Debugger Program

You don't need to scrap your old-style debugger to take advantage of the
386's built-in debugging features. With a little help from a friendly
debugging assistant (such as the working program fragment shown later in
this article), you can continue to use your current debugger. By enabling or
disabling data breakpoints with a pop-up routine, the debugging assistant
shown supplements the setting of instruction breakpoints by old-style
debuggers. You still have all the benefits of your older debugger (symbolics
and disassemblies, for example) plus the powerful built-in debugging
capabilities of the 386 mp.

The 386-based debugging assistant runs in the background as an adjunct to a
traditional, pre-386 debugger. The debugging assistant is a
terminate-and-stay-resident (TSR) program that pops up either when you press
the SYSREQ key or when a debug exception occurs. The pop-up screen describes
the 386 registers and identifies which of the four individually programmable
breakpoints occurred.

Our debugging assistant has several features built into it. A human
interface displays all the registers you're interested in and lets you
easily enter breakpoint addresses, enable/disable breakpoints, and define
breakpoint conditions. Figure 5 shows a model of this screen, which is
divided into the following five option fields:

■    BREAKPOINT contains the four 386 debug registers and their options.

■    COMPARE shows the Boolean comparison options that are available.

■    SPECIAL REGS contains the 386 EFLAGS register in its hexadecimal
representation, the debug control register (DR7), and the debug status
register (DR6).

■    REGISTER SET displays the values in the most frequently referenced 386
registers.

■    EFLAGS  presents  the EFLAGS register decoded into an English format.

You can change most of the values displayed on the screen by either editing
or toggling each respective field.

A Boolean comparison layer decides if the exception meets the criteria you
specified. The debugging assistant has Boolean comparison logic that is
armed and disarmed separately by toggling the switch (sw) field in COMPARE.
You can also toggle the Boolean (bool) field in COMPARE between:

<    less than

< =    less than or equal to

=    equal to

< >    not equal to

>    greater than

> =    greater than or equal to

The value to be used in the comparison logic is specified by editing the
value field in COMPARE.

An interrupt service routine handles exceptions. The debugging assistant
program has two interrupt service routines (ISRs). One ISR handles the
SYSREQ key and the other handles debug exception interrupts. Part of the
code in Figure 6 contains an interrupt service routine.

Common code is called by the interrupt service routines. In the debugging
assistant program, both ISRs call a common block of code that copies the 386
registers into a large data structure. The common code then passes the
address of the data structure to the high-level language program that
controls the human interface.

After the registers are modified (through the human interface), the
high-level language program returns control to the common code. The common
code then copies the edited register images back into the 386 registers and
"goes to sleep" until the next debug exception or until the SYSREQ key is
pressed. Figure 6 contains the common code, written in assembly language.

Some of the assembly language mnemonics may be unfamiliar. Because of the
absence of 386 assemblers, we hand-assembled certain 386-specific
instructions. Most of them were relatively easy to code because they only
needed a prefix byte. The prefix byte specifies that the following
instruction will use 32-bit operands, rather than 16-bit pre-386
instructions. For example, inserting the prefix byte 066h ahead of

mov ax,bx

forms an instruction which the 386 mp interprets as:

mov eax,ebx

Figure 7 shows the flow of the debugging assistant program.

A thorough understanding of the 386 debug support features presented in this
article will provide a good starting point for tackling difficult debugging
chores. It makes it possible to build special debugging utilities that focus
on specific problems that might not be handled by a generic debugger. We are
not, of course, suggesting that the reader should actually build a debugger.
But under certain circumstances a debugging assistant such as the one
presented here will decrease debugging time plus give you the satisfaction
of both fully exploiting the debugging capabilities and better understanding
the internal workings of the most advanced microprocessor on the market.


Strategies for Building and Using OS/2 Run-Time Dynamic-Link Libraries

 Ross M. Greenberg

Your process has died.

Later, when you run the BACKTRACE function in your debug DLL, you find that
you TOOK the ax with a strncmp but tried to THROW with a strcmp. And with a
quick correction the bug is history.

A Debug DLL

What is a debug DLL anyway, and how can you create one?

The Dynamic-Link Library (DLL) capabilities of OS/2 systems actually come in
two "flavors." The first, called static linking, was described fully in
"Design Concepts and Considerations in Building an OS/2 Dynamic-Link
Library," MSJ (Vol. 3, No. 3). Briefly stated, the static linking
implementation of DLLs lets an EXE function be called into memory from
another file, a DLL.

When the EXE program loads, the necessary functions are loaded into memory
from the DLL, linkage references to the functions are resolved, and the EXE
starts to run. Only functions referenced at link time are loaded into
memory, and the operating system can discard and reload them as needed.
Importantly, only one copy of any given function needs to be in memory and
it is then shared by all sessions requiring it.

There are various advantages to using the Dynamic-Link capabilities of OS/2
systems when developing code: smaller run times; the ability to truly share
a single copy of your developed routines between programs (when properly
configured, only a single copy of your routines will get loaded into memory
regardless of how many processes use them); the ability to distribute
different versions of your program packaged with different DLLs to serve
different markets; and so on.

DLLs were such a good concept that a majority of OS/2 is, in reality, a
collection of DLLs. Different OEMs, when porting OS/2 over to their own
unique hardware environment, need only replace individual DLLs as they
continue with their port effort.

Each software author will most likely find a different aspect of the DLL
concept that fits their needs; DLLs are that flexible. Although a little
difficult to use initially, I've found that most of my OS/2 code uses DLLs
more and more--and, more and more, it uses routines already written as
I create my own libraries, with a resolution granularity that's definable on
a routine-by-routine basis.

However, loading a DLL like this doesn't let you load functions as you might
wish--and all functions you reference in the link stage are loaded into
memory. If you happen to have a very large, infrequently used function, it
will still be read from disk, loaded into memory, and later (perhaps)
discarded by the operating system if there's a memory crunch.

Dynamic Linking

OS/2 provides a second flavor to its Dynamic-Link Library Package: the
ability to have "run-time linking." In essence, this lets you load functions
(called procedures in DLL parlance) as needed. And when necessary, you can
(in essence) discard them just as easily. You can even mix the two different
types of DLLs, so that the operating system takes care of the "usual" cases
and you take care of the unusual ones.

Necessary Functions

There are only five new function calls you need to learn to take advantage
of this feature. But there are several strategies you will need to consider
as you create and build your DLLs. The five new functions are:

DosLoadModule

DosGetProcAddr

DosFreeModule

DosGet ModHandle

DosGetModName

In discussing these functions I'm going to begin using the C notation
provided in the most recent set of manuals from the Microsoft(R) Software
Development Kit (SDK); assembly language programmers, be forewarned. The new
definitions and typedefs allow for more machine independent code, although
they might be a bit difficult to understand at first. A bit of sound advice
here: it is essential that the very first thing you do is to make a complete
hard-copy list of all the OS/2 header files. Study them now and you will
save yourself lots of digging for the information later. Future versions of
OS/2 systems will most likely allow the use of different addressing schemes,
so machine independence is actually a valuable concept, even at this stage.

To begin with we have DosLoadModule, defined (prototyped) in BSEDOS.H:

USHORT    DosLoadModule(
           bomb_ptr,
           bomb_ptr_len,
           mod_name_ptr,
               mod_handle_ptr)
PSZ       bomb_ptr;
USHORT    bomb_ptr_len;
PSZ       mod_name_ptr;
PHMODULE  mod_handle_ptr;

The bomb_ptr field is a character array, of length bomb_ptr_len. The
operating system will fill in this array with the null-terminated name of
the DLL file that caused the error in loading. Normally this null-terminated
string would be equivalent to the actual module name, mod_name_ptr. But
since a DLL can call other DLLs, there is a possibility that some DLL
further down the "tree" might have caused the problem.

If the function returns a zero, it loaded successfully. If the module was
already loaded by some other process, the reference count on the module is
incremented. The module will remain loaded as long as the reference count is
greater than zero. Only segments of the DLL marked as "preload" in the DLL's
DEF file will be automatically loaded.

The module name itself must be the name of a DLL in your LIBPATH or the
function will fail. The module handle must be preserved for all other calls
of the run-time load package; think of it as a file handle if you wish. Once
the module can be referenced by a handle, routines within the DLL can now be
accessed--if their address is known.

USHORT    DosGetProcAddr(
            mod_handle,
                function_name,
            function_ptr)
HMODULE   mod_handle;
PSZ       function_name;
PPFN      function_ptr;

This function will return the address in the DLL (specified in mod_handle)
of the named function into the function pointer, function_addr. Once
returned, it may be called via dereferencing as with any other function
pointer. Functions with the same name can be referenced in different DLLs
since the handle to the DLL itself would be different.

The name of the function, however, can also be an ordinal number. Each
function within a DLL can have a public name associated with it. For
efficiency, you can remove the function names from the DLLs and only allow
reference through the ordinal number. As an example, DOSCALLS.DLL does not
have the function names readily available, and functions contained therein
must be done by ordinal number.

An ordinal number is of the form "#xyz"--it must start with a pound
sign, and the remaining part of the string (that is, xyz) is the ASCII
representation of the ordinal number of the function you wish to reference.

OS/2 does not provide a means of discarding a function you no longer have
any use for. But the operating system will discard the function if it needs
the memory the function occupies. If you no longer reference the function,
then it will not be reloaded into memory.

On the other hand, when you no longer need a DLL module, you may discard it.

USHORT    DosFreeModule(
             mod_handle)
HMODULE   mod_handle;

This will decrement the reference count of the module itself. If the
reference count returns to zero, then all memory allocated to the module
will be deallocated and discarded as required. Even if the reference count
of the module is not zero, however, the local process's references to the
DLL are no longer valid; calls to function addresses returned by
DosGetProcAddr will fail with a protection violation.

When a program exits, it returns all of its resources to the operating
system, so it is not necessary to call DosFreeModule when you exit your
program--although it is good programming practice.

Two complementary functions exist, for determining whether the process has
already loaded a module and for determining the name of a module based on
its handle.

USHORT    DosGetModHandle(
           mod_ptr,
              mod_handle_ptr)
PSZ       mod_ptr;
PHMODULE  mod_handle_ptr;

This function will return the handle associated with a named DLL module.
It's useful if you want to double-check whether a given module is already
loaded. Since the handle itself is local to a given process, if you need to
use this call, you should double-check your error processing--how could
you forget a handle?

There are some specific reasons for using this function  in a large program.
Potentially, some other member of your programming team could have released
the module, and then reloaded it later. This would usually cause it to get a
different handle, which might cause you a problem if the handle is in a
local, static variable. You can find a way around that problem in the debug
routine in the sample program.

DosGetModName, which is the best function, is defined as follows:

USHORT    DosGetModName(
          mod_handle,
          name_len,
          name_ptr)
HMODULE   mod_handle;
USHORT    name_len;
PCHAR     name_ptr;

This function simply returns the DLL name associated with a given handle
into the character array of name_ptr. If the name of the DLL can't fit into
the character array, then an error condition is returned. This is useful in
determining the name of a currently loaded DLL. Again, take a look at the
debug function in the sample program for an idea of how to use it.

DLLs Call DLLs

One of the more interesting aspects of DLLs has largely been ignored until
now; the ability of a DLL to utilize another DLL almost infinitely. I use
this technique as an efficient way of dealing with two distinct DLLs
utilized via the run-time load mechanisms.

There is a decent enough reason for it. You needn't change any code in an
applications program to take advantage of things like extended commands and
functions; it becomes transparent where the actual DLL "lives," and the
total separation of the two DLLs--through a third, common
DLL--allows easy modification of one without having to recompile or
maintain the other.

FNDFILE

The simple application I use to demonstrate the run-time loading
capabilities of OS/2, FNDFILE (see Figures 1, 2, and 3), uses the OS/2
function call, DosFindFirst. This function lets you retrieve structures
containing information about files with names matching a given pattern. You
can retrieve information on as many files matching the pattern as you like,
so long as you provide a buffer large enough to hold the returned
information.

One interesting little quirk in DosFindFirst is that--although it is
documented to fill a buffer with the contents of a structure, called
FILEFINDBUF--it cheats a little. Basically the structure is a bunch of
dates and times, a couple of longs (to indicate filesize), a character
count, and an array of 13 characters, which holds the filename. The
character array contains only enough bytes to satisfy the need for a
null-terminated string. The next structure starts immediately after the null
byte, so a simple pointer-to-structure increment doesn't work, since that
increments the pointer by exactly one sizeof(FILEFINDBUF). Therefore, when
examining the code in the show routine, please forgive the ugliness of the
increment to the pointer.

An interesting concept used in OS/2 is that of a "directory-search handle."
This lets you consider the DosFindFirst function a resource which can be
allocated and deallocated like any other resource. For example you could set
up a number of initial requests to find a different matching filename
pattern (say, the same pattern in different directories) and to then call
functions, passing the directory-search handle as a parameter, which in turn
call DosFindNext using those handles to find the next file on a
pattern-by-pattern basis. Calling DosFindFirst with a directory-search
handle already in use causes closing and then reuse of that handle.

Other parameters in the DosFindFirst function let you include matching
"special" files in your request, including directories, hidden files,
read-only files, etc. By examining the returned attribute for each of the
files, you can determine what type of special file you're currently
examining.

The DosFindFirst function is useful enough that most of us will have some
need of it. Yet it's also sufficiently complex that we'll all make mistakes
when we first use it. Additionally it makes a perfect test case for a debug
DLL.

The structure of the FNDFILE program is a bit convoluted, so some
explanation is in order. The main routine lets you enter the parameters of
your choice via a simple keyword parsing if-then-else clause. If you enter
the "GO" command, you call the do_find function. Other "action" keywords let
you turn debugging on or off, show your current parameter list, and show the
current return buffer contents.

The do_find function is called with parameters passed exactly as
DosFindFirst expects them; it makes it easier to do the call later. When the
find program is first invoked, the debug function is called with the flag
turned off, and debug itself (in FNDFILE3) loads the normal DosFindFirst
function from the appropriate DLL, DOSCALLS.

You cannot, however, just load the DosFindFirst function as you'd wish. The
function name isn't immediately accessible from the DOSCALLS library. Since
the name is "private," I was forced to do a hex dump of the OS2.LIB file (or
the DOSCALLS.LIB file, depending on your version of OS/2) and to find the
so-called "ordinal number" of the DosFindFirst function.

An ordinal number is basically a simple numeric representation of the
function number itself in the library, and is a more efficient way of
calling important functions than forcing the kernel to do a strncmp on a
function name.

The hex dump (see Figure 4) shows 64 as the ordinal number for DosFindFirst.
To indicate that you're using an ordinal number, you must specify the number
as an ASCII null-terminated string, with a '#' prefixed to it as the
function name in the DosLoadProcAddr function.

Before you can call the DosLoadProcAddr function, the appropriate DLL must
be opened up and a handle, returned by the operating system, preserved for
future reference. Use of the DosGetModHandle checks whether the program has
already loaded the DLL; if so, DosLoadModule, the function which returns the
handle address, is not called. If, however, the DLL is not open, this
implies that the other DLL is open (for all calls to debug but the first).
Therefore a call is made to DosGetModName, simply to show the name of the
DLL already loaded, and the open DLL is closed via the DosFreeModule call.

The DosLoadProcAddr function returns the actual address of the requested
function, whether called with a function name (which should be in uppercase)
or an ASCII-based ordinal number. Simply by calling this function via the
normal C mechanisms, you can call any one of a number of functions, as long
as the parameters match.

One of the functions with matching parameters is the do_list function. This
is in FNDFILE2 (see Figure 2). This function simply prints out all of your
parameters, saving the ones the call might change, then calls the real
DosFindFirst function. This implies that the function is already loaded when
you first invoke the FNDFILE program. If you want to avoid this
"preloading," you can also have the do_list function call all the
appropriate run-time load functions for its own copy of the DosFindFirst
pointer--but this is only demonstration code, in any case.

The call to DosFindFirst is done simply to get back the status code, which
is expected and will be processed by the do_find function. The number of
files to be found on subsequent calls is reset, as is the original handle.

The important thing here to realize is that the do_find function calls
either do_list or DosFindFirst without really knowing which. So in a
transparent manner, you can create your program with full run-time loading
of your difficult functions, debug them completely, then let the actual
programming effort continue.

An obvious extension of this technique would let you provide your end-users
with a full debug DLL of all your functions and all the OS/2 system calls
you use. Then when you get that inevitable tech support phone call, you can
just have the user turn on debug mode and modem you a copy of the voluminous
output the debug DLL produces.

Run-time loading is one of the treasures in OS/2 which, with enough
exploration, you'll find extraordinarily valuable as you produce some of the
larger programs OS/2 makes possible. In fact, you can prompt for a DLL to be
loaded, and load it on the spot! The name doesn't have to be in the code.
Even the simple debug facility demonstrated here dwarfs the capabilities of
other debugging methods in other operating systems.

Figure 1

[FNDFILE.C]

#define    INCL_DOS
#define    INCL_ERRORS

#include    <os2.h>
#include    <stdio.h>
#include    <stdlib.h>
#include    <string.h>


USHORT    far    _loadds    pascal debug(USHORT);
USHORT    far    _loadds pascal do_find(PSZ, PHDIR, USHORT,
                     PFILEFINDBUF, USHORT, PUSHORT, ULONG);
USHORT    far    _loadds pascal do_list(PSZ, PHDIR, USHORT,
                     PFILEFINDBUF, USHORT, PUSHORT, ULONG);

void    do_usage(void);
void    main(void);

void
show(PFILEFINDBUF f_ptr, USHORT cnt)
{
    while (cnt--)
    {
        printf("Len is: %2d. Filename is:%s\n",
                (int)(f_ptr->cchName), f_ptr->achName );
        f_ptr = (PFILEFINDBUF) ((char far *)f_ptr + 24 +
                (int)(f_ptr->cchName));
    }
}

void
do_usage()
{
 printf("\n\nexit        - to exit\n");
 printf("file=<filename> - enter name of message file\n");
 printf("hand=<val>      - 1 = default, ffff = create new\handle\n");
 printf("attrb=<attrb>   - Attributes: see pg 98 of Ref Manual\n");
 printf("len=<buf_len>   - length of output buffer to\
                           use.Allocated\n");
 printf("cnt=<cnt>       - Maximum number of files to return\n");
 printf("debug=on|off    - turn debug mode on or off\n");
 printf("show            - show buffer contents...\n");
 printf("list            - show current parameter settings\n");
 printf("go              - call DosFindFirst()\n");
}

void
main()
{
CHAR        tmp_buf[256];
CHAR        name[64];
USHORT      max_len = 0;
char        *buf_ptr = (char *)NULL;
HDIR        handle = 1;
USHORT      attrb = 0;
USHORT      num_files = 0;

    if(!debug(FALSE))
    {
        printf("Error in initial call to debug\n");
        exit(1);
    }
    *name = (CHAR)NULL;
    while(TRUE)
    {
        printf(">");
        gets(tmp_buf);
        strlwr(tmp_buf);
        if(!strncmp(tmp_buf, "exit", 4))
            exit(1);
        else
        if(!strncmp(tmp_buf, "file=", 5))
            strcpy(name, tmp_buf + 5);
        else
        if(!strncmp(tmp_buf, "len=", 4))
        {
            if(buf_ptr)
                free(buf_ptr);
            max_len = atoi(tmp_buf + 4);
            buf_ptr = calloc(max_len, 1);
        }
        else
        if(!strncmp(tmp_buf, "cnt=", 4))
            num_files = atoi(tmp_buf + 4);
        else
        if(!strncmp(tmp_buf, "hand=", 5))
            sscanf(tmp_buf + 5, "%x", &handle);
        else
        if(!strncmp(tmp_buf, "attrb=", 6))
            sscanf(tmp_buf + 6, "%x", &attrb);
        else
        if(!strncmp(tmp_buf, "list", 4))
            do_list(name, (PHDIR)&handle, attrb,
            (PFILEFINDBUF)buf_ptr, max_len,
            (PUSHORT)&num_files, (ULONG)0);
        else
        if(!strncmp(tmp_buf, "debug=", 6))
        {
            if(!debug(!strncmp(tmp_buf + 6, "on", 2)))
            {
                printf("Error in subsequent call to debug\n");
                exit(1);
            }
        }
        else
        if(!strncmp(tmp_buf, "show", 4))
            show((PFILEFINDBUF)buf_ptr, num_files);
        else
        if(!strncmp(tmp_buf, "go", 2))
            do_find(name, (PHDIR)&handle, attrb,
            (PFILEFINDBUF)buf_ptr, max_len,
            (PUSHORT)&num_files, (ULONG)0);
        else
        if(*tmp_buf == '?')
            do_usage();
        else
            printf("?Huh?\n");
    }

}

[FNDFILE.DEF]

IMPORTS FNDFILE3.do_find
IMPORTS FNDFILE3.debug
IMPORTS FNDFILE2.do_list



Figure 2

[FNDFILE2.C]

#define    INCL_BASE
#define    INCL_ERRORS
#define    INCL_DOS

#include    <os2.h>
#include    <stdio.h>
#include    <stdlib.h>
#include    <string.h>

void show(PFILEFINDBUF f_ptr, USHORT cnt);

USHORT far _loadds pascal do_list(PSZ name_ptr, PHDIR
                            hptr, USHORT attrb,
                            PFILEFINDBUF
                            buf_ptr,
                            USHORT buf_len,
                            PUSHORT num_ptr,
                            ULONG reserved)
{
USHORT    stat;
HDIR      save_handle = *hptr;
USHORT    save_num = *num_ptr;

    printf("File name: %s\n", name_ptr);
    printf("Buffer address is %lx\n", buf_ptr);
    printf("Buffer length: %d\n", buf_len);
    printf("File count: %d, address: %ld\n",
            *num_ptr, num_ptr);
    printf("Handle is: %d\n", *hptr);
    printf("Attribute: %d\n", attrb);

    stat = DosFindFirst(name_ptr, hptr, attrb, buf_ptr,
                    buf_len, num_ptr, reserved);
    printf("Return status was:%d, cnt was:%d\n", stat,
             *num_ptr);
    *hptr = save_handle;
    *num_ptr = save_num;
    return(stat);
}

[FNDFILE2.DEF]

LIBRARY FNDFILE2
EXPORTS do_list



Figure 3

[FNDFILE3.C]

#define     INCL_BASE
#define     INCL_ERRORS
#define     INCL_DOS

#include    <os2.h>
#include    <stdio.h>
#include    <stdlib.h>
#include    <string.h>

#define    FAIL_BUF_LEN    128
PSZ    mod_name[] = {"doscalls", "fndfile2"};
PSZ    routine_name[] = {"#64", "DO_LIST"};

USHORT    (pascal far *routine)(PSZ, PHDIR, USHORT, PFILEFINDBUF, USHORT,
PUSHORT, ULONG);

USHORT    far    _loadds pascal do_find(PSZ name_ptr, PHDIR
                            hptr, USHORT attrb,
                            PFILEFINDBUF buf_ptr,
                            USHORT buf_len,
                            PUSHORT num_ptr,
                            ULONG reserved)
{
USHORT    stat;

    if(!(stat = routine(name_ptr, hptr, attrb, buf_ptr,
                    buf_len, num_ptr, reserved)))
    {
        printf("Good return. Files found = %d\n", *num_ptr);
    }
    else
    {
        switch(stat)
        {
        case  ERROR_BUFFER_OVERFLOW:
                printf("Buffer Overflow - Increase Buffer Size\n");
                break;

        case  ERROR_DRIVE_LOCKED:
                printf("Drive Locked\n");
                break;

        case  ERROR_FILE_NOT_FOUND:
                printf("File: %s not found\n", name_ptr);
                break;

        case  ERROR_INVALID_HANDLE:
                printf("Invalid handle: %d\n", *hptr);
                break;

        case  ERROR_INVALID_PARAMETER:
                printf("Invalid Parameter\n");
                break;

        case  ERROR_NO_MORE_FILES:
                printf("Ran out of files\n");
                break;

        case  ERROR_NO_MORE_SEARCH_HANDLES:
                printf("Can;t allocate another Search Handle\n");
                break;

        case  ERROR_NOT_DOS_DISK:
                printf("Not a DOS Disk\n");
                break;

        case  ERROR_PATH_NOT_FOUND:
                printf("I can't locate that Path\n");
                break;

        default:
                printf("Unknown error in FindFirst: %d\n", stat);
                break;
        }
    }
    return(stat);
}


far _loadds pascal debug(USHORT debug_flag)
{
USHORT    stat;
CHAR      fail_buf[FAIL_BUF_LEN];
static    HMODULE    handle = 0;
HMODULE   tmp_handle;
CHAR      tmp_buf[128];

    printf("Debug is: %s\n", debug_flag ? "On" : "Off");


    /* already a DLL loaded? */

    if(handle)
    {
        /* some DLL already loaded. Requested DLL? */

        stat = DosGetModHandle(mod_name[debug_flag],
                            &tmp_handle);

        /* if error, or a handle mismatch, then it isn't
         * the requested DLL */

        if(stat || tmp_handle != handle)
        {
            /* Get name of the DLL currently loaded */

            if(stat = DosGetModName(handle, 128,
                                tmp_buf))
            {
                printf("Couldn't retrieve loaded DLL Name. Error
                code is: %d\n", stat);
                return(FALSE);
            }
            else
                printf("Currently Loaded DLL is: %s\n",
                        tmp_buf);

            /* free the already loaded module, whatever
             * it is */

            DosFreeModule(handle);
        }
        else
        {
            /* current handle is for requested DLL.
             * Simply return */
            printf("DLL (%s) already loaded\n",
                    mod_name[debug_flag]);
            return(TRUE);
        }

    }

    /* wrong DLL is now freed */
    /* try to load the requested DLL, and get the entry
     * points */

    if(stat = DosLoadModule(fail_buf, FAIL_BUF_LEN,
                        mod_name[debug_flag],
                        &handle))
    {
        printf("Couldn't load: %s (stat is :%x)\n",
                mod_name[debug_flag], stat);
        printf("DLL problem was in: %s\n", fail_buf);
        return(FALSE);
    }
    else
        printf("Module handle is: %d\n", handle);

    /* Now get the entry point for the requested routine */

    if    (stat = DosGetProcAddr(handle,
         routine_name[debug_flag], &routine))
    {
        printf("Couldn't get routine: %s (stat is :%d)\n",
                routine_name[debug_flag], stat);
        return(FALSE);
    }
    else
        printf("Routine address is: %lx\n", routine);

    /* module loaded, entry point returned, so we return */
    return(TRUE);
}

[FNDFILE3.DEF]

LIBRARY    FNDFILE3
EXPORTS    do_find
EXPORTS    debug
IMPORTS    FNDFILE2.do_list



Figure 4

Partial Dump of \PMSDK\LIB\OS2.LIB

0FC0:   00 00 8A 02 00 00 00 00 00 00 00 00 00 00 00 00   ................
0FD0:   80 0F 00 0D 44 4F 53 43 41 4C 4C 53 30 30 30 36   ....DOSCALLS0006
0FE0:   33 16 88 1D 00 00 A0 01 01 0C 44 4F 53 46 49 4E   3.........DOSFIN
0FF0:   44 46 49 52 53 54 08 44 4F 53 43 41 4C 4C 53 40   DFIRST.DOSCALLS@
1000:   00 00 8A 02 00 00 00 00 00 00 00 00 00 00 00 00   ................
1010:   80 0F 00 0D 44 4F 53 43 41 4C 4C 53 30 30 30 36   ....DOSCALLS0006
1020:   34 15 88 1C 00 00 A0 01 01 0B 44 4F 53 46 49 4E   4.........DOSFIN
1030:   44 4E 45 58 54 08 44 4F 53 43 41 4C 4C 53 41 00   DNEXT.DOSCALLSA.
1040:   00 8A 02 00 00 00 00 00 00 00 00 00 00 00 00 00   ................



How the 8259A Programmable Interrupt Controller Manages External I/O Devices



Jim Kyle and Chip Rabinowitz

Unlike software interrupts, which are service requests initiated by a
program, hardware interrupts occur in response to electrical signals
received from a peripheral device such as a serial port or a disk
controller, or they are generated internally by the microprocessor itself.
Hardware interrupts, whether external or internal to the microprocessor, are
given prioritized servicing by the Intel(R) CPU architecture.

The 8086 family of microprocessors (which includes the 8088, 8086, 80186,
80286, and 80386) reserves the first 1024 bytes of memory (addresses
0000:0000H through 0000:03FFH) for a table of 256 interrupt vectors, each a
4-byte far pointer to a specific interrupt service routine (ISR) that is
carried out when the corresponding interrupt is processed. The design of the
8086 family requires certain of these interrupt vectors to be used for
specific functions (see Figure 1). Although Intel actually reserves the
first 32 interrupts, IBM, in the original PC, redefined usage of Interrupts
05H to 1FH. Most, but not all, of these reserved vectors are used by
software, rather than hardware, interrupts; the redefined IBM uses are
listed in Figure 2.

Nestled in the middle of Figure 2 are the eight hardware interrupt vectors
(08-0FH) IBM implemented in the original PC design. These eight vectors
provide the maskable interrupts for the IBM(R) PC-family and close
compatibles. Additional IRQ lines built into the IBM PC/AT(R) are discussed
under The IRQ Levels below.

The conflicting uses of the interrupts listed in Figures 1 and 2 have
created compatibility problems as the 8086 family of microprocessors has
developed. For complete compatibility with IBM equipment, the IBM usage must
be followed even when it conflicts with the chip design. For example, a
BOUND error occurs if an array index exceeds the specified upper and lower
limits (bounds) of the array, causing an Interrupt 05H to be generated. But
the 80286 processor used in all AT-class computers will, if a BOUND error
occurs, send the contents of the display to the printer, because IBM uses
Interrupt 05H for the Print Screen function.

Hardware Interrupt Categories

The 8086 family of microprocessors can handle three types of hardware
interrupts. First are the internal, microprocessor-generated exception
interrupts (refer to Figure 1). Second is the nonmaskable interrupt, or NMI
(Interrupt 02H), which is generated when the NMI line (pin 17 on the 8088
and 8086, pin 59 on the 80286, pin B8 on the 80386) goes high (active). In
the IBM PC family (except the PCjr and the Convertible), the nonmaskable
interrupt is designated for memory parity errors. Third are the maskable
interrupts, which are usually generated by external devices.

Maskable interrupts are sent to the main processor through a chip called the
8259A Programmable Interrupt Controller (PIC). When it receives an interrupt
request, the PIC signals the microprocessor that an interrupt needs service
by driving the interrupt request (INTR) line of the main processor to high
voltage level. This article focuses on the maskable interrupts and the 8259A
because it is through the PIC that external I/O devices (disk drives, serial
communication ports, and so forth) gain access to the interrupt system.

Interrupt Priorities

The Intel microprocessors have a built-in priority system for handling
interrupts that occur simultaneously. Priority goes to the internal
instruction exception interrupts, such as Divide by Zero and Invalid Opcode,
because priority is determined by the interrupt number: Interrupt 00H takes
priority over all others, whereas the last possible interrupt, 0FFH, would,
if present, never be allowed to break in while another interrupt was being
serviced. However, if interrupt service is enabled (the microprocessor's
interrupt flag is set), any hardware interrupt takes priority over any
software interrupt (INT instruction).

The priority sequencing by interrupt number must not be confused with the
priority resolution performed by hardware external to the microprocessor.
The numeric priority discussed here applies only to interrupts generated
within the 8086 family of microprocessor chips and is totally independent of
system interrupt priorities established for components external to the
microprocessor itself.

Interrupt Service Routines

For the most part, programmers need not write hardware-specific program
routines to service the hardware interrupts. The IBM PC BIOS routines,
together with MS-DOS services, are usually sufficient. In some cases,
however, the MS-DOS operating system and the ROM BIOS do not provide enough
assistance to ensure adequate performance of a program. Most notable in this
category is communications software, for which programmers usually must
access the 8259A and the 8250 Universal Asynchronous Receiver and
Transmitter (UART) directly.

Two major characteristics distinguish maskable interrupts from all other
events that can occur in the system: they are totally unpredictable, and
they are highly volatile. In general, a hardware interrupt occurs when a
peripheral device requires the complete attention of the system and data
will be irretrievably lost unless the system responds rapidly.

All things are relative, however, and this is especially true of the speed
required to service an interrupt request. For example, assume that two
interrupt requests occur at essentially the same time. One is from a serial
communications port receiving data at 300 bps; the other is from a serial
port receiving data at 9600 bps. Data from the first serial port will not
change for at least 30 milliseconds, but the second serial port must be
serviced within one millisecond to avoid data loss.

Unpredictability

Because maskable interrupts generally originate in response to external
physical events, such as the receipt of a byte of data over a communications
line, the exact time at which such an interrupt will occur cannot be
predicted. Even the timer interrupt request, which by default occurs
approximately 18.2 times per second, cannot be predicted by any program that
happens to be executing when the interrupt request occurs.

Because of this unpredictability, the system must, if it allows any
interrupts to be recognized, be prepared to service all maskable interrupt
requests. Conversely, if interrupts cannot be serviced, they must all be
disabled. The 8086 family of microprocessors provides the Set Interrupt Flag
(STI) instruction to enable maskable interrupt response and the Clear
Interrupt Flag (CLI) instruction to disable it. The interrupt flag is also
cleared automatically when a hardware interrupt response begins; the
interrupt handler should execute STI as quickly as possible to allow higher
priority interrupts to be serviced.

Volatility

As noted earlier, a maskable interrupt request must normally be serviced
immediately to prevent loss of data, but the concept of immediacy is
relative to the data transfer rate of the device requesting the interrupt.
The rule is that the currently available unit of data must be processed (at
least to the point of being stored in a buffer) before the next such item
can arrive. Except for such devices as disk drives, which always require
immediate response, interrupts for devices that receive data are normally
much more critical than interrupts for devices that transmit data.

The problems imposed by data volatility during hardware interrupt service
are solved by establishing service priorities for interrupts generated
outside the microprocessor chip itself. Devices with the slowest transfer
rates are assigned lower interrupt service priorities, and the most
time-critical devices are assigned the highest priority of interrupt
service.

Maskable Interrupts

The microprocessor handles all interrupts (maskable, nonmaskable, and
software) by pushing the contents of the flags register onto the stack,
disabling the interrupt flag, and pushing the current contents of the CS:IP
registers onto the stack.

The microprocessor then takes the interrupt number from the data bus,
multiplies it by 4 (the size of each vector in bytes), and uses the result
as an offset into the interrupt vector table located in the bottom 1Kb
(segment 0000H) of system RAM. The 4-byte address at that location is then
used as the new CS:IP value (see Figure 3).

External devices are assigned dedicated interrupt request lines (IRQs)
associated with the 8259A. See the subsection titled "The IRQ Levels" below.
When a device requires attention, it sends a signal to the PIC via its IRQ
line. The PIC, which functions as an "executive secretary" for the external
devices, operates as shown in Figure 4. It evaluates the service request
and, if appropriate, causes the microprocessor's INTR line to go high. The
microprocessor then checks whether interrupts are enabled, that is, whether
the interrupt flag is set. If they are, the flags are pushed onto the stack,
the interrupt flag is disabled, and CS:IP is pushed onto the stack.

The microprocessor acknowledges the interrupt request by signaling the 8259A
via the interrupt acknowledge (INTA) line. The 8259A then places the
interrupt number on the data bus. The microprocessor gets the interrupt
number from the data bus and services the interrupt. Before issuing the IRET
instruction, the interrupt service routine must issue an end-of-interrupt
(EOI) sequence to the 8259A so that other interrupts can be processed. This
is done by sending 20H to port 20H. (The similarity of numbers is pure
coincidence.)

The 8259A (see Figure 5) has a number of internal components, many of them
under software control. Only the default settings for the IBM PC family are
covered here.

Three registers influence the servicing of maskable interrupts: the
interrupt request register (IRR), the in-service register (ISR), and the
interrupt mask register (IMR).

The IRR is used to keep track of the devices requesting attention. When a
device causes its IRQ line to go high, to signal    the 8259A that it needs
service, a bit is set in the IRR that corresponds to the interrupt level of
the device.

The ISR specifies the interrupt levels that are currently being serviced; an
ISR bit is set when an interrupt has been acknowledged by the CPU (via INTA)
and the interrupt number has been placed on the data bus. The ISR bit
associated with a particular IRQ remains set until an EOI sequence is
received.

The IMR is a read/write register (at port 21H) that masks (disables)
specific interrupts. When a bit is set in this register, the corresponding
IRQ line is masked and no servicing for it is performed until the bit is
cleared. Thus, a particular IRQ can be disabled while all others continue to
be serviced.

The fourth major block in Figure 5, labeled Priority resolver, is a complex
logical circuit that forms the heart of the 8259A. This component combines
the statuses of the IMR, the ISR, and the IRR to determine which, if any,
pending interrupt request should be serviced and then causes the
microprocessor's INTR line to go high. The priority resolver can be
programmed in a number of modes, although only the mode used in the IBM PC
and close compatibles is described here.

The IRQ Levels

When two or more unserviced hardware interrupts are pending, the 8259A
determines which should be serviced first. The standard mode of operation
for the PIC is the fully nested mode, in which IRQ lines are prioritized in
a fixed sequence. Only IRQ lines with higher priority than the one currently
being serviced are permitted to generate new interrupts.

The highest priority is IRQ0, and the lowest is IRQ7. Thus, if an Interrupt
09H (signaled by IRQ1) is being serviced, only an Interrupt 08H (signaled by
IRQ0) can break in. All other interrupt requests are delayed until the
Interrupt 09H service routine is completed and has issued an EOI sequence.

Eight-level Designs

The IBM PC and PC/XT (and port-compatible computers) have eight IRQ lines to
the PIC chip--IRQ0 through IRQ7. These lines are mapped into interrupt
vectors for Interrupts 08H through 0FH (that is, 8 + IRQ level). These eight
IRQ lines and their associated interrupts are listed in Figure 6.

Sixteen-level Designs

In the IBM PC/AT, 8 more IRQ levels have been added by using a second 8259A
PIC (the "slave") and a cascade effect, which gives 16 priority levels.

The cascade effect is accomplished by connecting the INT line of the slave
to the IRQ2 line of the first, or master, 8259A instead of to the
microprocessor. When a device connected to one of the slave's IRQ lines
makes an interrupt request, the INT line of the slave goes high and causes
the IRQ2 line of the master 8259A to go high, which, in turn, causes the INT
line of the master to go high and thus interrupts the microprocessor.

The microprocessor, ignorant of the second 8259A's presence, simply
generates an interrupt acknowledge signal on receipt of the interrupt from
the master 8259A. This signal initializes both 8259A and also causes the
master to turn control over to the slave. The slave then completes the
interrupt request.

On the IBM PC/AT, the eight additional IRQ lines are mapped to Interrupts
70H through 77H (see Figure 7). Because the eight additional lines are
effectively connected to the master 8259A's IRQ2 line, they take priority
over the master's IRQ3 through IRQ7 events. The cascade effect is
graphically represented in Figure 8.

Programming for the Hardware Interrupts

Any program that modifies an interrupt vector must restore the vector to its
original condition before returning control to DOS (or to its parent
process). Any program that totally replaces an existing hardware interrupt
handler with one of its own must perform all the handshaking and terminating
actions of the original: reenable interrupt service, signal EOI to the
interrupt controller, and so forth. Failure to follow these rules has led to
many hours of programmer frustration.

When an existing interrupt handler is completely replaced with a new,
customized routine, the existing vector must be saved so it can be restored
later. Although it is possible to modify the 4-byte vector by directly
addressing the vector table in low RAM (and many published programs have
followed this practice), any program that does so runs the risk of causing
system failure when the program is used with multitasking or multiuser
enhancements or with future versions of DOS. The only technique that can be
recommended for either obtaining the existing vector values or changing them
is to use the MS-DOS functions provided for this purpose: Interrupt 21H
Functions 25H (Set Interrupt Vector) and 35H (Get Interrupt Vector).

After the existing vector has been saved, it can be replaced with a far
pointer to the replacement routine. The new routine must end with an IRET
instruction. It should also take care to preserve all microprocessor
registers and conditions at entry and restore them before returning.

Replacement Handler

Suppose a program performs many mathematical calculations of random values.
To prevent abnormal termination of the program by the default MS-DOS
Interrupt 00H handler when a DIV or IDIV instruction is attempted and the
divisor is zero, a programmer might want to replace the Interrupt 00H
(Divide by Zero) routine with one that informs the user of what has happened
and then continues operation without abnormal termination. The COM program
DIVZERO.ASM (see Figure 9) does just that.

Supplementary Handlers

In many cases, a custom interrupt handler augments, rather than replaces,
the existing routine. The added routine might process some data before
passing the data to the existing routine, or it might do the processing
afterward. These cases require slightly different coding for the handler.

If the added routine is to process data before the existing handler does,
the routine need only jump to the original handler after completing its
processing. This jump can be done indirectly, with the same pointer used to
save the original content of the vector for restoration at exit. For
example, a replacement Interrupt 08H handler that merely increments an
internal flag at each timer tick can look something like the code in Figure
10.

The added handler must preserve all registers and machine conditions, except
for those machine conditions that it will modify, such as the value of
myflag in the example (and the flags register, which is saved by the
interrupt action), and it must restore those registers and conditions before
performing the jump to the original handler.

A more complex situation arises when a replacement handler does some
processing after the original routine executes, especially if the
replacement handler is not reentrant. To allow for this processing, the
replacement handler must prevent nested interrupts, so that even if the old
handler (which is chained to the replacement handler by a CALL instruction)
issues an EOI, the replacement handler will not be interrupted during
postprocessing. For example, instead of using the preceding Interrupt 08H
example routine, the programmer could use the code shown in Figure 11 to
implement myflag as a semaphore and use the XCHG instruction to test it.

Note that an interrupt handler of this type must simulate the original call
to the interrupt routine by first doing a PUSHF, followed by a far CALL via
the saved pointer to execute the original handler routine. The flags
register pushed onto the stack is restored by the IRET of the original
handler. Upon return from the original code, the new routine can preserve
the machine state and do its own processing, finally returning to the caller
by means of its own IRET.

The flags inside the new routine need not be preserved, as they are
automatically restored by the IRET instruction. Because of the nature of
interrupt servicing, the service routine should not depend on any
information in the flags register, nor can it return any information in the
flags register. Note also that the previous handler (invoked by the indirect
CALL) will almost certainly have dismissed the interrupt by sending an EOI
to the 8259A PIC. Thus, the machine state is not the same as in the first
myint8 example.

To remove the new vector and restore the original, the program simply
replaces the new vector (in the vector table) with the saved copy. If the
substituted routine is part of an application program, the original vector
must be restored for every possible method of exiting from the program
(including Control-Break, Control-C, and critical-error Abort exits).
Failure to observe this requirement invariably results in system failure.
Even though the system failure might be delayed for some time after the exit
from the offending program, as soon as some subsequent program overlays the
interrupt handler code the crash is imminent.

Summary

Hardware interrupt handler routines, although not strictly a part of DOS,
form an integral part of many MS-DOS programs and are tightly constrained by
MS-DOS requirements. Routines of this type play important roles in the
functioning of the IBM personal computers, and, with proper design and
programming, significantly enhance product reliability and performance. In
some instances, no other practical method exists for meeting performance
requirements.

Figure 1

Interrupt

Number    Definition

00H    Divide by zero

01H    Single step

02H    Nonmaskable interrupt (NMI)

03H    Breakpoint trap

04H    Overflow trap

05H    BOUND range exceeded (see note 1)

06H    Invalid opcode (see note 1)

07H    Coprocessor not available (see note 2)

08H    Double-fault exception (see note 2)

09H    Coprocessor segment overrun (see note 2)

0AH    Invalid task state segment (see note 2)

0BH    Segment not present (see note 2)

0CH    Stack exception (see note 2)

0DH    General protection exception (see note 2)

0EH    Page fault (see note 3)

0FH    (Reserved)

10H    Coprocessor error (see note 2)

Note 1:    The 80186, 80286, and 80386 microprocessors only.

Note 2:    The 80286 and 80386 microprocessors only.

Note 3:    The 80386 microprocessor only.

Figure 2

    Interrupt

    Number        Definition

    05H        Print screen

    06H        Unused

    07H        Unused

    08H        Hardware IRQ0 (timer-tick) (see note 1)

    09H        Hardware IRQ1 (keyboard)

    0AH        Hardware IRQ2 (reserved) (see note 2)

    0BH        Hardware IRQ3 (COM2)

    0CH        Hardware IRQ4 (COM1)

    0DH        Hardware IRQ5 (fixed disk)

    0EH        Hardware IRQ6 (floppy disk)

    0FH        Hardware IRQ7 (printer)

    10H        Video service

    11H        Equipment information

    12H        Memory size

    13H        Disk I/O service

    14H        Serial-port service

    15H        Cassette/network service

    16H        Keyboard service

    17H        Printer service

    18H        ROM BASIC

    19H        Restart system

    1AH        Get/Set time/date

    1BH        Control-Break (user defined)

    1CH        Timer tick (user defined)

    1DH        Video parameter pointer

    1EH        Disk parameter pointer

    1FH        Graphics character table

    Note 1:    IRQ =  Interrupt request line.

    Note 2:    See figures 7 and 8.

Figure 6

IRQ

Line    Interrupt    Description

IRQ0    08H    Timer tick, 18.2 times per second

IRQ1    09H    Keyboard service required

IRQ2    0AH    I/O channel (unused on IBM PC/XT)

IRQ3    0BH    COM1 service required

IRQ4    0CH    COM2 service required

IRQ5    0DH    Fixed-disk service required

IRQ6    0EH    Floppy-disk service required

IRQ7    0FH    Data request from parallel printer

        (see note 1)

Note 1: This request cannot be reliably generated by older versions of the
IBM Monochrome/Printer Adapter and compatibles. Printer drivers that depend
on this signal for operation with these cards are subject to failure.

Figure 7

IRQ

Line    Interrupt    Description

IRQ0    08H    Timer tick, 18.2 times per second

IRQ1    09H    Keyboard service required

IRQ2    0AH    INT from slave 8259A:

IRQ8    70H        Real-time clock service

IRQ9    71H        Software redirected to IRQ2

IRQ10    72H        Reserved

IRQ11    73H        Reserved

IRQ12    74H        Reserved

IRQ13    75H        Numeric coprocessor

IRQ14    76H        Fixed-disk controller

IRQ15    77H        Reserved

IRQ3    0BH    COM2 service required

IRQ4    0CH    COM1 service required

IRQ5    0DH    Data request from LPT2

IRQ6    0EH    Floppy-disk service required

IRQ7    0FH    Data request from LPT1

Figure 9

name    divzero
    title    'DIVZERO - Interrupt 00H Handler'
;
;DIVZERO.ASM: Demonstration Interrupt 00H Handler
;This code is specific to 80286 and 80386 microprocessors.
;To assemble, link, and convert to a COM file:
;
;    MASM DIVZERO
;    LINK DIVZERO
;    EXE2BIN DIVZERO.EXE DIVZERO.COM
;    DEL DIVZERO.EXE
;

cr    equ    0dh    ; ASCII carriage return
lf    equ    0ah    ; ASCII linefeed
eos    equ    '$'    ; end of string marker

_TEXT    segment    word public 'CODE'

    assume    cs:_TEXT,ds:_TEXT,es:_TEXT,ss:_TEXT

    org    100h

entry:    jmp    start    ; skip over data area

intmsg    db    'Divide by Zero Occurred!',cr,lf,eos

divmsg    db    'Dividing '    ; message used by demo
par1    db    '0000h'    ; dividend goes here
    db    ' by '
par2    db    '00h'    ; divisor goes here
    db    ' equals '
par3    db    '00h'    ; quotient here
    db    'remainder'
par4    db    '00h'    ; and remainder here
    db    cr,lf,eos

oldint0    dd    ?    ; save old Int00H vector

intflag    db    0    ; nonzero if divide by
            ; zero interrupt occurred

oldip    dw    0    ; save old IP value


;
;The routine 'int0' is the actual divide by zero interrupt handler.
;It gains control whenever a divide by zero or overflow occurs. Its
;action is to set a flag and then increment the instruction pointer
;saved on the stack so that the failing divide will not be reexecuted
;after the IRET.
;
;In this particular case we can call MS-DOS to display a message during
;interrupt handling because the application triggers the interrupt
;intentionally. Thus, it is known that MS-DOS or other interrupt ;handlers
are not in control at the point of interrupt.
;

int0:    pop        cs:oldip        ;capture instruction pointer

    push    ax
    push    bx
    push    cx
    push    dx
    push    di
    push    si
    push    ds
    push    es

    push    cs    ; set DS = CS
    pop    ds

    mov    ah,09h    ; print error message
    mov    dx,offset _TEXT:intmsg
    int    21h

    add    oldip,2    ; bypass instruction causing
            ; divide by zero error

    mov    intflag,1    ; set divide by 0 flag

    pop    es    ; restore all registers
    pop    ds
    pop    si
    pop    di
    pop    dx
    pop    cx
    pop    bx
    pop    ax

    push    cs:oldip    ; restore instruction pointer

    iret        ; return from interrupt


;
;The code beginning at 'start' is the application program. It alters
;the vector for Interrupt 00H to point to the new handler, carries
;out some divide operations (including one that will trigger an
;interrupt) for demonstration purposes, restores the original
;contents of the Interrupt 00H vector, and then terminates.
;

start:    mov    ax,3500h    ; get current contents
    int    21h    ; ofInt 00H vector

            ; save segment:offset
            ; of previous Int 00H handler
    mov    word ptr oldint0,bx
    mov    word ptr oldint0+2,es

            ; install new handler ...
    mov    dx,offset int0    ; DS:DX = handler address
    mov    ax,2500h    ; call MS-DOS to set
    int    21h    ; Int 00H vector

            ; now our handler is active,
            ; carry out some test divides.

    mov    ax,20h    ; test divide
    mov    bx,1    ; divide by 1
    call    divide

    mov    ax,1234h    ; test divide
    mov    bx,5eh    ; divide by 5EH
    call    divide

    mov    ax,5678h    ; test divide
    mov    bx,7fh    ; divide by 127
    call    divide

    mov    ax,20h    ; test divide
    mov    bx,0    ; divide by 0
    call    divide    ; (triggers interrupt)

            ; demonstration complete,
            ; restore old handler

    lds    dx,oldint0    ; DS:DX = handler address
    mov    ax,2500h    ; call MS-DOS to set
    int    21h    ; Int 00H vector

    mov    ax,4c00h    ; final exit to MS-DOS
    int    21h    ; with return code = 0


;
;The routine 'divide' carries out a trial division, displaying the
;arguments and the results. It is called with AX = dividend and
;BL = divisor.
;

divide    proc    near

    push    ax    ; save arguments
    push    bx

    mov    di,offset par1    ; convert dividend to
    call    wtoa    ; ASCII for display

    mov    ax,bx    ; convert divisor to
    mov    di,offset par2    ; ASCII for display
    call    btoa

    pop    bx    ; restore arguments
    pop    ax

    div    bl    ; perform the division
    cmp    intflag,0    ; divide by zero detected?
    jne    nodiv    ; yes, skip display

    push    ax    ; no, convert quotient to
    mov    di,offset par3    ; ASCII for display
    call    btoa

    pop    ax    ; convert remainder to
    xchg    ah,al    ; ASCII for display
    mov    di,offset par4
    call    btoa

    mov    ah,09h    ; show arguments, results
    mov    dx,offset divmsg
    int    21h

nodiv:    mov    intflag,0    ; clear divide by 0 flag
    ret        ; and return to caller

divide    endp




wtoa    proc    near    ; convert word to hex ASCII
            ; call with AX = binary value
            ;           DI = addr for string
            ; returns AX, CX, DI destroyed

    push    ax    ; save original value
    mov    al,ah
    call    btoa    ; convert upper byte
    add    di,2    ; increment output address
    pop    ax
    call    btoa    ; convert lower byte
    ret        ; return to caller

wtoa    endp



btoa    proc        near    ; convert byte to hex ASCII
            ; call with AL = binary value
            ;           DI = addr to store string
            ; returns AX, CX destroyed

    mov    ah,al    ; save lower nibble
    mov    cx,4    ; shift right 4 positions
    shr    al,cl    ; to get upper nibble
    call    ascii    ; convert 4 bits to ASCII
    mov    [di],al    ; store in output string
    mov    al,ah    ; get back lower nibble

    and    al,0fh    ; blank out upper one
    call    ascii    ; convert 4 bits to ASCII
    mov    [di+1],al    ; store in output string
    ret        ; back to caller

btoa    endp



ascii    proc    near    ; convert AL bits 0-3 to
            ; ASCII (0...9,A...F)
    add    al,'0'    ; and return digit in AL
    cmp    al,'9'
    jle    ascii2
    add    al,'A'-'9'-1    ; "fudge factor" for A-F
ascii2    ret        ; return to caller

ascii   endp

_TEXT   ends

    end    entry



Figure 10

■
    ■
    ■

myflag    dw    ?    ; variable to be incremented
            ; on each timer-tick interrupt

oldint8 dd    ?    ; contains address of previous
            ; timer-tick interrupt handler
    ■
    ■        ; get the previous contents
    ■        ; of the Interrupt 08H vector...
    mov    ax,3508h    ; AH = 35H (Get Interrupt Vector)
    int    21h    ; AL = Interrupt number (08H)
    mov    word ptr oldint8,bx    ; save the address of the
    mov    word ptr oldint8+2,es    ; previous Int 08H Handler
    mov    dx,seg myint8    ; put address of the new
    mov    ds,dx    ; interrupt handler into DS:DX
    mov    dx,offset myint8    ; and call MS-DOS to set vector
    mov    ax,2508h    ; AH = 25H (Set Interrupt; Vector)
    int    21h    ; AL = Interrupt number (08H)
    ■
    ■
    ■


myint8:        ; this is the new handler
            ; for Interrupt 08H

    inc    cs:myflag    ; increment variable on each
            ; timer-tick interrupt

    jmp    dword ptr cs:[oldint8]    ; then chain to the
            ; previous interrupt handler



Figure 11

myint8:            ; this is the new handler
            ; for Interrupt 08H

    mov    ax,1    ; test and set interrupt-
    xchg    cs:myflag,ax    ; handling-in-progress
            ; semaphore

    push    ax    ; save the semaphore

    pushf        ; simulate interrupt,
            ; allowing the previous
    call    dword ptrcs:oldint8    ; handler for the
            ; Interrupt 08H
            ; vector to run

    pop    ax    ; get the semaphore back
    or    ax,ax    ; is our interrupt handler
            ; already running?

    jnz    myint8x    ; yes, skip this one


    ■        ; now perform our interrupt
    ■        ; processing here...
    ■


    mov    cs:myflag,0    ; clear the interrupt-
            ; handling-in-progress
            ; flag

myint8x:
    iret        ; return from interrupt



Advanced Techniques for Using Structures and Unions  In Your C Code

 Greg Comeau

Structures, unions, and typedefs, constructs essential to organizing data in
your programs, are often misunderstood and poorly used. In "Organizing Data
in Your C Programs with Structures, Unions, and Typedefs," MSJ (Vol. 4, No.
2), I attempted to clear up some of the mysteries surrounding the use of the
constructs. In this article, I build on that discussion by examining
pointers to structures and then move on to some of the finer points of
multilevel structure access, memory allocation, and arrays.

The previous article may have convinced some of you to use typedef more
often in your programs. However, when doing so, you should be aware of one
particular problem; structures that contain references to themselves in the
form of a pointer can begin to produce mysterious syntax errors. Figure 1
shows an example of such a situation. The problem here is that the typedef
for the identifier s1 has not yet been completed on line 2; therefore the
compiler cannot acknowledge the existence of the s1 typedef or even the
existence of any name for s1 and must deduce that an invalid type is being
used.

The case shown in Figure 2 is a different situation but a similar problem.
Two typedefs are set up; however, they each refer to the other (in circular
fashion). In this instance, the first typedef always gives an error since
the second one does not yet exist.

Is there a solution to these situations? There are a few; however, some are
wrong and some are messy. Without a clear syntactic way to remedy this, many
programmers are tempted to change the s1 * and/or the s2 * into char * (yes,
either one since the typedefs may actually be found in #include files whose
order of occurrence in your source file is not determinable) and then, when
using or assigning to s1ptr or s2ptr, will cast it into s1 * or s2 *,
respectively. This is a mess and a sure way to get into trouble with a
different compiler or hardware. Casting into s1 * or s2 * is a sure bet that
something will go wrong sooner or later since the cast will keep the
compiler from saying anything (that is, a cast is a directive to tell the
compiler that you've decided that you know what you're doing with a
mismatched type. Suffice it to say for now that even though a cast implies
portability, it in no way guarantees it).

The description of the problem having been stated, let's discuss possible
ways to go about solving it. We'll begin by breaking the problem down into
various steps. As we move along I will introduce some other concepts that
you might not be aware of along the way. Let me just say in passing that
Pascal fans will be glad to hear that the solution is to use a forward
reference. We will go through several variations of this technique.

Figure 1

1   typedef struct {
2       s1      *s1ptr;
3       char    s1data[100];
4   } s1;
5
6   main()
7   {
8       s1    s1instance;
9
10       /* ... */
11   }

Figure 2

1  typedef struct {
 2      s2      *s2ptr;
 3      char    s1data[100];
 4  } s1;
 5
 6  typedef struct {
 7      s1      *s1ptr;
 8      char    s2data[100];
 9  } s2;
10
11  main()
12  {
13      s1    s1instance;
14      s2    s2instance;
15
16      /* ... */
17  }

Forward References

Let's make the problem simpler for a moment by removing the typedef from the
examples (Figure 3). At this point it should be obvious that the ability to
reference a pointer to s1 or s2 in the code sample does not exist. In other
words, s1 and s2 are clearly not types. However, what if we were to add a
structure tag to each declaration first so that the structure shape can be
referenced instead (Figure 4)? The key here is that now s2ptr and s1ptr must
not deal with a "type of;" they only serve as references to some
structure_tag.

Note, however, that though a forward reference through a pointer declaration
is legitimate, an instance of the structure is not. If we look at Figure 5,
the declaration of anotherptr is fine, since the pointer does not yet have
to know what the shape of the structure it points to (the shape of the
struct anothertag) looks like. In C, this is one way that an incomplete type
can exist. Of course, if you wish to use anotherptr beyond having the
pointer assigned to it, you must first define the structure to which it
points.

Similarly, the declaration of anotherinstance will fail, since I was
attempting to include a whole instance of anothertag within sometag. This
can't happen because the compiler has no way to determine sometag's members
and therefore it is sizeless. The compiler can't delay this either since
sometag would not have a valid size and shape until anothertag had a size
and shape, thus creating an error.

The problem of incomplete types in C is an important one to be aware of. It
actually goes well beyond the discussion presented here and is not related
to structures only. In general, you can always refer to an incomplete type
but can never use it as a single entity, that is, as a unit by itself, since
it is not completely defined. Another popular place where incomplete types
are commonly used is with external arrays, for example, in a declaration
such as extern int array[ ];. Here you can index and get the address of the
array as normal; however, you cannot perform some operations such as
sizeof(array) since the dimension of the array may not be in the same source
file of such a declaration. This happens because array only serves as a
declaration and not a definition.

Figure 3

1   struct {
 2       s2      *s2ptr;
 3       char    s1data[100];
 4   } s1;
 5
 6   struct {
 7       s1      *s1ptr;
 8       char    s2data[100];
 9   } s2;
10
11   main()
12   {
13       s1    s1instance;
14       s2    s2instance;
15
16       /* ... */
17   }

Figure 4

1    struct s1_tag {
 2        struct s2_tag  *s2ptr;
 3        char    s1data[100];
 4    };
 5
 6    struct s2_tag {
 7        struct s1_tag  *s1ptr;
 8        char    s2data[100];
 9    };
10
11    main()
12    {
13        struct s1_tag s1instance;
14        struct s2_tag s2instance;
15
16        /* ... */
17    }

Figure 5

1    struct atag {
 2        struct anothertag  *anotherptr;
 3        /* reference to another tag before
 4           it comes into scope is fine */
 5    };
 6
 7    struct sometag {
 8        struct anothertag  anotherinstance;
 9            /* This is an error since a
10               description of anothertag
11               is not in scope */
12    };
13
14    main()
15    {
16        /* ... */
17    }

Typedef/Structure Tags

Generally speaking, when you are considering the use of #define and typedef,
it is usually safe to conclude that typedef is the better choice. Here I
have introduced yet another major source of confusion when using structs by
using structure tags. However, do not get the mistaken impression that I'm
saying tags are better than typedefs, since this is not necessarily the
case. I've only used tags as a stepping stone.

For instance, now that forward references are clearer, let's turn back to
the original problem and solve that using typedefs. There are two common
ways to set this up (see Figures 6 and 7)--both are equivalent. In the
first, two typedefs are set up that refer to structure tags (and since a
typedef only serves as a synonym, we only care that it matches a structure
tag that can be defined at a later time). This brings the two typedefs into
scope and lets you use them from that point onward, simply using the names
s1 and s2 as if they were types. This is true even when using them within
the definitions of the structure tags that they were based upon, as shown in
Figure 6.

The second method (Figure 7) allows us to produce the same result using a
different method. Actually it's the reverse of the previous procedure:
instead of typedefing the names and creating the structure tags last, we'll
create the structure tags as we're typedefing by using structure tags
internally.

Figure 6

1    typedef struct s2_tag s2;
 2    typedef struct s1_tag s1;
 3
 4    struct s1_tag {
 5        s2  *s2ptr;
 6        char    s1data[100];
 7    };
 8
 9    struct s2_tag {
10        s1  *s1ptr;
11        char    s2data[100];
12    };
13
14    main()
15    {
16        struct s1_tag s1instance;
17        struct s2_tag s2instance;
18
19        /* ... */
20    }

Figure 7

1    typedef struct s1_tag {
 2        struct s2_tag  *s2ptr;
 3        char    s1data[100];
 4    };
 5
 6    typedef struct s2_tag {
 7        struct s1_tag  *s1ptr;
 8        char    s2data[100];
 9    };
10
11    main()
12    {
13        struct s1_tag s1instance;
14        struct s2_tag s2instance;
15
16        /* ... */
17    }



Passing/Returning Structs

I'm assuming that the reader knows how to pass structures to functions. It
should be sufficient to say that you should almost always pass a pointer to
a structure or the address of a structure (&struct_var, which is mappable to
a pointer to a structure) as the argument to a function instead of the
actual structure itself. In other words, pass the structure by reference,
not by value. The latter case can require an unreasonable amount of stack
space and, if you are not careful, can cause symptoms that include random
crashes, lockups, invalid pointers, and other such problems.

Returning a structure should also be through a pointer, mostly for the sake
of efficiency. This involves three areas: returning a complete structure
from the function, returning a pointer to a structure, and the case where
just some of the structure's members are accessed after function(s) return
them.

Returning a Structure

When returning a complete structure from the function, consider what the
compiler must go through. A typical compiler might copy the structure into a
static area set aside by the compiler and linker. This area can be thought
of as a union of every structure in your program. This of course means that
it must be large enough to hold any given structure. It also needs to be
addressable without any idiosyncrasies, and must be static with suitable
alignment qualities.

This copying alleviates much of the game playing the generated object code
goes through since there is usually a standard function calling and return
convention that the compiler writer will try to make the code adhere to.
There are, however, problems. Consider the program in Figure 8, specifically
the call to func on line 25. This should work as if f1 and f2 return base
types, since you are, after all, allowed to return structures from
functions. Note the series of events that must occur--f1 will return a
struct ps1 by copying it into a static area; next that static area might be
copied onto the stack, which is accomplished on some systems by generating
lots of "move long word" instructions instead of a loop. The same will be
performed for f2's return value.

The end result is reached by a slow and tedious process. But that's only
half the problem. If you pass the structures returned from f1 and f2 by
their addresses as shown in line 26 (you can do this since a structure is an
aggregate and not a simple scalar), you may find that your compiler does not
work as expected. Both f1 and f2 might return the same address (remember our
friend provided by the linker), thus the addresses and strings printed out
by func2 could be the same.

Figure 8

1    struct ps1 {
 2        char    *p;
 3        char    array[1024 - sizeof(char *)];
 4    } v1;
 5
 6    struct ps2 {
 7        char    *p;
 8        char    array[512 - sizeof(char *)];
 9    } v2;
10
11    struct ps1 f1()
12    {
13        v1.p = "hi there";
14        return (v1);
15    }
16
17    struct ps2 f2()
18    {
19        v2.p = "HI THERE";
20        return (v2);
21    }
22
23    main()
24    {
25        func(f1(), f2());
26        func2(&f1(), &f2()); /* This doesn't mean the
27                             address of f1 and f2!
28                             It means the address
29                             of the structures they
30                             return--Remember
31                             precedence! */
32    }
33
34    func(struct ps1 v1, struct ps2 v2)
35
36
37    {
38        printf("%s\n", v1.p);
39        printf("%s\n", v2.p);
40    }
41    func2(struct ps1 *v1, struct ps2 *v2)
42
43
44    {
45        printf("%ld\n", v1); /* print out the addrs
46                                that v1 and v2 point
47                                to, these */
48        printf("%ld\n", v2); /* may also be printed
49                                with a %p instead of
50                                %ld */
51        printf("%s\n", v1->p);
52        printf("%s\n", v2->p);
53    }

Returning a Pointer

The second case is quite simple: don't return a pointer to a structure
that's automatic. Figure 9 shows an example of such misuse. Notice that func
returns &temp, which is perfectly valid C but not valid logic. When func
exits, the temp structure--not the temp tag, which has file scope in
this example--will terminate since it is local to a function and
nonstatic. Since temp has terminated, accessing it will result in undefined
behavior and most likely a program crash.

Note that changing the declaration to static struct temp temp; will not
cause any problems. Because temp is now static it will have a permanent
existence during the life of your program. In this case it doesn't matter if
the function that houses temp is currently executing or not; the fact is
that temp is addressable since it's static.

Additionally, whether temp is in scope or not is irrelevant here. Scope
relates only to identifiers. It does not deal with concepts such as
variables--for instance setting a pointer to an absolute position and
accessing it--or things like addresses. Of course you will only be able
to obtain an identifier's address while it is in scope; but once you have
the address of an object, during its lifetime it's yours to do with as you
please (within reason of course).

Figure 9

1  struct temp {
 2      int    members;
 3      /* ... */
 4  };
 5
 6  struct temp *
 7  func()
 8  {
 9      struct temp temp; /* yes, structure tags *and*
10                           structures can have the
11                           same name */
12
13      return (&temp);
14  }
15
16  main()
17  {
18      struct temp *temp;
19
20      temp = func();
21      /* <expressions using temp->???> */
22  }
23



Member Access

The last scenario is one in which a function returns a structure, but you're
only interested in accessing a few of its members. Think about this for a
moment. Should you access the members by returning the structure and be hit
with all the copying or should you take the easy route by returning a
pointer to the structure?

In the case of an operating system call, you'll rarely want a pointer to one
of the internal data structures of the operating system unless you're
writing a device driver or a very specialized application that requires some
special knowledge.

In another situation you might find you're calling a C library function
instead of a system call, and are most likely accessing something in your
"process's space." But the arguments to the particular library you are using
are usually a given and neither can nor should be changed. Routines may
accept an argument that's a pointer to a structure (one that you've properly
allocated based on some #include file) and completely avoid a return value,
which would have been a structure.

The routine itself will set the structure's members as might be appropriate.
This will prevent the situation I've presented and is a viable way to code
some of your programs. In addition, you might find routines of your own that
use large structures, and instead of passing whole entities back and forth,
it is often worth creating another smaller structure which is a subset of
the larger one and manipulating that instead.

Depending upon your logic and program design, you will also find it
worthwhile to return a pointer to a structure even if only one of two
members is going to be used. You only have to apply the arrow operator (->)
to the returned pointer to access the member in question, rather than
dealing with the whole structure. Remember that you may be calling a
function containing an instance of the structure it's returning a pointer to
as an internal static identifier. However, the consequence is that calling
the function twice could destroy the previous value of the structure, so the
calling function must account for this and copy all the data it needs before
calling the previously called function again.

This presents another situation in which either the caller of the function
or the function itself dynamically allocated the structure. You must
therefore be careful to control the allocation of the structure and be just
as careful to make sure that you free it properly.

Structure Access

Figure 10 contains both structures with pointers to other structures and
instances of the other structures. Chances are that you will never encounter
this specific situation. Nevertheless I've included it to provide some
further insights into structures in general.

Most of you can no doubt construct a simple structure member reference as in
line 21 of Figure 10. Some of you can even create a multistructure or
multilevel reference as in line 23--but without certainty that it is
correct (it is). Anything beyond this, however (meaning those nasty
creatures we call pointers), is unfamiliar territory.

To understand line 23, you must be aware that s3 contains an instance (that
is, an occurrence) of an s2tag structure (an s2tag structure named s2inst),
which in turn contains an instance to an s1tag structure (s1inst), which of
course contains an instance of an integer identifier named s1var.

To use each of these instances, you simply create a structure access to the
member, as shown in line 21. Since the operator precedence of the dot (.)
operator presents no problem and its associativity is naturally oriented to
be left to right, the setup is simply

structure1.<...>.structureN

which line 23 shows.

Remember that this all takes place within s3 because s3 contains an s2tag
and s2tag contains an s1tag. Under a different case, such as in lines
25-28, the code will use references to s3tag, s2tag, and s1tag by way
of ps3, ps2, and ps1, which go beyond the limits of s3 to gain access to
other variables such as s2 and s1.

On line 25, ps3 is set equal to the address of s3. Note the use of &s3
instead of s3 since we want to obtain the address of s3 and not an
assignment to the pointer of the actual contents of the s3 structure. Once
this is done, we can reference the members of the structure (s3 in this
case) that ps3 points to by using the -> operator. It happens that we want
to assign to ps2, which is another structure pointer (a pointer to an
s2tag). This assignment will take place just as smoothly as the ps3
assignment.

The evaluation of the arrow operator will take place in exactly the same
order of precedence as the dot operator. Again, this will occur with left to
right associativity. Our knowledge of line 23 coupled with this discussion
should make the interpretation of lines 27 and 28 very easy. Briefly then,
line 27 uses the fact that ps3->ps2 references an s2tag, which contains a
reference to an s1tag. This allows ps3->ps2->ps1 to be assigned to an
identifier such as s1, which has an s1tag size and shape. Line 28 uses
ps3->ps2->ps1, which is a pointer to an s1tag and therefore a reference to a
member such as s1var is possible as well.

Every pointer used in lines 25-27 needed to be initialized. You could
not simply have coded the access to s1var in line 28 without the other
assignments. That would not be valid logic since every pointer in this
example must access a memory location in order to be used.

Be careful here since this constraint is something that you as the
programmer must take care to enforce. The compiler doesn't care, for
instance, that you may have coded line 28 without lines 25 through 27. You
may actually have performed the structure pointer assignments in those lines
in another part of the program based on if/else logic and not necessarily
right before line 28. In general the compiler has no easy way of determining
this.

The moral is to make sure all your assignments and pointers are set up
properly since the compiler is not going to give you a warning or error
message for failing to initialize them correctly. Problems of this nature
will typically begin to crop up during the execution of your program; more
often than not they will be sporadic and very hard to debug. If you provide
the extra ounce of prevention during coding, you will avoid many of these
situations.

Line 29 really means less than you might think it does. If you take a closer
look at the code, you will see that the execution of line 27 allowed line 28
to gain access to s1.s1var. However, this may work properly even if ps3
and/or ps3->ps2 are not valid pointers. Or I should say it appears to work
properly--can you see the problem here?

If we work under the assumption that line 25 was accidentally deleted from
the source file, would the program continue to work properly? Perhaps. If it
did continue to work, should it have? No. As explained above, it would most
certainly compile so that's not the concern here. It would most likely
execute under DOS as though nothing were wrong. However, most versions of
the XENIX(R) operating system, as well as OS/2 systems, would produce a
general protection fault because of the invalid memory access.

This would occur because ps3 is an external (that is, an external defined in
the same source file with no initializer) variable and would therefore be
implicitly initialized to zero. If this is the case, then line 26 would be
indexing ps2 off a pointer that points at memory location zero. That
assignment would then be assigning something to a memory location of 0 +
sizeof(int), which may map into memory location 2 and then be treated as the
location of ps3->ps2, which we all know is wrong. However, if an access to
that location at that moment in time does not stomp on something it
shouldn't (and in many cases this is not as clear as the scenario that I'm
describing), execution will continue with no apparent damage.

Microsoft(R) C Optimizing Compiler versions 5.0 and later usually protect
against the case above with their infamous R6001 run-time error message upon
program termination; however, this is not something you should particularly
depend upon, and it shouldn't be relied on to help solve your problems.
Furthermore, the R6001 error functions only when your program writes in low
core. Anything beyond that certain magic number and you're on your own.
Therefore, make sure that your pointers are always legitimate regardless of
whether or not your program executes properly.

Clearly looks can be deceiving in this case. An excellent  example of this
scenario is when you merge together different stubs to your program and
suddenly something seems to be going wild. Most likely this is due to
invalid pointers pointing to locations that cannot be touched without
causing a problem. This would occur because your program modules' variables
and functions will most likely be in different memory locations as well as
in a larger program. When this happens, errors that didn't show up in the
stubs will begin to appear.

Given the preceding information, we're now ready to tackle some of the other
derivations in Figure 10. For instance, if we wanted to access s1inst
indirectly through the ps2 pointer, we might use code similar to line 31. If
we read this left to right (since both -> and . have equal precedence and
left to right associativity), you'll notice that the part referring to
ps2.s1inst is in error. Since ps2 is a pointer, constructing anything with
ps2. (note the dot) is not going to work because the dot operator requires
that the left operand have a structure type. This structure type must be
either an identifier reflecting a structure name, or a dereference of a
parenthesized pointer to structure type as in ps=s;  (*ps).m;.

The proper way to do this is shown on line 32, which is the same as line 27,
only instead of referencing another pointer (ps1), we're accessing a real
structure (s1inst).

You may see another solution to this problem since you should know from your
knowledge of C that a structure reference such as pointer->member is
translatable into (*pointer).member. However, how do we implement it in this
case? Line 33 seems like a good possibility to resolve this problem (let's
not even consider the syntax of line 34) but it does not go far enough.
Unfortunately, compiling this will not clue you in any better since most
compilers will simply report a syntax error. This isn't immediately
intuitive--at least not until you see exactly what's going on behind
the scenes.

You need to ask yourself: What exactly is ps2? We know it's a pointer, and
we think we know its name but in fact its name is not ps2. If it were, you
would be able to say something like ps2 = 0; and that is clearly
unreasonable (compile it to prove it), since there is no identifier with a
name consisting solely of ps2. Note that there is one named s3.ps2 and
another that can reference it through pointer notation, as in ps3->ps2.
Therefore, these are two names we must refer to in this particular program
when accessing ps2.

Tying this back to the (*pointer).member notation discussion above, the
correct syntax for line 33 is shown in line 37 since ps3->ps2 is the proper
pointer reference to ps2. This in no way infers line 38 is equivalent to
line 37. As discussed in the previous article, this line would produce an
error because of the precedence of the * operator.

As a final note, make sure that you understand that lines 32 and 37 do not
access the same s1var as in line 38. Again, recall that an s3tag contains a
pointer to an s2tag and an instance of an s2tag--they are not the same
thing. You could point the s2tag pointer (ps2) to the s2tag instance
(s2inst), but if you had wanted to do that, you'd need to code lines
39-42.

Figure 10

1   struct s1tag {
 2       int    s1var;
 3   } s1;
 4
 5   struct s2tag {
 6       int    s2var;
 7       struct s1tag *ps1;
 8       struct s1tag s1inst;
 9   } s2;
10
11   struct s3tag {
12       int    s3var;
13       struct s2tag *ps2;
14       struct s2tag s2inst;
15   } s3;
16
17   struct s3tag *ps3;
18
19   main()
20   {
21       s1.s1var = 99;
22
23       s3.s2inst.s1inst.s1var = 5;
24
25       ps3 = &s3;
26       ps3->ps2 = &s2;
27       ps3->ps2->ps1 = &s1;
28       ps3->ps2->ps1->s1var = -99;
29       printf("s1var=%d\n", s1.s1var);
30
31       /* ps3->ps2.s1inst.s1var = 11; */
32       ps3->ps2->s1inst.s1var = 22;
33       /* ps3->(*ps2).s1inst.s1var = 33; */
34       /* ps3->*ps2.s1inst.s1var = 44; */
35       /* ps2 = 0; */
36         /* might as well have called this 'abccba' */
37       (*ps3->ps2).s1inst.s1var = 55;
38       /* *ps3->ps2.s1inst.s1var = 66; */
39       printf("s3.s2inst.s1inst.s1var=%d\n",
40                 s3.s2inst.s1inst.s1var);
41
42       ps3->ps2 = &ps3->s2inst;
43       ps3->ps2->s1inst.s1var = 22;
44       printf("s3.s2inst.s1inst.s1var=%d\n",
                   s3.s2inst.s1inst.s1var);
45   }

Multilevel Structure Access Involving Functions

There are many situations in which you may call functions that return
structures, or pointers to structures and the use of temporary variables
becomes a nuisance. Consider the function example1 in Figure 11. Is it clear
that all of the lines from 27 through 30 will print out 12345?

Line 27 ought to be self-explanatory as a reference to s1.s1var, which has
been statically initialized to the value 12345. Line 28 involves a structure
analysis exactly like any other, with the same precedence and associativity
of operators involved. Do you care that there is a ( ) involved? No, because
you memorized the top line of the Operator Precedence/Associativity chart.
Therefore since s1returner returns an s1tag type and explicitly returns s1,
why not just treat it like line 27? It's simply a structure.member
reference.

Line 29 presents a function that returns a pointer to an s1tag, but nothing
is that different here even though it does involve a function. If it returns
a pointer, access it as a pointer->member reference. Of course line 30 is
just a familiar pointer->member case being mapped into (*pointer).member.

These variable references are all rather natural. The alternative is to code
something along the lines of the statements shown in the example2 function.
As you can see, this makes things longer and more tedious than necessary and
in this program needn't be used. This point hits home when you realize that
you may have a second structure involved, such as s2, and as in the previous
section, there are pointers and references to all sorts of variables. This
would result in code such as that found in example3 and example4 (or even
more complex situations).

Figure 11

1   struct s1tag {
 2       int    s1var;
 3   } s1 = { 12345 };
 4
 5   struct s2tag {
 6       int    s2var;
 7       struct s1tag *ps1;
 8       struct s1tag s1inst;
 9   } s2;
10
11   struct s2tag *ps2;
12
13   struct s1tag
14   s1returner()
15   {
16       return (s1);
17   }
18
19   struct s1tag *
20   ps1returner()
21   {
22       return (&s1);
23   }
24
25   void example1()
26   {
27       printf("%d\n", s1.s1var);
28       printf("%d\n", s1returner().s1var);
29       printf("%d\n", ps1returner()->s1var);
30       printf("%d\n", (*ps1returner()).s1var);
31       printf("%d\n", (ps1returner())->s1var);
32       /* printf("%d\n", (ps1returner()).s1var); */
33   }
34
35   void example2()
36   {
37       struct s1tag s1holder;
38       struct s1tag *ps1holder;
39
40       printf("%d\n", s1.s1var);
41       s1holder = s1returner();
42       printf("%d\n", s1holder.s1var);
43       ps1holder = ps1returner();
44       printf("%d\n", ps1holder->s1var);
45       printf("%d\n", (*ps1holder).s1var);
46   }
47
48   struct s2tag
49   s2returner()
50   {
51       return (s2);
52   }
53
54   struct s2tag *
55   ps2returner()
56   {
57       return (&s2);
58   }
59
60   void example3()
61   {
62       printf("%d\n", s2.s1inst.s1var);
63       printf("%d\n", s2returner().s1inst.s1var);
64       printf("%d\n", ps2returner()->s1inst.s1var);
65   }
66
67   void example4()
68   {
69       printf("%d\n", ps2returner()->ps1->s1var);
70   }
71
72   main()
73   {
74       example1();
75       example2();
76
77       s2.s1inst.s1var = 54321;
78       example3();
79
80       s2.ps1 = &s1;
81       example4();
82   }

Structures and Malloc

There is one other important point to consider. Typically, one must deal
with packets of information. In other words, you may find that you're being
fed some group of data that is prefixed with control or status information
and is then followed by the actual data being described. This would normally
occur when dealing with communications, but it needn't occur only there. The
problem this presents to the C programmer is that the information part of
the packet is finite and predictable, whereas the data portion may be
variable in length. For instance, you may find that the following structure
is sufficient for describing the control information:

struct apacket {
   int   packethead;
   int   packetcontrol1;
   int   packetcontrol2;
   char  data[???];
};

But how large do you make the dimension of data[ ]?

If you make it too small, you could lose data. If you make it too large, you
could waste critical space in your program or machine state. Make it
reasonably large, and you may not know if that size will be too small for
the future--then what do you do? You could add another field that
encodes the size of the data length, then derives some unions and associated
code to perform something like the codedrecord example in the previous
article. I'm sure we can all agree, however, that that would be a big mess.

The best answer is to avoid the constraints altogether and work with what
you have been given. In other words, since you know what the members of the
structure that represent the control information are, why not use that to
your advantage? This, along with the ability to allocate memory dynamically
through standard library routines, such as alloc, calloc, and malloc, is all
you need.

A first attempt at a solution might result in a structure template and
sample code such as:

struct apacket {
   int    packethead;
   int    packetcontrol1;
   int    packetcontrol2;
   int    packetsize;
   char   data[0];
};
    < other code >
struct apacket apacket;
struct apacket *ppacket;
ppacket = (struct
   apacket *)malloc(
   sizeof(struct apacket) +
   apacket.packetsize);

Unfortunately, C does not allow for zero-length data items to occur, even in
structure tags. (Note that some compilers allow this, but they are clearly
in error. This is not a portable construct and should be completely
avoided.)

Many of you will be quick to point out that making data into a 1-dimensional
array with a bound of 1 (for example, char data[1]) should work without a
problem (which is true). Note that the 1-byte length will need to be
subtracted from the value that's being passed to malloc. This length can be
presented as sizeof(char), sizeof(char [1]), or simply the constant 1 since
in this case they all represent and refer to the same thing. By the way,
because sizeof can accept a derived type as its argument, the second sizeof
says, "give me the size of an array of x characters, where x is 1."

An equal yet slightly less messy approach is to make use of the offsetof
macro. Under this circumstance, the structure tag would still contain a char
data[1], however instead of

ppacket = (struct
        apacket *)malloc(
sizeof(struct apacket) -
   sizeof(char[1]) +
   apacket.packetsize);

you would code:

ppacket = (struct
        apacket *)malloc(
offsetof(apacket, data) +
   apacket.packetsize);

since the offset of data within apacket would represent the length of the
previous fields (in our case all the fields) in apacket. I feel this is a
superior method. With offsetof it's perfectly clear: get the size of the
structure and add it to the size desired for the data.

Once you have a pointer of the correct length, you may copy the structure
members as appropriate. However, before leaving this subject, let me once
again refer you to the March article and encourage you to reread the
sections dealing with structure holes and structure packing since you may
find that you will need to set up your structures in the same "manner" as
the one(s) which you are copying it from.

This scheme of creating dynamic structure images is, as most things, not
without problems and requires some careful thought before you use it. For
instance, it should be clear that if you use this scheme, every reference to
the packet's members will be through a pointer.

This is due to the variable length data that must remain as a single unit.
It's important to note that if you do not need a variable length structure,
you should think about organizing your data in a less elaborate way. For
example, you could change the apacket structure tag layout so that data is
not an array but a character pointer.

Allowing for this circumstance will usually be far more natural, since
apacket instances must be set up (using a char *data; construct) and then
referenced. Note that now all the structure members can be accessed directly
with the dot operator. The close relationship between arrays and pointers
allows data to be referenced in array notation, if desired, as follows:

struct apacket somepacket;
somepacket.data =
    (char *) malloc(
somepacket.packetsize);
    <...>
somepacket.data[i] =  <...>;
    <...>
*somepacket.data = <...>;
/* Note precedence with
   no parens! */
    <...>

The only ramification of this example is that you will need to free the
memory block associated with somepacket.data when you no longer need the
data (for dynamically allocated data, the programmer controls the lifetime
of the memory block).

Arrays

Since we are on the subject of dynamic memory, it's worth dwelling a bit on
arrays and structures. First, look at line 21 of Figure 12. By now I would
hope that you wouldn't have much of a problem interpreting it. Nor should
you have a problem deciphering lines 24-26, where a pointer to a
structure is set equal to an element of an array (which is therefore using
only one structure).

The use of arrays (and pointers) implies that their usage and idiosyncrasies
remain the same regardless of whether they are used inside structures or as
structures (for instance, the syntax and semantics of passing an array to a
function does not change because the array might consist of structures
rather than a base type like int).

I'd also like to direct your attention to the use of HBOUND that occurs on
line 20. It's actually very simple (as lines 17-18 demonstrate) and is
very handy when dealing with a good many array situations.

Finally, if you needed to use and access s1 strictly via pointers, an
alternate to lines 20-22 can be found in lines 29-35. The latter
lines are surely more cryptic; however, they do remove the indexing
notation, which would be advantageous if you were to reference other
elements of the structure or even repeated occurrences of the same element.

Returning to HBOUND, you should study the constructs on lines 29 and
32-33. The code is checking to see if the pointer has gone beyond the
end of the array by checking it against an array dimension that is one
greater than the array. In other words, ps1 will be checked against &s1[10],
which is one greater than nine (remember that in C arrays start at element
0). Similarly, cp will be checked against &ps1->charray[20]. Note in
particular how all of this occurs without having to explicitly use any
constants in either of the two nested loops that occur in this program. You
should strive for this type of construction in your own programs, whether
you are dealing with structures or not.

Figure 12

1   #define HBOUND(array)  (sizeof(array) /
 2     sizeof(array[0]))
 3
 4   struct s1tag {
 5       char    charray[20];
 6   };
 7
 8   struct s1tag s1[10];
 9   struct s1tag *ps1;
10
11   main()
12   {
13       int     i;
14       int     j;
15       char    *cp;
16
17       printf("%d/%d=%d\n", sizeof(s1),
18                  sizeof(s1[0]), HBOUND(s1));
19
20       for (i = 0; i < HBOUND(s1); i++)
21      for (j = 0; j < sizeof(s1[0].charray); j++)
22          s1[i].charray[j] = '\0';
23
24       ps1 = &s1[5];
25       ps1->charray[2] = 5; /* Note that this is ASCII 5 */
26       (*ps1).charray[2] = 5; /* not '5'                 */
27
28       printf("%d\n", sizeof(ps1->charray));
29       for (ps1 = &s1[0]; ps1 < &s1[HBOUND(s1)];
30              ps1++) {
31      cp = ps1->charray;
32      while (cp < &ps1->charray[sizeof(
33                                    ps1->charray)])
34          *cp++ = '\0';
35       }
36   }

Summary

Although this article has focused specifically on structures and the
different aspects   of having pointers to structures, accessing struct
members, avoiding memory conflicts with arrays, and memory allocation, most
of these concepts apply to unions as well. With the information presented
here, added to the insights presented in the previous article, you now have
a solid base of knowledge concerning the use of  structures. When you add to
this the insights into unions, typedefs, and C declarations that you have
been storing up from the past several issues, you are ready for some serious
C programming.

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

Volume 4 - Number 4

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


Circumventing DOS Program Memory Constraints with an Overlay Manager

 Dan Mick

In the current state of computing, both the PC end user and the programmer
have an incredible array of software tools at their disposal. Thousands of
terminate-and-stay-resident (TSR) utilities add timesaving, useful
features to MS-DOS(R) and PC-DOS operating system machines, while
integrated environments and multitasking shells extend the PC's power. But
all these tools use memory, sometimes a good deal of it. Therefore,
application programmers need to be certain their code is well designed,
using small utility modules to decrease duplicated routines. Although such
optimization can save a lot of code space, sometimes it isn't enough. In
such cases, an overlay manager may be just the thing an application needs.

The overlay is a fairly common concept for data space; the C programmer will
be familiar with malloc and free to manage dynamic memory, as will the
Pascal programmer with New and Free, Mark and Release. Overlaying may be
thought of as a kind of dynamic code memory allocation.

The basic concept of the overlay manager is that not every routine in a
program needs to be loaded into memory at the same time. In a word
processing program, for example, the printing output routines for a document
will probably never be active while that document is being edited. Overlays
make it possible for the printer routines to stay on disk until needed, at
which point the printing code can be put in memory and the editing code can
be put on disk (to be read later or to be discarded). This way, the word
processor uses less memory. Another example is the printer driver code.
Since only one printer is used at a time, only that printer driver is loaded
at a time.

Using Overlays

The only justification for overlaying code is the decrease in resident code
size, since speed will suffer as a result of disk loading times. But
decreasing code size is important.

No matter what the overlay scheme, the programmer's job is much easier if
the code need not be modified greatly; in particular, the application should
be designed as a "flat" application to aid in development and debugging,
with the overlay management added late in the development cycle. (Debugging
an overlaid program can really give you headaches.) Also, the decisions
concerning how the program is overlaid are best delayed until late in the
development cycle, since the complexity and size of routines may make a
difference. If it's easy to add overlay management, it can be added late in
the cycle without requiring a lot of editing to existing working code.

TSRs can make life easier, as programmers and users realize. Editors,
calendars, alarms, datebooks, help and documentation, communications, even
games are nice to have available at the touch of a key, but they come at the
expense of precious memory. It's not difficult to have two or three
favorite utilities and find out there's no longer room for much editing or
compilation on the system. Overlaying TSRs is a natural, since they're
dormant most of the time; it's only when the TSR is active that any code
is needed. At that point, the memory could be allocated from DOS and the
code loaded; it could then be released when the TSR is deactivated.

It's nice to be able to have some TSRs loaded while running other, more
complex programs in which all the code is not needed. A word processor
could be a desktop-publishing-style program that takes up 500Kb, leaving no
room for anything else to be loaded. But if it were overlaid, it might use
only 300Kb, leaving room for whatever else the user might desire.

The speed of the application will suffer--but only when moving from one
overlay to another. With good planning, the programmer can ensure that the
swapping time is minimal, or perhaps even unnoticeable, buried in the time
it takes a user to read the screen output from the last command. Even if the
delay is noticeable, the program's smaller memory size may be worth the
speed tradeoff.

To alleviate some of the speed loss, the overlaid code can be stored in LIM
EMS memory, allowing it to load nearly instantly. Expanded memory is ideal
for this application, as it's not easy to actually run code from the EMS
window (at least with older versions of EMS), but it's certainly easy to
copy pages to and from the EMS window in the overlay manager. A disk cache
in EMS can, however, serve the same purpose with far less effort.

Early Approaches

Programmers have been using different approaches to implement overlays for
awhile. One early approach was built-in language statements, such as OVERLAY
FUNCTION and OVERLAY SUBROUTINE. Mainframe FORTRAN compilers often had
built-in statements for the programmer to direct the overlaying
process. Another early approach was roll-your-own overlaying, as practiced
by WordStar(R) (CP/M(R) and DOS) and Lotus(R) 1-2-3(R).

The main disadvantages to the above two approaches are code complexity and
the lack of transparency to the programmer. Extra work means extra
places to introduce bugs, and overlay-system bugs can be hard to find.

Other approaches to overlay management leave the application programmer
to do what he or she will, and the operating system to do the work of
swapping code. One approach, used on some early mainframes, is
whole-process swapping, which allowed multiple applications to share the
computer's resources by swapping the entire program to secondary storage and
gave control of the machine to the next program in line.

Demand-paged or segmented virtual-memory management, used more often today,
break the code space into smaller units than processes or programs, called
pages (or segments), and swap parts of processes rather than the entire
process. This allows finer granularity in swapping code space than
whole-process swapping, therefore using memory more efficiently and
involving less disk I/O.

There are some disadvantages, however, to allowing the operating
system to do the swapping work. For one thing, each process may suffer
more memory-management overhead during its execution since all the pages
for the entire process may not be present at once. In addition, the
operating system has to manage the swapping for all processes, so it must
approach things in the general sense, trying to manage memory well for an
"average" application--which, of course, doesn't exist. The application
itself will know more about which code needs to be coresident and which does
not. Moreover, the operating system makes its swapping decisions at
times unrelated to program execution. The program may know, for
instance, that it is now done with the initialization code forever. The
operating system has no way of telling that the initialization code is
used only once.

Current Approaches

There are several current solutions available for PC programmers who
want to overlay their applications. Phoenix Technologies, Ltd., sells a
product called PLINK that works with Microsoft(R) OBJ files, allowing
incredible flexibility in overlaying both code and data. Also, PLINK is a
nonstandard linker; this may or may not be a bad thing, but it certainly
increases the number of manuals on your desk.

Microsoft C and Microsoft FORTRAN optimizing compilers provide overlaying
capability through the Microsoft Overlay Linker (LINK) and the overlay
managers supplied with the language run-time libraries. But the Microsoft
Macro Assembler (MASM) programmer cannot use LINK's overlaying
features without those languages. Also, the Microsoft overlay manager and
linker do not support languages that Microsoft doesn't offer, such as C++,
Modula-2, and PL/I, or even Microsoft QuickBASIC.

LINK

LINK does provide overlaying capabilities that programmers can make
use of without developing in Microsoft C or FORTRAN. LINK provides the
most basic form of overlaying. One section of the program is defined as the
overlay area, and there may be several different code overlay segments (but
fewer than 256, because the selector is 1 byte). Each overlay segment is
made up of one or more modules (object files) that live, one at a time, in
the overlay area. The overlay manager is expected to install itself as an
interrupt routine (the linker changes the entry point of the program to a
location in the overlay manager so the manager has a chance to initialize
itself before the main program code is called). The linker then translates
all overlay routine calls to software interrupts, as described below, to
allow the overlay manager to load the overlay code from disk before the
control transfer takes place.

There is only one overlay segment present in the overlay area at once, but
it may involve object code from more than one module. For example, the
command line

LINK main+(p1+p2)+(p3) ;

causes LINK to make MAIN.EXE, which consists of a resident portion main, and
two overlay segments, one made up of code from modules p1 and p2, and one
made up of code from module p3.

An important point to note is that in order for this to work, modules p1,
p2, and p3 need to have class names ending in "CODE", and each needs to be a
different segment than the segment defined in MAIN. In the examples shown
here, MASM's .LARGE directive and separate source files take care of this,
but it's something to be careful of.

The far code model must be used for the overlay manager; that is, code must
be located at far addresses and use far calls to transfer control. This
enables the overlay linker to replace the far call (5 bytes) with a software
interrupt instruction (2 bytes), an overlay number (1 byte), and an offset
into the overlay area for the call (when more than one entry point into the
overlay is provided). For example, a CALL MY_ROUTINE might be replaced with
INT 3FH (which is the default overlay-interrupt-handler number), DB 01
(signifying the first overlay), and DW 0100 (representing an offset of
100H into the beginning of the overlay area as the call
destination). LINK replaces only those calls that refer to overlaid
code. Interestingly, if you examine an overlay-linked program with SYMDEB,
the overlay calls are disassembled as INT 3F; <overlay number> <overlay
offset> just as described. SYMDEB is at least partially overlay aware. The
Microsoft CodeView(R) debugger (referred to herein as CodeView) even has
hooks so that local symbols for overlay procedures are properly mapped in.

Note that this method of calling the overlay manager will not properly
handle functions that are called through pointers. If you attempt to use the
simple overlay manager presented here, and call an overlaid function
through a function pointer, LINK will not replace the call with an INT 3FH
and the overlay manager will never have the chance to load the routine.
Needless to say, this will have disastrous effects.

LINK also structures the EXE file differently for overlaid executables. In
short, there is a separate EXE header for each overlay segment, with
fields specific to the overlay routine that follows it and a
code-segment-only data block. I'll describe this in more detail below.

Global Variables

LINK inserts several global variables for the overlay manager's use. I'll
list here the ones I've examined.

■    OVERLAY_AREA, OVERLAY_END. Two segment names defined by the linker at
the very end of the CODE-class segments. OVERLAY_AREA is defined at the
beginning of the overlay area. The end of the overlay area (first
available paragraph after the largest overlay) is marked by OVERLAY_END.

■    $$CGSN. A variable containing a count of the number of separate
overlay segments. It's not used in our overlay manager, which simply tries
to find the requested overlay until there are no more in the file.

■    $$COVL. Contains the same value as $$CGSN. $$COVL is a limit on the
number of overlays, whereas $$CGSN is used as a size parameter for the
$$MPGSNBASE array.

■    $$EXENAM. The name of the executable file--base name and extension
(which will be EXE). It's not used in my overlay manager, since the full
executable name is available from DOS in the environment built for the
program.

■    $$INTNO. The software interrupt number that is assigned for
overlay manager operations. The
/OVERLAYINTERRUPT switch on the LINK command line allows this to be
changed from its default of 3FH. Since the calls will use this number, the
overlay manager should use it to pick the interrupt vector to take over.

■    $$MAIN. The original program entry point. This is the entry point
that would be used if the program were not overlaid. Once the program is
overlaid, the entry is changed to $$OVLINIT (see below). The overlay manager
initialization code can branch to $$MAIN once it has initialized itself and
taken over the $$INTNO interrupt vector.

■    $$MPGSNBASE. An array of $$CGSN words, each containing the segment
address of an overlay. $$MPGSNBASE[0] contains the segment address of the
resident section of the program (that is, the PSP address + 10H), and the
entries 1-$$CGSN contain OVERLAY_SEGMENT. $$MPGSNBASE is used to find the
segment address of the requested overlay segment.

■    $$MPGSNOVL. An array of $$CGSN bytes, each one of which contains an
overlay number corresponding to an entry in $$MPGSNBASE. $MPGSNOVL[0], the
entry for the resident section, is null and each entry thereafter is equal
to its index.

■    $$MPOVLLFA. A cache for overlay file offsets in the Microsoft overlay
manager. There are as many doubleword entries as overlays, and
$$MPOVLLFA[n] is either zero (as initialized) or set to the location in the
file for the overlay numbered n. Although it could speed up large
applications significantly, I decided not to use this in my overlay
manager. For small tests the time spent searching the executable is minimal.

■    $$OVLBASE. A symbol equal to the address of OVERLAY_SEGMENT. This seems
to be a conveniently initialized, alternate way of finding out  where to
load the overlay. I used its value instead of OVERLAY_SEGMENT; either would
be appropriate.

■    $$OVLINIT. The address of the new entry point of the program after
overlaying. The overlay manager should define its entry point as $$OVLINIT
and perform a branch to $$MAIN after initialization. LINK sets $$MAIN to
the original code's entry point.

EXE File Structure

As mentioned above, the EXE file from an overlay linker session is
structured a bit differently from a normal DOS executable file. Each
overlay segment (those modules that are contained within one set of
parentheses make up an overlay segment) has its own EXE header. The header
looks just like the normal EXE header, omitting some data (which is used
only by the DOS loader) that applies only to the program as a whole. Initial
stack settings, entry point, and so on, appear only in the initial header.
The overlay number, however, changes in each subsequent header, and the
length fields change as needed in order to find the next section. See the
file EXEHDR.H for a full description of the structure. Of course, as with
any LINK session, it's a good idea to create a MAP file with the /M option
and examine the result.

Overlay Manager

It's possible, knowing the hooks that LINK provides, to write a simple
overlay manager for languages that do not include support, particularly
MASM. OVRMGR.ASM (see Figure 1) and RDOVLY.C (see Figure 2) make up an
overlay manager that can be used with MASM programs and non-Microsoft
languages.

OVRMGR.ASM takes care of the overlay manager initialization and also
contains the shell of the interrupt handler code. RDOVLY.C does all the
dirty work. It actually reads the EXE file and reads in the overlay code at
the address determined by OVRMGR.ASM. C code is much easier to write,
especially at this level, and certainly easier to debug. Also, the C
run-time facilities for file handling take a lot of load off the programmer
for development effort.

For those of you who have a Microsoft C compiler, the C function was
translated into an assembly file, RDOVLY.ASM (see Figure 3), and the
pertinent C run-time functions were added as assembly definitions in
SUPPORT.ASM (see Figure 4). The code is commented and should be
self-explanatory, but the following offers a rundown.

OVRMGR assumes control at $$OVLINIT and proceeds to install ovly_int as the
new interrupt service routine for the INT $$INTNO handler, saving the old
vector for completeness. It's not restored by the code in its present form.
It would be simple enough to define an add-on to your language's exit
clean-up routines that restores the vector from old_ovint, where it's
stored. Once the initialization is complete, OVRMGR performs a far jump to
$$MAIN, the original program entry point.

When an overlay is called, OVRMGR again assumes control at ovly_int.
First, the handler checks to see if it's already in the middle of an
overlay call; if it is, it prints an error message and terminates the
program with error code 41H. (That is, this example manager does not allow
an overlay to call another overlay or itself.) If the overlay manager is not
currently active, the overlay number and offset are obtained from the
original code stream; the segment address for the overlay area is obtained
from the $$MPGSNBASE array, and read_overlay_section is called to read in
the overlay. On return, if there was an error reading the file, OVRMGR exits
with error code 42H; otherwise, the overlay may be called. I will go into
more detail on this below.

The read_overlay_section routine is reasonably straightforward. First, the
executable file is opened, using the executable_name variable that was set
by the original program. The variable executable_name is an ASCIIZ string (a
C-style string) containing the full pathname of the executable file; this
is easily obtained from a C program and is located just after the
environment passed to any process.

 Many languages have argument-string arrays like C's argv[ ] array, so I
won't dwell on how to create executable_name. It must, however, be defined
by the program that uses the overlay manager. The example assembly main
routine, _main, calls a procedure named fill_exe_name to initialize
executable_name (it's allocated in OVRMGR.ASM). Be careful if you try to run
this under SYMDEB. I found out, much to my chagrin, that SYMDEB eats the
path specifiers from the front of the executable name stored in the
environment. When running OVASM.EXE from the DOS command line, the whole
path is placed in the environment.

After opening the file that was named by executable_name,
read_overlay_section determines the file size for later use. The lseek
function is used to find the file size, in contrast to the expected method
of using the file length or tell functions. Once I decided to prototype in
C, I tried to limit the number of run-time calls made (since I needed to
program them in assembly language anyway).

Following this, the file pointer position is returned to the beginning of
the file, and read_overlay_section begins an endless loop to search for the
requested overlay. If there is an error at any point during the search,
read_overlay_section( ) prints an error message using the custom routine
errputs, closes the executable, and returns with a nonzero error code (in
this case, 1). Errors include read errors, unrecognizable information in
the executable file, and overlay not found. Presence of the file that is
named in executable_name is assumed. If the file is not present (for
example, if it's on a floppy that's been removed), however, the read of the
executable header will fail, returning an error.

When read_overlay_section finds the requested overlay, it reads that section
of the file into the area pointed to by the ovarea parameter. Then it must
relocate the code. Each segment reference in the code must be offset by
the program load address. This is the same relocation performed by
COMMAND.COM when it loads the program initially, but only for the root
section--the overlay manager is responsible for relocating the loaded
overlay. In order to offset each reference in the program by the load
address, $$OVLINIT sets the variable pspaddr for use during relocation. The
program load address is always the address of the PSP plus 10H, since the
PSP is 100H bytes long. The variable, pspaddr, and the relocation table
located in the file header are used by read_overlay_section in order to
locate the overlay.

The read_overlay_section handle  does the relocation in the while
(eh.num_relocs) loop; it continues until all relocation entries in the
table have been used. Each relocation fix-up is done by adding the segment
part of the relocation entry to the segment part of the load address
(pspaddr + 0x10). The resultant far pointer is the address of the target
word of the relocation. The pointer points into a code at a segment
reference that must be fixed up. Once the pointer is formed,
read_overlay_section adds the program load address (pspaddr + 0x10) to this
target word, changing 0-relative segment references to actual absolute
segment references. Notice the buffering of relocation entries; it is one of
the reasons I decided to write the routine in C.

Once the relocation is finished, the executable file is closed and control
returns to OVRMGR.ASM with a return code of 0. (If there was an error in the
load, as explained above, the return code would have been 1 and ovly_int
would print an error message and die.) If there was no error, the address of
return_from_ov is pushed on the stack so that the RETF from the overlaid
routine will return to return_from_ov (thus explaining the choice of
label).

Next, the address of the overlaid routine itself is pushed onto the stack
and a RETF is executed, causing a jump to the overlaid, relocated routine.
I did this mainly because I'm already pushing things; a jump through a far
pointer would have been just as easy. When the overlaid routine is done,
it executes a RETF (remember that we are working strictly in far code).
Control transfers to return_from_ov, which cleans up, resets the flag that
indicates that we're no longer in the overlay manager, and returns control
to the mainline program, just after the call to the overlaid routine.

SUPPORT.ASM contains the assembly functions that supplant the C run-time
functions used in RDOVLY.ASM: errputs, myopen, myclose, mylseek, and myread.
OVRMGR.ASM contains the overlay initialization and interrupt handler and
RDOVLY.C translates to RDOVLY.ASM, completing the simple overlay
management package.

Test Code

Several test routines are provided with the overlay manager, both in
assembly language and in C. The files OV.C, P1.C, and P2.C (see Figure 5)
comprise a simple test, and OVASM.ASM, P1ASM.ASM, and P2ASM.ASM (see
Figure 6) are similar test routines in assembly language. If you have
Microsoft C Version 5.1 and MASM Version 5.1, you can remake the entire
package in C or assembly language using MAKE and the sample make files, OV
and OVASM (see Figure 7). Of course MASM and LINK must be available in
your DOS executable path, you must have a version of LINK that supports
overlaying, and so on.

The RDOVLY.C source file, which eventually becomes the RDOVLY.ASM assembly
language source, is processed by a small utility called SMERGE (see
SMERGE.C listed in Figure 8), which merges the C source into the /Fa
assembly language output from Microsoft C. SMERGE also removes an annoying
external definition, an EXTRN _acrtused:ABS, which is issued by the
compiler. Reasonably portable source for SMERGE.C is provided for those
with C compilers.

Limitations

The code given here isn't sufficient for a really full-featured overlay
manager. For one thing, the error handling could be improved. For instance,
there is currently no provision for handling the case in which a floppy
disk containing the executable file is removed during program execution.
Granted, most PC users have hard disks, and the programs that will benefit
from overlay management often live on those hard disks, but a specific
error handler could be added for a really bulletproof package. The Microsoft
overlay manager, for instance, requests either the floppy disk or a new
pathname for the executable file.

A more serious limitation is that only one overlay can be active at a time.
This is due to the way information is stored in the overlay manager and the
way returns are processed. It would be a fairly simple matter to modify
OVRMGR.ASM to support an arbitrary number of overlays. The Microsoft C
overlay manager supports up to 128 nested overlay calls. I'll leave this
as an exercise for the investigative and adventurous reader.

Microsoft notes, in the MASM documentation, that CodeView is now compatible
with the overlay manager; the manual even mentions that overlaying may
be necessary to get a large application to run under CodeView. In fact, the
Microsoft overlay manager contains special hooks into CodeView and DEBUG
so that special calls are made to both in order to map in new symbols.

This overlay manager clearly can't support undocumented features, but
I've found that debugging is still possible--it's just not as
convenient--by using a symbolic debugger's capabilities for loading
symbol files after the program has been loaded. Just rip out the sections
of the MAP file that relate to the overlaid routines. Since data segments
aren't overlaid, the problem usually isn't too limiting; data labels are the
things I most often want to see in the disassembly. But it's worth
noting that debugging an overlaid program can be interesting.

Using overlay management offers one solution to the problem of limited
code memory. This article has offered some insight into programming
techniques for limited code memory by discussing Microsoft's solution
for far code program development in addition to providing MASM
programmers with a way to use it. While this particular overlay manager
may not be useful to you, a modification along the lines suggested above
might be useful. Overlay management is definitely an area worth
exploring.


Programming Assembly Language Routines in C

Programming in assembly language has undeniable advantages; the low-level
control, raw speed, and small code size just can't be equaled by a
high-level language. But assembly programming can be terrible drudgery,
prototyping in assembly can be painful (even for experienced assembly
language programmers), and routines that involve array access, looping with
complex termination conditions, or file I/O can involve lots of error-prone
code. For these reasons, I decided to write the initialization and shell of
the overlay interrupt handler in MASM and use Microsoft C for the main body
of the routine. The result is the function read_overlay_section contained in
the file RDOVLY.C.

Since I also wanted to present an overlay manager for assembly language
programmers, I proceeded to rewrite C portions of the routines in
assembly--until it occurred to me that the Microsoft C /Fa option (to
generate a MASM-compatible assembly language output file) could write the
code for me, with some help.

The first thing I had to do was rewrite the run-time routines I needed from
the C run-time library in assembly language. This was easy with a bit of
advance planning. For instance, since I was aware of how easily I could
write an lseek function using DOS function call 42H, I used that function
instead of the C function tell to get the current file position. I also knew
I would need lseek in other places, and the proper form of lseek returns the
same value as tell. To make the rewriting job easier, I limited file I/O to
level 1 I/O, which is most like DOS file I/O. The small routines in
SUPPORT.ASM are those that take the place of the C run-time routines as well
as a special print to stderr function (errputs).

Switches for the Microsoft C compiler driver, CL, are contained in both
example make files. /c instructs CL to compile the file but doesn't go on to
the link step, and /Fa and /Fonul cause the assembly language output to
appear while sending any OBJ output to the NUL device, effectively
discarding it. /AL causes the compiler to use large-model coding; that is,
far pointers for data and code. LINK requires this when overlaying code. /Gs
suppresses generation of calls to _chkstk, the stack-overflow-check routine,
since this is a hidden run-time routine that isn't duplicated in my assembly
code. /Zl suppresses the default library search, usually written to the OBJ
file. This is probably not needed here, since I'm generating ASM files
directly, but I'm careful. Finally, since I planned on including the
assembly output, I used /Od to suppress all optimizations, so the output
code would be clearer.

Since the ASM output contains references to the original C source line, I
decided to merge the C source and the compiler-generated assembly source to
make it easier to see what's happening in the assembly output. SMERGE.C is a
program written to automate the merging process, replacing comments of the
form

; Line <n>

with the actual nth line of the C source. When I discovered that CL inserts
an extra data declaration for a variable _acrtused (whose purpose I never
discovered), I decided to remove that declaration with SMERGE as well. Thus,
CL and SMERGE together make a clearer, commented assembler file.

I didn't hand optimize anything in the result. It's possible that
performance could be improved by doing so, and certainly more mnemonic
labels could be added by hand. A first glance at the code generated by CL
indicated that it wasn't unclear, so I decided to leave well enough alone.

The experiment worked well with RDOVLY.C. I was able to write a functional
overlay manager in an afternoon, tweak its performance easily by using C,
and then write an assembly-language-only version in just a few more hours.
Had I started writing in assembly language, changing the overlay section
reader's error handling and buffering of relocation records would have been
much more difficult. But C is especially suited for such design modification
and is close enough kin to assembly to generate reasonably efficient output.
Give prototyping in C a serious try; you'll like it.

Figure 1

.model LARGE
            extrn  $$INTNO:byte
            extrn  $$MPGSNBASE:word
            extrn  $$MPGSNOVL:byte
            extrn  $$MPOVLLFA:word
            extrn  $$OVLBASE:byte

            extrn  $$MAIN:far
            extrn  _read_overlay_section:far
            extrn  _errputs:far

            .data
_pspaddr    dw     ?     ;for relocation with read_overlay_section
old_ovint   dw     ?     ;old overlay interrupt offset
            dw     ?     ;and segment

_executable_name   db    80 dup (0)     ;space for full pathname

cantmap     db     'Can''t map overlay!...exiting',13,10,0
reentry     db     'Already mapping an overlay!...exiting',13,10,0


            .code

            public  $$OVLINIT
            public  _executable_name
            public  _pspaddr
            public  ax_save,bx_save,cx_save,es_save,ret_ip,ret_cs
            public  req_ov,ov_ofs,ov_seg,in_ovly_mgr,ovly_int
            public  return_from_ov

ax_save     dw     ?     ;save area for used registers
bx_save     dw     ?
cx_save     dw     ?
es_save     dw     ?

ret_ip      dw     ?     ;original return address
ret_cs      dw     ?     ;(from call to overlaid routine)

req_ov      db     ?     ;requested overlay number
ov_ofs      dw     ?     ;address to call after mapping
ov_seg      dw     ?

in_ovly_mgr        db    0     ;flag to indicate already mapping

$$OVLINIT   label  far
            push   ax
            push   bx
            push   dx
            push   es
            push   ds          ;need to use ds:dx
            mov    ax,DGROUP
            mov    ds,ax

            mov    ax,es       ;initialize _pspaddr
            mov    _pspaddr,ax

            mov    al,[$$INTNO]      ;get interrupt number
            mov    ah,35h      ;get overlay number interrupt vector
            int    21h

            mov    old_ovint,bx
            mov    old_ovint+2,es

            mov    bx,cs
            mov    ds,bx
            mov    dx,offset ovly_int      ;new interrupt
            mov    ah,25h
            int    21h         ;install new int handler
            pop    ds
            pop    es
            pop    dx
            pop    bx
            pop    ax

            jmp    $$MAIN      ;go to mainline code


;**
;** INTERRUPT HANDLER
;**

ovly_int    proc   far
            cmp    cs:in_ovly_mgr,0  ;are we not here already?
            je     ok                ;right
; Error if we're already in overlay manager!
            mov    ax,offset reentry       ;error message
            push   ds
            push   ax
            call   _errputs
            add    sp,4
            mov    ax,4c41h    ;exit with error code 41h
            int    21h

ok:         mov    cs:in_ovly_mgr,1      ;note that we're here
            mov    cs:ax_save,ax
            mov    cs:bx_save,bx
            mov    cs:cx_save,cx
            mov    ax,es
            mov    cs:es_save,ax

            pop    bx          ;bx = return ip
            push   bx
            add    bx,3              ;adjust for extra stuff
            mov    cs:[ret_ip],bx    ;save it
            pop    bx                ;get original return address back

            pop    ax                ;return cs
            mov    cs:[ret_cs],ax    ;save it
            mov    es,ax             ;es:bx -> bytes after INT

            pop    ax                ;flags, discard

            xor    ah,ah
            mov    al,byte ptr es:[bx] ;get requested overlay number
            inc    bx                      ;move to next item
            mov    byte ptr cs:req_ov,al   ;save it
            mov    cx,word ptr es:[bx] ;get offset in overlay segment
            mov    cs:[ov_ofs],cx          ;save it

            push   ax                      ;p3 for C function
            mov    bx,ax
            shl    bx,1                    ;* 2
            add    bx,offset $$MPGSNBASE   ;add to base of
                                           ;segment table
            mov    bx,[bx]
            mov    ov_seg,bx
            push   bx                      ;push the segment for p2
            mov    ax,0                    ;p2, low word (0)
            push   ax
            call   _read_overlay_section   ;call the pup!
            add    sp,6                    ;get rid of parms
            cmp    ax,0                    ;error return?
            je     no_error                ;no

; Error if can't map this overlay!
            mov    ax,offset cantmap     ;error message
            push   ds
            push   ax
            call   _errputs
            add    sp,4
            mov    ax,4c42h              ;exit with error code 42h
            int    21h

no_error:   push   cs
            mov    ax,offset return_from_ov    ;trick up return
                                               ;to our code
            push   ax
            mov    ax,seg $$OVLBASE  ;set up call to overlay code
            push   ax
            mov    ax,ov_ofs
            push   ax
            retf                ;jump to overlay code...

return_from_ov:                 ;..and come back here
            mov    ax,ret_cs    ;restore original return address
            push   ax
            mov    ax,ret_ip
            push   ax
            mov    ax,cs:es_save    ;and original reg values
            mov    es,ax
            mov    ax,cs:ax_save
            mov    bx,cs:bx_save
            mov    cx,cs:cx_save
            mov    cs:in_ovly_mgr,0      ;we're not here anymore

            ret
ovly_int    endp

            end

Figure 2

#include "exehdr.h"

/* externally-defined full pathname of executable */

#define min(a,b) ((a) < (b)) ? (a) : (b)
#define max(a,b) ((a) < (b)) ? (a) : (b)
#define OPENMODE_RO 0
#define MK_FP(a,b) ((void far *)( ((unsigned long)(a) << 16) |
                                   (unsigned)(b) ))

extern char executable_name[];
extern int pspaddr;

void errputs(char far *s);
long mylseek(int handle, long offset, int type);
int myread(int handle, void far *buf, int len);
int myopen(char far *name, int mode);
int myclose(int handle);


int read_overlay_section(
void far *ovarea,              /* buffer into which to read him */
int requested_overlay_number   /* number of overlay to read */
)

#define RELOC_BUF_SIZE 32
{
    struct reloc_entry_ reloc_buf[RELOC_BUF_SIZE];
    struct reloc_entry_ *reloc_ptr;
    int reloc_chunk;
    unsigned far *target_ptr;
    struct exehdr_ eh;
    int i;
    long startpos, nextpos, filesize;
    long sectionsize, imagesize;
    int executable_handle;

    /* open executable */
    executable_handle = myopen(executable_name,OPENMODE_RO);

    /* determine file size */
    filesize = mylseek(executable_handle,0L,2);

    /* search from beginning of file */
    mylseek(executable_handle,0L,0);

    while (1) {
        /* use mylseek() to avoid calling runtime functions */
        startpos = mylseek(executable_handle,0L,1);
        if (myread(executable_handle,&eh,sizeof(eh)) != sizeof(eh)) {
            errputs("Something's wrong...can't read EXE header\r\n");
            myclose(executable_handle);
            return(1);
    }

    if (eh.M_sig != 'M' || eh.Z_sig != 'Z') {
    errputs("Found non-EXE signature!\r\n");
            myclose(executable_handle);
    return(1);
    }
        if (eh.remain_len == 0)
            sectionsize = (long)eh.page_len * 512;
        else
            sectionsize = (long)(eh.page_len - 1) * 512 +
                                 eh.remain_len;

        if (eh.overlay_number == requested_overlay_number) {
            /* found ours...load and fix up */

            /* move to executable image */
            mylseek(executable_handle, startpos + eh.hsize * 16, 0);

            myread(executable_handle, ovarea, (int)(sectionsize -
                                                    eh.hsize * 16));

            /* fix up relocations in the loaded overlay */

            mylseek(executable_handle,startpos +
                    (long)eh.first_reloc,0);
            while (eh.num_relocs) {
                reloc_chunk = min(RELOC_BUF_SIZE,eh.num_relocs);
                myread(executable_handle,reloc_buf,reloc_chunk *
                       sizeof(struct reloc_entry_));
                eh.num_relocs -= reloc_chunk;
                for (i = 0; i < reloc_chunk; i++) {
                    reloc_ptr = reloc_buf + i;
                    target_ptr = MK_FP(pspaddr + 0x10 +
                                       reloc_ptr->segment,
                                       reloc_ptr->offset);
                    *target_ptr += pspaddr + 0x10;
                }
            }
            myclose(executable_handle);
            return 0;
        } else {
    nextpos = startpos + sectionsize;
            /* round up to 512-byte bound */
            nextpos = (nextpos + 511L) & ~511L;
    if (nextpos >= filesize) {
                myclose(executable_handle);
    return 1;
            }
    mylseek(executable_handle,nextpos,0);
        }
    }
    myclose(executable_handle);
    return 1;
}

Figure 4

.model  LARGE,C
            .code

            public errputs
errputs     proc   far C uses ax bx cx di ds es, string:ptr
            les    di,string       ;es:di -> string
            push   di              ;save original offset
            mov    al,0            ;terminator byte
            mov    cx,0100h        ;length to examine for null
            repne  scasb           ;look for it
            pop    ax              ;ax = orig pointer
            sub    di,ax
            dec    di              ;di = length
            mov    cx,di
            mov    bx,2            ;stderr
            mov    dx,ax           ;buffer address
            mov    h,40h           ;write to file
            int    21h
            ret
errputs     endp


mylseek     proc   far C uses bx cx,handle:WORD,ofs:DWORD,whence:WORD
            public mylseek
            mov    ax,whence
            mov    ah,42h
            mov    bx,handle
            push   es
            les    dx,ofs
            mov    cx,es
            pop    es
            int    21h             ;result left in dx:ax, nicely
            jnc    lseekdone
            mov    ax,-1
lseekdone:  ret
mylseek     endp


myread      proc   far C uses bx ds dx, handle:WORD, buf:PTR,len:WORD
            public myread
            mov    ah,3fH
            mov    bx,handle
            mov    cx,len
            lds    dx,buf
            int    21h             ;result nicely in ax for return
            jnc    readdone
            mov    ax,-1
readdone:   ret
myread      endp

myopen      proc   far C uses ds dx, fname:PTR, mode:WORD
            public myopen
            mov    ax,mode
            mov    ah,3dH
            lds    dx,fname
            int    21h             ;result nicely in ax for return
            jnc    opendone
            mov    ax,-1
opendone:   ret
myopen      endp

myclose     proc   far C uses bx, handle:WORD
            public myclose
            mov    ah,3eH
            mov    bx,handle
            int    21h             ;result nicely in ax for return
            ret
myclose     endp

            end

Figure 5

OV.C

#include <stdio.h>
#include <string.h>

void p1(void);
void p2(void);

extern char executable_name;

void main(int argc, char **argv)
{
    int i;

    strcpy(&executable_name,argv[0]);
    argc = argc;

    printf("This is main\n");
    for (i = 0; i < 1000; i++) {
        p1();
        p2();
    }
}

P1.C

#include <stdio.h>
void p1()
{
   printf("In p1, whose address is %Fp\n",p1);
   printf("\t 3 * 3 + 9 = %d\n",3*3+9);
   printf("\t 4 * 4 / 5 = %d\n",4*4/5);
}

P2.C

#include <stdio.h>
void p2()
{
   printf("In p2, whose address is %Fp\n",p2);
   printf("\t 10 %% 3 = %d\n",10 % 3);
}

Figure 6

OVASM.ASM

            extrn  _errputs:far, p1:far, p2:far
            extrn  _executable_name:byte
_text       segment public 'CODE'
_text       ends
_data       segment public 'DATA'
_data       ends

            public fill_exe_name,ENVSEG,main_msg,
            public call_loop,envstr_loop
            public fname_loop

ENVSEG      equ    2CH             ;loc'n of environment pointer
DGROUP      group  _data,_stack
_text       segment
            assume cs:_text,ds:DGROUP,ss:DGROUP
_main       proc   far
            mov    ax,DGROUP
            mov    ds,ax
            call   far ptr fill_exe_name
            lea    ax,main_msg
            push   ds
            push   ax
            call   far ptr _errputs
            add    sp,4
            mov    cx,1000
call_loop:
            call   far ptr p1
            call   far ptr p2
            loop   call_loop
            mov    ax,4c00h
            int    21h
_main       endp

fill_exe_name   proc   far
            push   es
            push   di
            push   ds
            push   si
            push   ax

            mov    ax,es
            mov    ds,ax
            mov    ax,DGROUP:ENVSEG  ;point at env
            mov    ds,ax
            mov    si,0
            mov    ax,seg _executable_name   ;point at dest string
            mov    es,ax
            lea    di,DGROUP:_executable_name
envstr_loop:
            lodsb                      ;get first env
            cmp    al,0                ;end of string?
            jnz    envstr_loop         ;no, keep looking
            cmp    byte ptr ds:[si],0  ;end of env?
            jnz    envstr_loop         ;no, get another string
            add    si,3                ;yes: go past, 2 more
fname_loop: movsb                      ;move a byte
            cmp    byte ptr ds:[si],0  ;end of name?
            jnz    fname_loop          ;no, keep it up
            mov    byte ptr es:[di],0  ;store zero terminator

            pop    ax
            pop    si
            pop    ds
            pop    di
            pop    es
            ret
fill_exe_name   endp

_text       ends


_data       segment
main_msg    db      'This is main...',13,10,0
_data       ends


_stack      segment stack 'STACK'
            dw     200 dup (?)
_stack      ends

            end    _main

P1ASM.ASM

            extrn  _errputs:far
            .model LARGE
            .data
p1_msg      db     'This is p1...',13,10,0

            .code
p1          proc   far
            public p1
            lea    ax,p1_msg
            push   ds
            push   ax
            call   _errputs
            add    sp,4
            ret
p1          endp

            end

P2ASM.ASM

            extrn   _errputs:far
            .model  LARGE
            .data
p2_msg      db      'This is p2...',13,10,0

            .code
p2          proc   far
            public p2
            lea    ax,p2_msg
            push   ds
            push   ax
            call   _errputs
            add    sp,4
            ret
p2          endp

            end

Figure 7

OV

ov.obj: ov.c
  cl /c /AL /Fm ov.c

p1.obj: p1.c
  cl /c /AL /Fm p1.c

p2.obj: p2.c
  cl /c /AL /Fm p2.c

ovrmgr.obj: ovrmgr.asm
  masm /MX ovrmgr.asm;

rdovly.asm : rdovly.c exehdr.h
# /c compile only
# /Fa create .ASM output
# /Fonul don't create object (leave that for MASM)
# /Al large model
# /Gs no stack checking
# /Zl no default library search
# /Od no optimization (for code clarity)
  cl /c /Fa /Fonul /Od /AL /Gs /Zl rdovly.c
  smerge rdovly.asm rdovly.c rdovly.mrg
  del rdovly.asm
  ren rdovly.mrg rdovly.asm

rdovly.obj : rdovly.asm
  masm /mx rdovly;

support.obj : support.asm
  masm /mx support;

# Note - the ov.exe : ... line cannot break but had to be broken
#        here to fit on page

ov.exe : ov.obj support.obj rdovly.obj ovrmgr.obj
         p1.obj p2.obj ovrmgr.obj
  link /m/li ov+support+rdovly+(p1)+(p2)+ovrmgr,ov,ov.map;

OVASM

ovasm.obj: ovasm.asm
  masm /mx ovasm.asm;

p1asm.obj: p1asm.asm
  masm /mx p1asm;

p2asm.obj: p2asm.asm
  masm /mx p2asm;

ovrmgr.obj: ovrmgr.asm
  masm /MX ovrmgr.asm;

rdovly.asm : rdovly.c exehdr.h
# /c compile only
# /Fa create .ASM output
# /Fonul don't create object (leave that for MASM)
# /Al large model
# /Gs no stack checking
# /Zl no default library search
# /Od no optimization (for code clarity)
  cl /c /Fa /Fonul /Od /AL /Gs /Zl rdovly.c
  smerge rdovly.asm rdovly.c rdovly.mrg
  del rdovly.asm
  ren rdovly.mrg rdovly.asm

rdovly.obj : rdovly.asm
  masm /mx rdovly;

support.obj : support.asm
  masm /mx support;


# Note - the ov.exe : ... and link lines cannot break but had
#        to be broken here to fit on page

ovasm.exe : ovasm.obj support.obj rdovly.obj ovrmgr.obj
            p1asm.obj p2asm.obj ovrmgr.obj
  link /m/li ovasm+support+rdovly+(p1asm)+(p2asm)+
             ovrmgr,ovasm,ovasm.map;

Figure 8

/* smerge - merge source and assembly files */

#include <stdio.h>

main(argc,argv)
int argc;
char *argv[];
{
    FILE *csrc, *asmsrc, *mergefile;
    char buffer[128];
    long sline, line, totslines;

    if (argc < 4) {
        printf("usage: smerge asmsrc csrc mergefile\n");
        exit(1);
    }

    sline = 0;

    asmsrc = fopen(argv[1],"r");
    csrc = fopen(argv[2],"r");
    mergefile = fopen(argv[3],"w");

    setvbuf(csrc,NULL,_IOFBF,8192);
    setvbuf(asmsrc,NULL,_IOFBF,8192);
    setvbuf(mergefile,NULL,_IOFBF,8192);

    totslines = 0;
    while (fgets(buffer,128,csrc) != NULL)
        totslines++;
    fseek(csrc,0L,SEEK_SET);

    while ((fgets(buffer,128,asmsrc)) != NULL) {

        /* special hack for MSC -> assembly ...
           kill the "__acrtused" extrn */

        if (strstr(buffer,"__acrtused") != NULL)
            continue;

        if (strncmp(buffer,"; Line",6) == 0) {
            sscanf(buffer,"; Line %ld",&line);
            if (line <= totslines) {
                while (sline < line) {
                    fgets(buffer+1,128,csrc);
                    fputs(buffer,mergefile);
                    ++sline;
                }
            }
        } else
            fputs(buffer,mergefile);
    }
    fclose(asmsrc);
    fclose(csrc);
    fclose(mergefile);
}



Extended Memory Specification 2.x:

Taking Advantage of the 80286 Protected Mode

 Chip Anderson

The IBM PC and compatibles support three types of memory--conventional,
expanded, and extended. Expanded memory has been widely supported by
programmers for a number of years; extended memory, on the other hand, has
been largely ignored. Now a new industry standard, known as the Extended
Memory Specification (referred to herein as XMS), has made that memory more
readily accessible to all programmers working in the DOS1 environment.
This article takes a look at the evolution of extended memory and examines
how XMS provides more memory for DOS applications.

Extended Memory Issues

DOS programmers have been slow to take advantage of extended memory for two
primary reasons. The first is that until recently the number of machines
that would not support extended memory (that is, 8088- and 8086-based
machines) was far greater than the number that would. Most programmers,
therefore, had to ignore the additional protected-mode capabilities of
the 80286, since most copies of programs were purchased to run on the 8088
or 8086 machines. But earlier in 1989, the sales of 286- and 386-based
machines, many of which contained one or more megabytes of extended memory,
began to surpass those of 8086- and 8088-based machines. Due to the
popularity of the 286/386 machines, developers will certainly write programs
to take advantage of extended memory.

The second reason programmers were reluctant to provide access to extended
memory was due to the lack of an accepted standard for accessing it. A
standard for using extended memory must define methods that will allow
programs to reserve part of the available extended memory for their
exclusive use (allocation), copy data to and from extended memory
(transfer), and release the reserved memory when finished (deallocation).

Two conflicting pseudostandards for accessing extended memory have evolved
over time. The IBM VDISK.SYS, a RAM disk program released with DOS Version
3.0, allocates extended memory using the VDISK method. Programs such as
Microsoft(R) RAMDRIVE.SYS use an alternate scheme known as the INT 15h
method. Examining the details of how these two methods implement the
allocation, transfer, and deallocation of extended memory demonstrates
why using extended memory has remained difficult.

Allocation

VDISK.SYS was the first program to use extended memory and assumed that no
other programs would use it. VDISK stores a data structure called a VDISK
Header just above 1Mb. The VDISK Header indicates the amount of memory used
by the first memory disk. If two VDISKs are installed, the second
instance will install its header just above the end of the first disk. This
process is called a bottom-up allocation scheme (see Figure 1).

The INT 15h method approaches allocation from the opposite direction. When
a DOS program uses INT 15h, Function 88h, the ROM BIOS returns the amount of
extended memory in the computer. Using the INT 15h method, a DOS program
reserves a portion of that memory by intercepting or hooking all
subsequent INT 15h calls. After hooking INT 15h, the program gets control
whenever another program executes INT 15h. This means that the first program
can "lie" to all subsequent programs about the amount of extended memory
that is installed. A consequence of this is that by returning a smaller
amount, the first program guarantees that it has exclusive access to the
remaining memory.

A PC/AT(R) with 2Mb of RAM, for example, returns 1408Kb of memory when INT
15h, Function 88h is executed (since 2048Kb - 640Kb = 1408Kb). When a 1Mb
RAMDrive is installed, it hooks INT 15h so that all subsequent calls to
INT 15h, Function 88h, return 384 (since 1408Kb - 1024Kb = 384Kb). If any
other program uses INT 15h, Function 88h, it is fooled into thinking that
the machine only contains 1Mb of RAM. RAMDrive is then free to store its
data in the memory between 1408Kb and 2048Kb. If two RAMDrives are
installed, the second instance also hooks INT 15h and has it tell other
programs that even less memory is installed. Notice that the INT 15h
method allocates memory in a top-down manner (see Figure 2).

Since these methods allocate extended memory in different directions,
they are incompatible. Another problem is that both methods assume that
all available extended memory is contiguous, but several IBM compatibles
have noncontiguous blocks of extended memory.

Transfer

Neither the VDISK method nor the INT 15h method specify a way in which to
move data between conventional and extended memory. Therefore,
programmers must write their own subroutine for doing this. But the only
way of moving data into and out of extended memory is to quickly switch the
microprocessor into 286 protected mode, perform the copy, and switch
back into real mode. Realizing the difficulty in writing such a routine, IBM
engineers provided a ROM BIOS function (INT 15h, AH=87h) to perform
extended memory transfers in the original PC/AT computer.

Unfortunately, the IBM ROM BIOS transfer subroutine has a major design
problem--it disables hardware interrupts while data is being
transferred. This means that during long transfers, time-dependent
programs such as communications packages and networks will lose data,
especially when they are running under multitasking environments such
as Microsoft Windows/286 and Desqview. Programs that use the IBM
transfer routine must break large transfers into many smaller chunks, thus
significantly slowing an already slow process.

Deallocation

Unfortunately, there is no way to free allocated extended memory under
either the VDISK or the INT 15h method of managing extended memory. To see
why, consider the following scenario (see Figure 3). Running under a
real-mode multitasking environment, for example Windows/286, Program A
allocates 100Kb of extended memory, Program B allocates 50Kb of extended
memory, and Program C allocates 75Kb of extended memory. How would each
allocation scheme work if Program B finishes executing and wishes to
free its 50Kb?

Because the VDISK method is a bottom-up allocator, it assumes that no holes
occur below the last block allocated. In this case, any future program that
wishes to allocate extended memory has to find and use the header that
Program C installed to determine how much extended memory is free and
where it is located. The header does not indicate if any holes exist.

The INT 15h method requires that the allocating program install an
interrupt hook that fools other programs about the amount of available
extended memory. Once they are installed, however, interrupt hooks cannot
be removed without risk of a system crash. Not only does this fact prevent
deallocation of extended memory, but, since interrupt hooks are part
of a program, it means that the allocating program cannot be removed either.
Therefore, only terminate-and-stay-resident (TSR) programs and device
drivers can safely use the INT 15h method.

High Memory

Complicating the extended memory picture even further, developers at
Microsoft discovered a simple hardware trick that allows DOS programs to
access an additional 64Kb directly. In real mode, 286-based computers only
enable 20 address lines and thereby emulate the 1Mb address space of
8086-based computers. By selectively enabling and disabling the computer's
21st address line, a DOS program running on a 286-based machine can access
the first 64Kb of extended memory. This 64Kb area is called the High Memory
Area (HMA). See "The High Memory Area: Addressing 64Kb More Memory in
Real Mode," MSJ (Vol. 3, No. 6) for more information. Notice that the HMA
must be located just above the 1Mb boundary and thus is incompatible with
the VDISK allocation scheme.

Introducing XMS

In July 1988, after another period of lengthy discussions, Lotus(R),
Intel(R), AST(R), and Microsoft (with input from many other vendors)
finalized the Extended Memory Specification Version 2.0 (XMS 2.0). XMS 2.0
solves all of the problems associated with both the VDISK and INT 15h
methods of accessing extended memory. It provides a set of functions that
DOS programs can use to do the following:

■    reserve portions of extended memory for themselves

■    transfer any amount of data from conventional memory into extended
memory or vice versa

■    release reserved extended memory when finished

In addition, XMS 2.0 provides functions that:

■    reserve and protect the HMA

■    control the computer's 21st address line in a machine-independent
manner

■    reserve and release any additional memory located between 640Kb and
1Mb (called Upper Memory Blocks)

The XMS functions are typically added to the DOS system by installing an
XMS driver via a "DEVICE=" command in the machine's CONFIG.SYS file. Anyone
can get the Microsoft XMS driver, HIMEM.SYS, along with source code and the
official XMS 2.0 specification by contacting the Microsoft Information
Center at (800) 426-9400.

Using XMS Functions

Programs that wish to use XMS to access extended memory can do so by
performing the following steps:

1.    Determine if an XMS driver is installed.

2.    Get the address of the driver's control function.

3.    Set AH to the requested function's number.

4.    Initialize other registers where appropriate.

5.    Call the driver's control function.

The first two steps are done via Interrupt 2Fh, the multiplex interrupt.
Executing an INT 2Fh instruction with AX=4300h returns a value of 80h in
AL if an XMS driver is installed. Programs can then get the address of the
driver's control function by executing an INT 2Fh with AX=4310h. The driver
will return the address in ES:BX. The program then has access to any of
the 18 XMS functions (which are listed in the Extended Memory
Specification sidebar). Some of these functions are defined in SCRXMS.ASM
(Figure 4), a subset of the C library XMM.LIB that provides access to all
XMS functions. XMM.LIB is included on the Microsoft XMS distribution
diskette.

Programs that need to store large amounts of data in extended memory only
need to use the following XMS functions:

08h    Query Free Extended Memory

09h    Allocate Extended Memory Block

0Ah    Free Extended Memory Block

0Bh    Move Extended Memory Block

These functions are demonstrated by the SCRSAVE program (see Figure 5),
which shows how a DOS program can use XMS calls to store data in extended
memory.

Query Free Extended Memory (08h) returns two numbers--the total
amount of free extended memory and the size of the largest contiguous free
Extended Memory Block (EMB). Because most programs only work with contiguous
data buffers, the second number is usually more important than the first
number.

Allocate Extended Memory Block (09h) attempts to allocate a contiguous EMB
that is at least as big as the caller requested, in this case 8Kb of
extended memory for the screen:

lret = XMM_AllocateExtended(8);

(See also _XMM_AllocateExtended in Figure 4.)

If the allocation succeeds, a 16-bit EMB Handle is returned to the caller:

xHandle = (WORD)lret;

If there is less than 8Kb available, then an appropriate error message
is generated and the memory is not allocated:

if (!xHandle)
{
  printf("Allocation Error: %X/n",
         XMSERROR(lret));
  exit(-1);
}

The caller can use this handle in subsequent XMS calls as a nickname for the
EMB. 0000h is reserved as a special type of EMB handle that refers to
memory below the 1Mb boundary.

Move Extended Memory Block (0Bh) can be used to move data from any part of
memory to any other part. Typically, it is used to move data from
conventional memory into extended memory or vice versa. The caller passes
the function a pointer to a data structure, which contains pointers for the
source and destination buffers along with the number of data words to
transfer. In this case, SCRSAVE stores the contents of the test screen in an
EMB. It then fills the screen with a test pattern:

lret = XMM_MoveExtended
       (&MoveStruct);

The function performs the transfer without disabling interrupts, thus side
stepping the problems with INT 15h, Function 87h. In addition, HIMEM.SYS
will use a faster version of the Move function when it is installed on an
80386-based machine, thus ensuring the fastest transfer rate regardless of
the hardware platform.

Free Extended Memory Block (0Ah) deallocates the previously allocated EMB.
SCRSAVE retrieves the old contents of the screen from extended memory,
restores the screen, and terminates:

XMM_FreeExtended(xHandle);

Since XMS does not use hooks or headers, deallocation is always possible. As
with any reasonable memory management scheme, after each deallocation
any adjacent blocks that are free are merged into one larger free block.

The standardization of XMS and the development of the XMM function library
have made extended memory easily accessible to DOS programmers. This
means that you will begin to see more applications designed specifically to
take advantage of the 286/386 extended memory capabilities. This article
describes the extended memory functions. These functions will help give
your programs instant access to more memory.



Figure 4

; SCRXMS.ASM - Adapted from the XMM C Library Routines (c) 1988 ;
;Microsoft Corp

; DATA SEGMENT

_DATA    segment WORD PUBLIC 'DATA'

XMM_Initialized dw    0        ; 1 if the XMS driver has
                                                    ; been found
XMM_Control    label    dword        ; Points to the XMS control function
        dw    offset _XMM_NotInitialized
        dw    seg _TEXT
_DATA    ends


DGROUP    GROUP _DATA

; CODE SEGMENT

_TEXT    segment WORD PUBLIC 'CODE'

assume cs:_TEXT
assume ds:DGROUP

;-------------------------------------------------------------------
;  _XMM_NotInitialized
;    Called by default if the program hasn't called _XMM_Installed
;    or if no XMS driver is found.
;-------------------------------------------------------------------

_XMM_NotInitialized proc far

    xor    ax,ax            ; Simulate an XMS "Not
                    ; Implemented" error
    mov    bl,80h
    ret

_XMM_NotInitialized endp

;-------------------------------------------------------------------
;  BOOL _XMM_Installed(void);
;    Called to initialize the XMM library routines.
;-------------------------------------------------------------------

public _XMM_Installed

_XMM_Installed proc near

    ; Have we already been initialized?
    cmp    [XMM_Initialized],0
    jne    Already_Initialized        ; Yes, return TRUE

    ; Is an XMS driver installed?
    mov    ax,4300h
    int    2Fh
    cmp    al,80h
    jne    NoDriver            ; No, return FALSE

    ; Get and store the address of the driver's control function.
    mov    ax,4310h
    int    2Fh
    mov    word ptr [XMM_Control],bx
    mov    word ptr [XMM_Control+2],es

    ; Set XMM_Initialized to TRUE.
    inc    [XMM_Initialized]

Already_Initialized:
NoDriver:
    mov    ax,[XMM_Initialized]
    ret

_XMM_Installed endp

;-------------------------------------------------------------------
;  WORD _XMM_Version(void);
;    Returns the driver's XMS version number.
;-------------------------------------------------------------------
public _XMM_Version

_XMM_Version proc near

    xor    ah,ah            ; Call XMS Function 0
    call    [XMM_Control]        ; Sets AX to XMS Version
    ret

_XMM_Version endp

;-------------------------------------------------------------------
;  long _XMM_AllocateExtended(int SizeK);
;    Allocates an Extended Memory Block and returns its handle.
;-------------------------------------------------------------------
public _XMM_AllocateExtended

_XMM_AllocateExtended proc near

    push    bp            ; Create a stack frame
    mov    bp,sp

    ; Call the XMS Allocate function.
    mov    ah,9
    mov    dx,[bp+4]        ; [bp+4] is the parameter
    call    [XMM_Control]

    ; Was there an error?
    or    ax,ax
    jz    AEFail            ; Yup, fail
    mov    ax,dx            ; Return XMS Handle in AX
    mov    dx,0            ; Zero out DX
    jnz    AESuccess
AEFail:
    mov    dh,bl            ; Put error return in DH

AESuccess:
    pop    bp            ; Restore the stack
    ret

_XMM_AllocateExtended endp

;-------------------------------------------------------------------
;  long _XMM_FreeExtended(WORD Handle);
;    Deallocates an Extended Memory Block.
;-------------------------------------------------------------------
public _XMM_FreeExtended

_XMM_FreeExtended proc near

    push    bp            ; Create a stack
                    ; frame
    mov    bp,sp

    ; Call the XMS FreeExtended function.
    mov    ah,0Ah
    mov    dx,[bp+4]        ; [bp+4] is the parameter
    call    [XMM_Control]

    xor    dx,dx            ; Zero DX

    ; Was there an error?
    or    ax,ax
    jz    FESuccess        ; Nope, return
    mov    dh,bl            ; Yup, return err code in DH
FESuccess:
    pop    bp            ; Restore the stack
    ret

_XMM_FreeExtended endp

;-------------------------------------------------------------------
;  long _XMM_MoveExtended(XMSMOVE *pInfo);
;    Moves a block of data into/outof/within extended memory.
;-------------------------------------------------------------------
public _XMM_MoveExtended

_XMM_MoveExtended proc near

    push    bp            ; Create a stack frame
    mov    bp,sp

    ; Call the XMS MoveExtended function.
    mov    ah,0Bh
    mov    si,[bp+4]        ; [bp+4] is the parameter
    call    [XMM_Control]

    xor    dx,dx            ; Zero DX

    ; Was there an error?
    or    ax,ax
    jz    MESuccess        ; Nope, return
    mov    dh,bl            ; Yup, return err code in DH
MESuccess:
    pop    bp            ; Restore the stack
    ret

_XMM_MoveExtended endp


_TEXT    ends

end

Figure 5

SCRSAVE

scrxms.obj: scrxms.asm
    masm scrxms;

scrsave.obj: scrsave.c
    cl -c scrsave.c

scrsave.exe: scrxms.obj scrsave.obj
    link scrsave+scrxms,,,slibcec;

SCRSAVE.C

/********************************************************************
SCRSAVE.C -     Chip Anderson
XMS Demonstration Program.  Saves the contents of the text screen in
extended memory, writes a test pattern on the screen, then restores the
screen's original contents.  11/1/88
********************************************************************/

#include "stdio.h"

#define WORD unsigned int

WORD XMM_Installed(void);
WORD XMM_Version(void);
long XMM_AllocateExtended(WORD);
long XMM_FreeExtended(WORD);
long XMM_MoveExtended(struct XMM_Move *);

struct XMM_Move
  {
    unsigned long   Length;
    unsigned int    SourceHandle;
    unsigned long   SourceOffset;
    unsigned int    DestHandle;
    unsigned long   DestOffset;
  };

#define XMSERROR(x)    (unsigned char)((x)>>24)

main()

{
  int        i,j;
  long        lret;
  WORD        xHandle = 0;
  struct XMM_Move   MoveStruct;

  /* Display the credits. */
  printf("SCRSAVE - XMS Demonstration Program\n");

  /* Is an XMS Driver installed? */
  if (!XMM_Installed())
    {
      printf("No XMS Driver was found.\n");
      exit(-1);
    }

  /* Is it a version 2.00 driver (or higher)? */
  if (XMM_Version() < 0x200)
    {
      printf("This program requires an XMS driver version 2.00 or \
higher.\n");
      exit(-1);
    }

  /* We've found a legal XMS driver.  Now allocate 8K of extended
     memory for the screen. */

  lret = XMM_AllocateExtended(8);
  xHandle = (WORD)lret;

  if (!xHandle)
    {
      printf("Allocation Error: %X\n", XMSERROR(lret));
      exit(-1);
    }

  /* Copy the contents of the screen video buffer into the extended
     memory block we just allocated. */

  MoveStruct.SourceHandle = 0;    /* Source is in conventional
                                   memory */
  MoveStruct.SourceOffset = 0xB8000000L;/*Text screen data is a
                                          B800:0000 */
  MoveStruct.DestHandle   = xHandle;
  MoveStruct.DestOffset   = 0L;
  MoveStruct.Length    = 8000L;    /* 80 x 25 x 4 */

  lret = XMM_MoveExtended(&MoveStruct);

  if (!(WORD)lret)
    {
      printf("Memory Move Error: %X\n", XMSERROR(lret));
      exit(-1);
    }

  /* Fill the screen with garbage. */
  for (i=0; i < 22; i++)
      for (j=64; j < (64+81); j++)
    printf("%c",j);

  /* Prompt the user. */
  printf("Press Enter to restore the screen:");
  getchar();

  /* Restore the screen buffer. */
  MoveStruct.DestHandle   = 0;
  MoveStruct.DestOffset   = 0xB8000000L;
  MoveStruct.SourceHandle = xHandle;
  MoveStruct.SourceOffset = 0L;
  MoveStruct.Length    = 8000L;

  lret = XMM_MoveExtended(&MoveStruct);

  if (!(WORD)lret)
    {
      printf("Memory Move Error: %X\n", XMSERROR(lret));
      exit(-1);
    }

  /* Free the extended memory buffer. */
  XMM_FreeExtended(xHandle);

  printf("Program terminated normally.");
  exit(0);
}



Extended Memory Specification

Get XMS Version Number (Function 00h)

ARGS:    AH = 00h
    RETS:    AX = XMS version number
        BX = Driver internal revision number
        DX = 0001h if the HMA exists, 0000h otherwise

This function returns with AX equal to a 16-bit BCD number representing the
revision of the DOS Extended Memory Specification (XMS) that the driver
implements (for example, AX=0235h would mean that the driver implemented XMS
Version 2.35).

Request High Memory Area (Function 01h)

ARGS:    AH = 01h
        If the caller is a TSR or device driver,
        DX = Space needed in the HMA by the caller
         in bytes
        If the caller is an application program,
        DX = FFFFh
    RETS:   AX = 0001h if the HMA is assigned to the caller,
         0000h otherwise

This function attempts to reserve the 64Kb-by-16 byte high memory area (HMA)
for the caller. If the HMA is currently unused, the caller's size parameter
is compared to the /HMAMIN= parameter on the driver's command line. If the
value passed by the caller is greater than or equal to the amount specified
by the driver's parameter, the request succeeds. This provides the ability
to ensure that programs that use the HMA efficiently have priority over
those that do not.

Release High Memory Area (Function 02h)

ARGS:    AH = 02h
    RETS:     AX = 0001h if the HMA is successfully released,
0000h otherwise

This function releases the HMA and allows other programs to use it. Programs
that allocate the HMA must release it before exiting. When the HMA has been
released, any code or data stored in it becomes invalid and should not be
accessed.

Global Enable A20 (Function 03h)

ARGS:    AH = 03h
    RETS:    AX = 0001h if the A20 line is enabled,
         0000h  otherwise

This function attempts to enable the A20 line. It should only be used by
programs that have control of the HMA. The A20 line should be turned off via
Function 04h (Global Disable A20) before a program releases control of the
system. (Note: On many machines, toggling the A20 line is a relatively slow
operation.)

Global Disable A20 (Function 04h)

ARGS:    AH = 04h
    RETS:     AX = 0001h if the A20 line is disabled,
        0000h otherwise

This function attempts to disable the A20 line. It should only be used by
programs that have control of the HMA. The A20 line should be disabled
before a program releases control of the system.

Local Enable A20 (Function 05h)

ARGS:    AH = 05h
    RETS:    AX = 0001h if the A20 line is enabled,
        0000h otherwise

This function attempts to enable the A20 line. It should only be used by
programs that need direct access to extended memory. Programs that use this
function should call Function 06h (Local Disable A20) before releasing
control of the system.

Local Disable A20 (Function 06h)

ARGS:    AH = 06h
    RETS:    AX = 0001h if the function succeeds,
        0000h otherwise

This function cancels a previous call to Function 05h (Local Enable A20). It
should only be used by programs that need direct access to extended memory.
Previous calls to Function 05h must be canceled before releasing control of
the system.

Query A20 (Function 07h)

ARGS:    AH = 07h
    RETS:    AX = 0001h if the A20 line is physically enabled,
         0000h otherwise

This function checks to see if the A20 line is physically enabled. It does
this in a hardware-independent manner by seeing if memory wrap occurs.

Query Free Extended Memory (Function 08h)

ARGS:     AH = 08h
    RETS:     AX = Size of the largest free extended memory
         block in kilobytes
        DX = Total amount of free extended memory
        in  kilobytes

This function returns the size of the largest available extended memory
block (EMB) in the system. (Note: The 64Kb HMA is not included in the
returned value even if it is not in use.)

Allocate Extended Memory Block (Function 09h)

ARGS:     AH = 09h
        DX = Amount of extended memory being
        requested in kilobytes
    RETS:     AX = 0001h if the block is allocated,
        0000h otherwise
        DX = 16-bit handle to the allocated block

This function attempts to allocate a block of the given size out of the pool
of free extended memory. If a block is available, it is reserved for the
caller, and a 16-bit handle to that block is returned. The handle should be
used in all subsequent extended memory calls. If no memory was allocated,
the returned handle is null. (Note: Extended memory handles are scarce
resources. Programs should try to allocate as few as possible at any one
time. When all of a driver's handles are in use, any free extended memory is
unavailable.)

Free Extended Memory Block (Function 0Ah)

ARGS:    AH = 0Ah
        DX = Handle to the allocated block that should
        be freed
    RETS:    AX = 0001h if the block is successfully freed,
        0000h otherwise

This function frees a block of extended memory that was previously allocated
using Function 09h (Allocate Extended Memory Block). Programs that allocate
extended memory should free their memory blocks before exiting. When an
extended memory buffer is freed, its handle and all data stored in it become
invalid and should not be accessed.

Move Extended Memory Block (Function 0Bh)

ARGS:    AH = 0Bh
        DS:SI = Pointer to an Extended Memory Move
        Structure  (see below)
    RETS:    AX = 0001h if the move is successful,
        0000h otherwise

Extended Memory Move Structure Definition

ExtMemMoveStruct    struct
            Length    dd  ?    ; 32-bit number of bytes
                ; to  transfer
            SourceHandle    dw  ?    ; Handle of source
                ; block
            SourceOffset    dd  ?    ; 32-bit offset into
                ; source
            DestHandle    dw  ?    ; Handle of
                ; destination block
            DestOffset    dd  ?    ; 32-bit offset into
                ; destination block
        ExtMemMoveStruct    ends



This function attempts to transfer a block of data from one location to
another. It is primarily intended for moving blocks of data between
conventional memory and extended memory; however, it can be used for moving
blocks within conventional memory and within extended memory.

Length must be even. Although not required, WORD-aligned moves can be
significantly faster on most machines. DWORD aligned moves can be even
faster on 80386 machines.

SourceHandle and DestHandle do not have to refer to locked memory blocks.

(Note: If SourceHandle is set to 0000h, the SourceOffset is interpreted as a
standard segment:offset pair that refers to memory that is directly
accessible by the processor. The segment:offset pair is stored in Intel
DWORD notation. The same is true for DestHandle and DestOffset.)

If the source and destination blocks overlap, only forward moves (that is,
those in which the source base is less than the destination base) are
guaranteed to work properly.

Programs should not enable the A20 line before calling this function. The
state of the A20 line is preserved. This function is guaranteed to provide a
reasonable number of interrupt windows during long transfers.

Lock Extended Memory Block (Function 0Ch)

ARGS:     AH = 0Ch
        DX = Extended memory block handle to lock
    RETS:     AX = 0001h if the block is locked,
        0000h otherwise
        DX:BX = 32-bit linear address of the
        locked block



This function locks an EMB and returns its base address as a 32-bit linear
address. Locked memory blocks are guaranteed not to move. The 32-bit pointer
is only valid while the block is locked. Locked blocks should be unlocked as
soon as possible.

Unlock Extended Memory Block (Function 0Dh)

ARGS:     AH = 0Dh
        DX = Extended memory block handle to unlock
    RETS:    AX = 0001h if the block is unlocked,
        0000h otherwise



This function unlocks a locked EMB. Any 32-bit pointers into the block
become invalid and should no longer be used.

Get EMB Handle Information (Function 0Eh)

ARGS:    AH = 0Eh
        DX = Extended memory block handle
    RETS:    AX = 0001h if the block's information is found,
         0000h otherwise
        BH = The block's lock count
        BL = Number of free EMB handles in the system
        DX = The block's length in kilobytes

This function returns additional information about an EMB to the caller.
[Note: To get the block's base address, use Function 0Ch (Lock Extended
Memory Block).]



Reallocate Extended Memory Block (Function 0Fh)

ARGS:     AH = 0Fh
        BX = New size for the extended memory block
        in  kilobytes
        DX = Unlocked extended memory block handle
        to  reallocate
    RETS:    AX = 0001h if the block is reallocated,
        0000h otherwise

This function attempts to reallocate an unlocked EMB so that it becomes the
newly specified size. If the new block's size is smaller than the old one's,
all data at the upper end of the old block is lost.

Request Upper Memory Block (Function 10h)

ARGS:    AH = 10h
        DX = Size of requested memory block in
        paragraphs
    RETS:    AX = 0001h if the request is granted,
        0000h otherwise
        BX = Segment number of the upper
        memory block
        If the request is granted,
        DX = Actual size of the allocated block in
        paragraphs otherwise,
        DX = Size of the largest available upper memory
        block in paragraphs

This function attempts to allocate an upper memory block (UMB) to the
caller. If the function fails, the size of the largest free UMB is returned
in DX.

Release Upper Memory Block (Function 11h)

ARGS:     AH = 11h
        DX = Segment number of the upper
        memory block
    RETS:    AX = 0001h if the block was released,
        0000h otherwise

This function frees a previously allocated UMB. When an UMB has been
released, any code and data stored in it becomes invalid and should not be
accessed.

ERROR CODE INDEX

If AX=0000h when a function returns and the high bit of         BL is set,
BL equals

        80h if the function is not implemented
       81h if a VDISK device is detected
       82h if an A20 error occurs
       8Eh if a general driver error occurs
       8Fh if an unrecoverable driver error occurs
       90h if the HMA does not exist
       91h if the HMA is already in use
       92h if DX is less than the /HMAMIN= parameter
       93h if the HMA is not allocated
       94h if the A20 line is still enabled
       A0h if all extended memory is allocated
       A1h if all available extended memory handles are
           in  use
       A2h if the handle is invalid
       A3h if the SourceHandle is invalid
       A4h if the SourceOffset is invalid
       A5h if the DestHandle is invalid
       A6h if the DestOffset is invalid
       A7h if the Length is invalid
       A8h if the move has an invalid overlap
       A9h if a parity error occurs
       AAh if the block is not locked
       ABh if the block is locked
       ACh if the block's lock count overflows
       ADh if the lock fails
       B0h if a smaller UMB is available
       B1h if no UMBs are available
       B2h if the UMB segment number is invalid

Copyright (c) 1988, Microsoft Corporation



Exploring the Key Functions of the OS/2 Keyboard and Mouse Subsystems

 Richard Hale Shaw

The OS/2 operating systems (referred to herein as OS/2) offer services for
handling user input from both the keyboard and the mouse. This article will
explore the KBD and MOU subsystems, each of which contains a core set of
functions that you will be able to use regularly in most applications. We'll
also glance at some of the lesser-used, more esoteric functions. Finally,
we'll include a simple program that builds on the knowledge of threads and
the VIO subsystem that you've obtained from previous articles in this
series. It will show you how to incorporate OS/2 keyboard and mouse
functions quickly and easily into your own multithreaded applications.

KBD Subsystem

Since most PC programs are highly interactive and rely heavily on effective
interpretation of and efficient interaction with the keyboard, an
application's ability to deal effectively with the keyboard is second only
to its ability to handle its video output. OS/2 keyboard services therefore
provide applications with basic keyboard input facilities plus a variety of
additional keyboard functions.

In theory, an application can use the DosOpen Application Programming
Interface (API) routine to open its session's STDIN handle and receive
character-based data from the keyboard. In practice, however, it makes more
sense to use OS/2 keyboard services, particularly in highly interactive
programs. Or, if you wish, you can bypass the keyboard subsystem entirely
and go directly to the keyboard device driver via IOCTL services.

The keyboard device driver, KBD$, provides the physical keyboard support for
OS/2, which loads it from KBD01.SYS or KBD02.SYS. This device driver
services the keyboard interrupts and translates keyboard scan codes into
characters via the code page in use by the current screen group. KBD$ also
works with  the OS/2 kernel to create and maintain the device monitor chains
for each screen group. The primary concern of the keyboard subsystem,
however, is an application's ability to receive and process keystrokes.

With OS/2 keyboard subsystem services, a program can obtain a key's ASCII
code and scan code, the keyboard shift state, and a time stamp every time
the user presses a key. The keyboard state affects all keyboard input
functions and includes the status of the shift and toggle keys and the
turnaround (end-of-input) character. It also affects whether characters are
echoed and the character input mode (discussed below). The keyboard state
can be retrieved and altered for the session in which the program is
running.

More advanced keyboard services provide code-page switching, keyboard
monitoring, and a means of replacing the keyboard subsystem services with
functions of your own. Code-page switching allows an application to switch
the character set used for keyboard interpretation. Keyboard monitor
services allow background applications to monitor keyboard inputs for other
processes in the same screen group. Keyboard replacement services allow an
application to replace many of the keyboard service routines with its own.

Like its video services, OS/2 supplies the keyboard subsystem as
dynamic-link libraries (DLLs) and several API services. Two DLLs support the
keyboard API: KBDCALLS.DLL and BKSCALLS.DLL. KBDCALLS.DLL is known as the
keyboard router. It contains the entry points for the KBD API and passes the
calls to the subsystem in use in each screen group. This is usually
BKSCALLS.DLL (the default subsystem). BKSCALLS.DLL contains the Basic
Keyboard Subsystem and implements the KBD services. All or part of the
Keyboard Subsystem may be replaced in each screen group.

In "Using the OS/2 Video I/O Subsystem to Create Appealing Visual
Interfaces," MSJ (Vol. 4, No. 3), you saw that the OS/2 video services are a
superset of the IBM(R) PC ROM BIOS Int 10h function. Similarly, some OS/2
keyboard functions resemble ROM BIOS Int 16h. Each of the 16 keyboard
services are prefixed with Kbd to designate them as part of the KBD
subsystem. Even though many of them overlap some C standard library
routines, they are more powerful. You can continue to use C library routines
for generic keyboard input, but you will need to use KBD calls to gain full
control of the keyboard.

Logical Keyboards

As you have seen in this series of articles, OS/2 "virtualizes" the
resources of the computer. The OS/2 Session Manager executes every OS/2
application in its own screen group. A program started in the OS/2
Presentation Manager (referred to herein as PM) screen group will either
execute in a PM window or in its own session. For VIO, this meant that each
program or process can access a logical video buffer, even when running in a
PM window.

The same is true for applications that access the keyboard. Each screen
group has a logical keyboard (the virtualized physical keyboard). Any
process can access it without any preliminaries. When a user brings a screen
group into the foreground, the logical keyboard is "bound" to the physical
keyboard, and processes (or, more precisely, threads) in that screen group
can read any key the user presses. Once the user moves the screen group into
the background, the OS/2 operating system blocks any threads reading the
keyboard in that screen group. OS/2 provides the logical keyboard
automatically.

Each KBD routine requires a keyboard handle. Like VIO, the default keyboard
handle is 0, and an application's keyboard thread can call any KBD service
with a handle of 0. There is no need to open a handle as long as the
application isolates keyboard I/O to one thread (regardless of what else the
thread may be doing). If the process is going to spread keyboard input among
multiple threads, then it will have to perform some kind of synchronization.

Unlike VIO or MOU however, KBD allows you to create multiple logical
keyboard buffers for each session. Thus, you can have a process open an
additional logical keyboard handle and perform its own keyboard input. You
can use this capability when you want a program to perform keyboard input
through a handle other than the one provided to the screen group. You can
use the KbdOpen function to open a new logical keyboard and obtain a handle
to access it.

When you associate a logical keyboard handle with the physical keyboard, the
handle has the keyboard focus. Processes in the same screen group can give
or get the keyboard focus when necessary. To request the keyboard focus from
another process in the same screen group, you can call KbdGetFocus. Note
that you can only call KbdGetFocus in the foreground session; it will block
if called from a background thread and it will remain blocked until the
thread is in the foreground. KbdGetFocus will also block when another
process has the focus and will not obtain the focus until the other process
releases it. You can release the keyboard focus with KbdFreeFocus.

Obtaining and granting the keyboard focus forces processes in the same
session to use the keyboard on a serial basis: only one may get the keyboard
focus at a time. If, however, OS/2 is blocking more than one thread in a
session with KbdGetFocus, there is no way to predict which one will get the
keyboard focus when it is released. If there are no KbdGetFocus calls
pending when a thread calls KbdFreeFocus, the physical keyboard reverts to
the default keyboard buffer.

As I mentioned earlier, you don't have to open a keyboard handle; the
default handle will usually do. A process that uses the default logical
keyboard is, however, vulnerable to the interference of other processes in
the same screen group that may also use the default handle. For instance,
suppose one program asynchronously runs another program via DosExecPgm. If
both processes simultaneously attempt to interact with the default keyboard,
the results will be unpredictable. To protect against this, you can open a
new handle, obtain the keyboard focus, and begin interacting with the
keyboard. When you are done, you should release the keyboard focus and close
the handle.

There is no need for the process to worry about the integrity of keyboard
data when each process uses its own logical keyboard handle. Incidentally,
the OS/2 operating system uses KbdOpen and KbdGetFocus to obtain a default
logical keyboard handle for each new session (which the session references
with the default handle, 0). When you close a session, OS/2 deletes the
logical keyboard that corresponds to the default handle.

In summary, if your application performs keyboard input in only one of its
threads or if your application is not going to share the keyboard with
another application in the same session, you won't have to worry about
keyboard focus. Otherwise, you should consider it. The same is true for PM.
It will synchronize the access to the keyboard among the applications
running in its screen group.

Scan Codes

As you probably know, the PC keyboard does not generate ASCII codes for the
letters shown on the keys. Indeed, the keyboard does not know which
characters are associated with which keys. Each time a key is pressed, the
keyboard generates a value that corresponds to the key's position on the
keyboard, called a scan code. By using scan codes, you can use different
keyboards and easily adapt them to foreign languages. In addition, scan
codes let you develop and use alternative keyboard layouts, such as the
Dvorak keyboard.

When a key is pressed, the keyboard generates an interrupt in the main
system unit of the computer. This causes the OS/2 operating system to
execute a routine that reads the scan code of the key you pressed from the
keyboard port. The same is true when you release the key: the keyboard sends
a release code (consisting of the scan code plus 128) to the system unit.
The key press and release are sometimes called make and break interrupts,
respectively.

Each key is identified by its scan code (note that there are no scan codes
of 0). Under the MS-DOS(R) operating system, a PC's ROM-BIOS translates the
scan code into a key. Under OS/2, a routine translates a scan code into a
character via a translation table. When it finishes the translation, OS/2
places the scan code and the character into the keyboard buffer for the
foreground process's screen group. Then the foreground application can
retrieve it from there with a call to one of the KBD routines.

Other keys may influence the generation of a particular ANSI character code.
There are two groups of these keys: instance keys--Alt, Shift, Ctrl,
and SysRq, and lock keys-- CapsLock, NumLock, ScrollLock, and Ins. The
KBD routines use shift states to indicate the depression of a key in the
first group and whether the user has toggled a key in the second group. OS/2
uses an extended ASCII code set to incorporate the input from function keys,
numeric keypad keys, and keys pressed with Alt-Ctrl, for a total of 256
ASCII keystrokes. It can also support the double-byte character sets used by
some foreign languages.

Some keystrokes, such as the function keys and arrow keys, do not have an
ASCII equivalent. For these keystrokes, the character value returned is
either 0 or E0H after translation. You can safely use these two values to
indicate the presence of a non-ASCII key.

Although you must use the scan code when OS/2 returns a character code of
either 0 or E0H, the latter indicates that the key is unique to an enhanced
keyboard. For instance, the two sets of arrow keys on an enhanced keyboard
generate the same scan codes, but a character code of E0H indicates the
arrow key is from the independent arrow-key keypad. The same is true for
other keys duplicated on the enhanced keyboard. Therefore, you can
distinguish them in your program if you wish; that is something you usually
cannot do with most high-level language keyboard functions.

Cooked vs. Raw Mode

OS/2 offers two flavors of keyboard input: cooked (or ASCII) and raw (or
binary) mode. In cooked mode (the default), OS/2 will translate some
keyboard characters, as shown in Figure 1. In addition, it treats the
Carriage-Return character as an end-of-input character that is not passed
back from the keyboard. The input function you use will determine whether
the character is echoed to the screen and whether the extended keyboard keys
and editing keys are used.

In raw mode, OS/2 places each keyboard character in the foreground screen
group's keyboard buffer without modification. In addition, in this mode OS/2
never echoes the characters to the screen. In either mode, the standard
operating system keys are trapped by OS/2. These include Ctrl-Esc, Alt-Esc,
Ctrl-Alt-Del, Ctrl-Alt-NumLock, and PrtSc.

Figure 1

In cooked input mode, OS/2 will translate the keys listed here, and the
application will not receive them. Each key is given with a description of
the system action associated with it. In each case it affects only the
program currently accepting keyboard input.

^S      Pauses or continues the execution of the program.

^P      Toggles the printer echo.

^C    Terminates the program.

^Break    Terminates the program.

TAB    Inserts eight characters onto the screen at the cursor position.

^PrtSc    Dumps the input screen to the print spooler.

KBD Data Structures

You can use three principal data structures to access the KBD subsystem. The
first, KBDKEYINFO (shown in Figure 2), retrieves characters from the
keyboard buffer. As previously described, the chChar field is 0 or E0H when
you press a non-ASCII key. In addition, this structure contains the keyboard
scan code for the key, a byte that indicates the shift key state, and a time
stamp (in milliseconds). An additional structure member, fbStatus, indicates
a change in the shift key status (even if a key was not pressed) and signals
whether a character is only the first of two bytes in a foreign language
character set.

The second structure you can use, KBDINFO shown in Figure 3, retrieves and
sets the keyboard state. The fsMask member of this structure lets you change
the input mode (raw or cooked), whether or not character echo is on, and
lets you indicate which keyboard flags and settings you are changing. You
can also use this structure to set the turnaround character. Although the
turnaround character is usually a Carriage Return by default, it can be any
character. Note that the cb member of this structure must be set to the size
of the structure, thus allowing this structure to expand in the future.
Finally, you can use the STRINGINBUF structure (see Figure 4) for reading in
whole lines of input.

Figure 2

typedef struct _KBDKEYINFO
{
unsigned char chChar;    /* ASCII code         */
unsigned char chScan;    /* scan code          */
unsigned char fbStatus;    /* status code        */
unsigned char bNlsShift;    /* reserved, 0        */
unsigned fsState;    /* shift key state    */
unsigned long time;    /* time in mseconds   */
} KBDKEYINFO;

The fsState bit settings:

Bit     Mask     Meaning when bit is set

 0    0001    Right Shift pressed
 1    0002    Left Shift pressed
 2    0004    Ctrl key pressed
 3    0008    Alt key pressed
 4    0010    ScrollLock on
 5    0020    NumLock on
 6    0040    CapsLock on
 7    0080    INS on
 8    0100    Left Ctrl key pressed
 9    0200    Left Alt key pressed
10    0400    Right Ctrl key pressed
11    0800    Right Alt key pressed
12    1000    ScrollLock pressed
13    2000    NumLock pressed
14    4000    CapsLock pressed
15    8000    SysRq key pressed

Figure 3

typedef struct _KBDINFO
{
unsigned cb;    /* structure length    */
unsigned fsMask;    /* keyboard modes      */
unsigned chTurnAround;    /* end-of-input char   */
unsigned fsInterim;    /* interim char flag   */
unsigned fsState;    /* shift key state     */
} KBDINFO;

The fsMask bit settings:

Bit     Mask    Meaning when bit set

 0    0001    Echo on

 1    0002    Echo off

 2    0004    Raw mode

 3     0008    Cooked mode

 4    0010    Shift state to be changed

 5    0020    Interim flags to be changed

 6    0040    Turnaround char to be changed

 7    0080    Length of turnaround character (1 then 2 bytes,

        0 then 1 byte)

The fsState bit settings:

Bit     Mask    Meaning when set

 0    0001    Right Shift pressed

 1    0002    Left Shift pressed

 2    0004    Ctrl key pressed

 3    0008    Alt key pressed

 4    0010    ScrollLock on

 5    0020    NumLock on

 6    0040    CapsLock on

 7    0080    INS on

 8-15    reserved and always 0

Figure 4

typedef struct _STRINGINBUF
{
unsigned cb;    /* buffer length    */
unsigned cchIn;    /* bytes read       */
} STRINGINBUF;

KBD API Functions

If you've read previous installments in this series, you'll be familiar with
the OS/2 keyboard API functions (see Figure 5). Recall that we've used two
of the better known functions, KbdCharIn and KbdStringIn, in our example
programs. Generally, you'll find the keyboard API is simpler and easier to
use after using VIO; that's because there aren't nearly as many ways to
obtain keyboard input as there are to write to the video output.

Whenever possible, limit keyboard I/O to one thread, regardless of whether
that thread carries out other activities. In general, it is simpler to
create one thread whose sole purpose is to interface with the keyboard,
which means that it will block when there is no keyboard activity. Then that
thread can communicate with the other threads via some form of interprocess
communication (like a semaphore).

Figure 5

    KbdCharIn    returns keyboard character (ascii, scan code) and shift
state

    KbdClose    closes logical keyboard

*    KbdDeRegister    deregisters alternate subsystem

    KbdFlushBuffer    flushes keyboard buffer for screen group

    KbdFreeFocus    frees keyboard focus

    KbdGetCp    retrieves code page identifier

    KbdGetFocus     retrieves logical keyboard focus

    KbdGetStatus     retrieves keyboard status

    KbdOpen    opens logical keyboard

    KbdPeek    returns status of next character in keyboard buffer

*    KbdRegister    registers alternate keyboard subsystem

    KbdSetCp    sets code page identifier

    KbdSetCustXt    installs alternate translation table

    KbdSetStatus     modifies keyboard status

    KbdStringIn    reads line from keyboard

    KbdSynch     serializes keyboard access

    KbdXlate     translates scan codes to Ascii

*Not allowed or does nothing if calling process is run in PM window.

KbdCharIn

The most commonly used function in the KBD API is KbdCharIn. This function
takes three parameters: a pointer to the KBDKEYINFO structure, a wait/nowait
parameter, and a keyboard handle (usually 0). If the wait parameter is
IO_WAIT, the function will block the calling thread until keyboard input is
available; otherwise it will return immediately. Either way you'll need to
use the fbStatus member of the KBDKEYINFO structure to see if the user
pressed a key. Note that KbdCharIn does not echo the characters it returns.

Probably the biggest mistake made by the new OS/2 programmer is to write a
function that calls KbdCharIn in a loop with the wait flag set to IO_NOWAIT.
Since the function will return immediately regardless of whether a character
is available, it will burn up tremendous amounts of CPU time as it cycles
through the loop. Instead, it is wiser to dedicate one thread to keyboard
I/O and have that thread block on a call to KbdCharIn with the wait flag set
to IO_WAIT. When the thread does return, it can use a semaphore or some
other device to notify the main thread that it has received a character from
the keyboard. Alternatively, it can insert the character in an application
buffer and return to KbdCharIn.

KbdPeek

Another popular function is KbdPeek. It is similar to the kbhit standard
library function, since you can use it to interrogate the status of the
keyboard without removing any characters waiting in the keyboard buffer. It,
however, not only tells you whether a keypress is present, but can return
the keyboard status and the value of the next character (if there is one) as
well. Like KbdCharIn, it requires a KBDKEYINFO structure and a keyboard
handle as parameters.

Keyboard Status

KbdGetStatus and KbdSetStatus can return and set the keyboard status
information via the KBDINFO structure. To set part of the keyboard status,
you must first set the proper bit in fsMask in the KBDINFO structure to
indicate what type of function you are changing. Then you can set the value
of the related parameter. For instance, to turn on the CapsLock key, first,
you must set bit 4 in fsMask and bit 6 in fsState. Then a call to
KbdSetStatus will toggle this key on. For switching between cooked and raw
input modes and character echo on and off, you need only set the bits in
fsMask.

Note that these changes are local to the session in which the calling thread
is running: they do not affect the keyboard status of other sessions. You
should call KbdGetStatus first to initialize the structure so the rest of
the flags in KBDINFO will be unchanged.

KbdStringIn

The KbdStringIn function reads a string of characters from the keyboard
buffer. It requires four parameters: the buffer into which the characters
will be placed, the STRINGINBUF structure, a wait/nowait flag, and the
keyboard handle. You can read up to 255 characters with this function.

KbdStringIn performs differently, depending on which input mode is in place
when you call it. In raw mode, if you set the wait flag to IO_WAIT, the
function will read characters from the keyboard buffer until your buffer is
full (based on the buffer size placed in STRINGINBUF). If the wait flag is
IO_NOWAIT, the function will read however many characters are waiting in the
keyboard buffer (but not more than will fit into your buffer) and return
immediately. KbdStringIn will not echo the characters to the display and you
won't be able to use the editing keys in raw mode. The function places
extended ASCII keys in the buffer as a 2-byte sequence of 00h or E0h
followed by the scan code.

In cooked mode, you must use IO_WAIT for the wait flag. The function will
echo each character to the screen unless the echo flag is off. You can use
the standard editing keys, but the function will ignore all other extended
keys. TABS are expanded to spaces on the screen, using eight-column tab
stops; but the function will leave them in the buffer as 09h. You must
terminate input to KbdStringIn in cooked mode with ENTER, which the function
will place in the buffer as 0Dh. When the buffer contains one character less
than the requested buffer length, the function will discard additional
keystrokes and sound a warning beep until the user presses ENTER.

The cchIn member of the STRINGINBUF structure will indicate the number of
characters placed in the buffer upon return from the function. In cooked
mode, however, you can also set cchIn to the number of characters already in
the buffer before you call the function. This allows you to use the data
already in your buffer as an editing template.

Mouse Interface

Although the screen and keyboard are the traditional mechanisms for user
interaction on microcomputer systems, the mouse is fast becoming an
indispensable accessory. Nowhere is this more true than in the OS/2
environment, where the PM graphical interface seamlessly lends itself to a
mouse-based operation. OS/2 programs that do not support the mouse are
already rare; they are all character-based applications ported from DOS
before the release of OS/2 Version 1.1.

OS/2 offers two sets of mouse interface services: the core API services and
the PM routines. The PM mouse services make it easy to write programs that
use the mouse, since the program doesn't have to know you are using a mouse.
You can, however, use the core API services to take advantage of the mouse
interface without having to write a PM program.

The mouse device interface gives the appearance of being tightly linked to
the video screen. You can think of the screen and your desktop as grids. The
mouse movements on the desktop grid control parallel movements of the mouse
pointer upon the screen. Applications that use and manipulate a mouse may
concern themselves with correlating the mouse and the mouse pointer, the
definition of restricted areas, and, possibly, the shape of the mouse
pointer itself.

Mouse Drivers

The OS/2 environment uses two device drivers for its mouse interface:
POINTDD.SYS to draw the mouse pointer and MOUSEA0.SYS to interface with the
mouse itself. OS/2 loads POINTER$, the pointer draw driver from POINTDD.SYS.
POINTER$ draws and maintains the mouse pointer on screen and interfaces with
the mouse driver to determine where to draw the pointer. It represents the
motion of the mouse on screen by determining the mouse's new position,
erasing its old image at its previous position, and redrawing the new image
at the new position. POINTER$ is only operational in text mode. You must
disable the driver before switching into graphics mode. Ordinary graphics
applications must do much of the mouse interface work themselves, including
monitoring the movement of the mouse, supplying a pointer image, and drawing
the pointer on the screen. The OS/2 Presentation Manager takes care of these
chores for applications running in its screen group.

OS/2 loads the character device driver, MOUSE$, from MOUSEXX.SYS. There are
several different versions of MOUSEXX.SYS; which one you load will depend on
which mouse you use. OS/2 supports the full range of popular mice, including
the entire Microsoft(R) line and the IBM(R) PS/2(R) mouse. Note that whereas
the standard IBM/Microsoft mouse has two buttons, other models have one,
two, or three buttons, all of which OS/2 can support. The MOUSE$ driver
services the mouse interrupts and keeps track of the mouse position and
button states. It also provides the device I/O interface used by the Basic
Mouse Subsystem (BMS).

You can configure the mouse driver parameters and the mouse driver itself by
modifying its entry in CONFIG.SYS. For instance, my Microsoft Inport Mouse
uses the following entry in CONFIG.SYS:



DEVICE=C:\OS2\MOUSEA04.SYS



You can add parameters after the driver filename that specify which serial
port to use (if it's a serial mouse), the size of the mouse event queue
(explained below), and the mouse operational mode. For instance,

DEVICE=C:\OS2\MOUSEXX.SYS SERIAL=COM2,QSIZE=50,MODE=R

will set this driver to use communications port 2, with a queue size of 50.
The MODE parameter will tell OS/2 to use this mouse driver in real mode.

The SERIAL= parameter will force a serial mouse driver to use the specified
communications port; the default is COM1. QSIZE= will set the queue to hold
up to a specified number of mouse event packets (the default is 10). Note
that each packet requires 10 bytes of storage. MODE=P will set the driver to
operate only in protected mode; MODE=R will do the same for real mode. The
default MODE is both real and protected mode.

Mouse DLLs

The OS/2 operating system supplies two sets of DLLs that provide mouse
interface support to the API. BMCALLS.DLL contains the BMS, which implements
the MOU services and communicates with the MOUSE$ driver. Like its
counterpart in the keyboard subsystem, MOUCALLS.DLL is the mouse router. It
contains entry points for the MOU API functions and passes incoming calls to
the appropriate subsystem in each screen group. By default, this subsystem
is BMSCALLS; you can, however, replace it wholly or in part, just as you can
with other subsystems.

Mouse Events

The mouse is a read-only device. It can generate information about its
movements and the state of the mouse buttons (pressed or released). Each
piece of information--a mouse movement, a button press, or
both--is called a mouse event. The mouse can generate up to 30 events
per second. Like the keyboard, the mouse sends event information to the
mouse driver via a hardware interrupt.

OS/2 is capable of providing your application with the event information. It
organizes the mouse information as a packet and places it in a first-in
first-out (FIFO) queue until your program reads it. The packet includes the
current position of the mouse pointer and the state of the mouse buttons.
Although the queue is not very large, a considerable number of packets can
be generated before your program reads them. If this happens, newer packets
will overwrite older ones to make room in the queue. This is not a problem,
since your application will usually want to receive the most timely mouse
information. Unlike the keyboard, it is unusual that you would need to trace
every mouse event.

The OS/2 mouse services can return mouse pointer position information in two
ways. The default method returns the row and column position of the mouse
pointer in screen units. In text mode these correspond to character
positions; in graphics mode they are pel coordinates. A mouse pointer cannot
be "in between" positions. In addition, all mouse coordinates are set
relative to the upper-left-hand corner of the screen, which is row 0, column
0 in screen units.

OS/2 can also return the mouse position information in mickey counts (a name
taken from the cartoon character). A mickey is the smallest unit of mouse
measurement and corresponds to a specific measurement in centimeters on the
desktop. Mickey references use the x and y coordinates found in Cartesian
algebra. If the y value is negative, it means you have moved the mouse away
from you on the desk, leaving the pointer further up the screen. If the y
value is positive, it means you have moved the mouse toward you with a
corresponding movement of the mouse down the screen. If the x value is
negative, both the mouse and pointer have moved left; if the x value is
positive, both have moved right.

Scaling Factors

OS/2 does not force you to use a fixed relationship between mouse movements
on the desktop and mouse pointer movements on the screen. Instead, you can
change the scaling factors, that is, the number of mickeys you must move the
mouse in order to change the location of the mouse pointer by 1 screen unit.

If the scaling factor is 1, the pointer driver will move the mouse pointer 1
screen unit for every mickey you move the mouse. If the scaling factor is 2,
the pointer driver will move the mouse pointer 1 screen unit for every 2
mickeys you move the mouse, and so on. In other words, the greater the
scaling factor, the more you must physically move the mouse to move the
mouse pointer on the screen. That's why it's easy to make the case that the
choice of scaling factor is governed more by the amount of free desk space
than by preference. The larger the scaling factor, the more desk space
you'll need.

Collision Areas

The OS/2 mouse interface allows you to create collision areas. These are
areas of the screen into which the mouse pointer cannot enter. The pointer
driver accomplishes this simply by not drawing the mouse pointer inside the
designated area. A text mode application can use a collision area to
restrict mouse input from a certain part of the screen during a specific
operation. OS/2 does not support collision areas in graphics mode, so you'll
have to implement the collision areas yourself.

PM Mouse Support

The OS/2 Presentation Manager provides a set of mouse API functions that are
more advanced than those supplied by the standard mouse subsystem (see
Figure 6). As such, there is generally less work involved when dealing with
the mouse interface under Presentation Manager. Indeed, PM graphics
applications do not have to draw or move the mouse--the Presentation
Manager will do that for them. In addition, the PM mouse interface is
simpler, and the application does not have to keep track of the mouse
position. Instead, PM notifies the application of the user selection of a
menu item--the application does not have to know whether the user
selected the item with the keyboard or the mouse. Also note that
character-mode applications that use the mouse will generally work fine in a
PM window--except for the few side effects described below.

Figure 6

    MouClose    closes mouse handle

*    MouDeRegister    deregisters alternate subsystem

*    MouDrawPtr    displays mouse pointer

    MouFlushQue    flushes mouse event queue

    MouGetDevStatus    retrieves mouse status

    MouGetEventMask    retrieves mouse event mask

    MouGetHotKey    retrieves mouse hot key combination

    MouGetNumButtons    retrieves number of buttons

    MouGetNumMickeys    retrieves mickeys per centimeter

    MouGetNumQueEl    retrieves number of mouse events in queue

    MouGetPtrPos    retrieves mouse pointer position

*    MouGetPtrShape    retrieves pointer bit mask

*    MouGetScaleFact    retrieves scaling factors

    MouInitReal    initializes real mode mouse

    MouOpen    opens mouse handle

    MouReadEventQue    retrieves mouse event information

*    MouRegister    registers alternate subsystem

    MouRemovePtr    removes mouse pointer (creates collision area)

*    MouSetDevStatus    sets mouse device status

    MouSetEventMask    sets mouse event mask

    MouSetHotKey    sets mouse hot key

    MouSetPtrPos    sets mouse pointer position

*    MouSetPtrShape    sets mouse pointer mask

*    MouSetScaleFact    sets scale factors

    MouSynch    synchronizes mouse access

*Not allowed or does nothing if calling process is run in PM window.

Family Mode Support

We haven't addressed Family Mode issues since the first OS/2 program in this
series, "Planning and Writing a Multithreaded OS/2 Program with Microsoft
C," MSJ (Vol. 4, No. 2). That's because we haven't had to. The moment we
wrote the first multithreaded program, we couldn't consider Family Mode
since DOS does not support multithreaded programs. Many of the VIO and KBD
API services, however, have Family Mode counterparts that let you write
bound programs that run under both OS/2 and DOS (as long as they are not
multithreaded). The MOU services, on the other hand, are not available under
DOS, and you'll have to use Int 33h. To develop an application that uses the
mouse and runs under both real and protected mode, you'll have to write
functions that call the MOU API in protected mode and Int 33h in real mode.

Opening the Handle

Since OS/2 does not provide a default handle for the mouse interface, as it
does for the keyboard and screen, you must first open a mouse handle before
your application can use the mouse. You can use the MouOpen function to open
the mouse and receive a handle that your application can use. This will
initialize the mouse driver for your application's session, but it will not
display the mouse pointer. That's because when you open a mouse handle, the
entire screen is a collision area and the mouse pointer will not appear
until your application calls MouDrawPtr. You can close the mouse pointer
with MouClose.

The Basic Mouse Subsystem can synchronize access to the physical mouse by
several sessions, but it cannot synchronize mouse access by multiple
processes in the same session. The mouse threads in a screen group share the
same mouse event queue. Thus, as with the keyboard, if your application must
share mouse access with other processes in its screen group, you'll have to
make provisions to synchronize this access. Unfortunately, there is no MOU
equivalent to KbdGetFocus or KbdFreeFocus, so you'll have to perform this
synchronization yourself. But OS/2 does provide an API service, MouSynch, to
do this; in fact, the Presentation Manager uses this same facility to
coordinate mouse access among the processes in its screen group.

Note that like the keyboard, OS/2 blocks a background thread that tries to
read the mouse. Thus, it is wise not only to limit mouse access to one
thread in your program but to make mouse access the thread's raison d'etre.

In text mode, it is relatively simple to access the mouse: open the mouse
with MouOpen, make the pointer appear with MouDrawPtr, and then begin to
evaluate the mouse events with MouReadEventQue. In a graphics mode, the
process is slightly more complex. As I mentioned earlier, a graphics
application must perform its own mouse manipulation. Therefore you must open
the mouse while the application is in text mode and then disable it with
MouSetDevStatus. You then change the video mode to graphics with VioSetMode
and create your own mouse pointer with MouSetPtrShape. The application
should call MouReadEventQue to interpret mouse events, but it will need to
use MouSetPtrPos to move the pointer accordingly.

Reading from the Mouse Queue

As we have already mentioned, every time you move the mouse or press a mouse
button, OS/2 will place an event information packet on the end of the mouse
queue. You can use MouReadEventQue to read information from the queue. This
function takes three parameters: a pointer to the MOUEVENTINFO structure, a
wait/nowait parameter, and a mouse handle.

The MOUEVENTINFO structure, shown in Figure 7, contains the information
returned by the function. The fs member contains bits that indicate the
current state of the mouse buttons and whether the mouse has moved. The Time
member records the time of the event in milliseconds. You can use Time to
determine when an event occurred relative to the previous or next event in
the queue. This allows your application to interpret double-clicks of the
mouse button, intervals between mouse movements, and so on. MOUEVENTINFO
also includes the row and column coordinates of the current mouse position.
These are in screen units, which are character rows and columns for text
mode or pels in graphics mode.

The wait/nowait parameter to MouReadEventQue allows the calling thread to
block if there are no mouse events in the queue. Note that unlike other OS/2
API functions, you must pass this parameter by reference instead of by value
(that is, you must set a variable to the value and pass the address of the
variable). Also note that this parameter uses 1 to wait and 0 to return
immediately, which again differs from other API functions.

MouGetPtrPos is an alternative to MouReadEventQue. This function will return
the mouse coordinates, but it won't return the state of the mouse buttons.
MouGetPtrPos uses the PTRLOC structure shown in Figure 8.

The default OS/2 mouse queue size is 10. Although this should be sufficient
in most cases, you can set a value from 1 to 100 for it by passing a
parameter to the mouse driver specified in CONFIG.SYS. You can use
MouGetEventMask and MouSetEventMask to screen out certain types of events.
This is particularly useful if, for instance, you want an application to
ignore presses of the second mouse button.

You can use MouGetScaleFact to retrieve the currently used scaling factor
and use  MouSetScaleFact to set a new scaling factor. These functions take
two parameters: a pointer to the SCALEFACT structure and a mouse handle. The
SCALEFACT structure, shown in Figure 9, allows you to set scaling factors
from 1 to 32,767, although a more practical range is from 1 to about 24.

Figure 7

typedef struct _MOUEVENTINFO
{
unsigned fs;               /* event mask         */
unsigned long Time;        /* system timestamp   */
unsigned row;              /* pointer position   */
unsigned col;              /* pointer position   */
} MOUEVENTINFO;

The fs bit settings (this is 0 if no event occurred):

Bit        Mask        Meaning when bit set

 0    0001    Mouse moved, no buttons pressed

 1    0002    Mouse moved, button 1 pressed

 2    0004    No movement, button 1 pressed

 3    0008    Mouse moved, button 2 pressed

 4    0010    No movement, button 2 pressed

 5    0020    Mouse moved, button 3 pressed

 6    0040    No movement, button 3 pressed

 7    0080    Reserved, 0

Figure 8

typedef struct _PTRLOC
{
unsigned row;    /* pointer position    */
unsigned col;    /* pointer position    */
} PTRLOC;

Figure 9

typedef struct _SCALEFACT
{
unsigned rowScale;    /* vertical factor    */
unsigned colScale;    /* horizontal factor    */
} SCALEFACT;

The Time member contains the system time (in milliseconds) at which the
mouse event occurred. The row and column members contain the screen location
of the mouse pointer. By default this is in screen units, but a call to
MouSetDevStatus can change this to mickeys.

KMSTATUS.C

The program presented with this article, KMSTATUS.C, illustrates how you can
use the OS/2 keyboard and mouse subsystems. When you run the program, it
will present a display that splits the screen into two sections (see Figure
10). The upper section contains mouse-related event information, and the
lower section contains keyboard information.

The mouse event information includes the button and mickey counts, the time
of each mouse event, and the scaling factors. It also includes the row and
column of the mouse pointer position. As you move the mouse or press the
mouse buttons, the program updates this event information. Specifically,
pressing and releasing the mouse buttons, will cause the program to display
the mouse pointer location and the time stamp for each button. Each time you
press a mouse button, the program will clear the information for that button
and print the press information. It will not print the release information
until you release the mouse button. The program will print information on
each of three mouse buttons.

Below the mouse statistical information are four push buttons, which are
crude imitations of the push buttons in a PM window. You can, in fact, use
these push buttons as you would in the PM environment: move the mouse
pointer to the push button and press either the left or right mouse button
(if you are using a three-button mouse, use mouse buttons 1 and 2).

Two push buttons control the horizontal and vertical scaling factors. If you
click left (press the left mouse button) on the horizontal scaling factor
push button the program will increment the value. If you click right, the
program will decrement the value. You can modify the vertical scaling factor
by clicking on its push button. The scaling factor information at the top of
the screen will reflect these changes.

A third push button on the left of the screen is not really a push button at
all; what it does is demark a collision area. Appropriately, a quote inside
the button parodies a popular saying with, "You can't get here from there  "
What it means is that you will not be able to move the mouse pointer into
this push button; if you do, it will disappear. The program will continue to
update statistical information at the top of the screen, but the mouse
pointer will not reappear until you move it out of the collision area. Note
that this is not the case when you run the program in a PM window. There,
the pointer will continue to display, even inside of the collision area. You
can use the last push button (on the right of the screen) to quit the
program.

The lower portion of the screen contains statistical information on the
keyboard. As you press any keyboard keys, the program will display the Ascii
code and Scan code received for each. It will also display the keyboard
state for the Shift, Ctrl, and Alt keys. Since it is reporting the state of
the keyboard for each keyboard event, these will stay on the screen until
the next keyboard event.

You can change the keyboard input mode (raw or cooked), by using the mouse
to press the Input Mode push button. This will toggle the input mode display
when the next keyboard event occurs.

Examining Threads

The main( ) function for KMSTATUS.C is simple. It starts a mouse thread and
a keyboard thread. Then it calls DosSleep to sleep indefinitely. The mouse
thread starts by opening a mouse handle and clearing the screen. Then it
displays the various push buttons, displays the mouse pointer, and creates
the collision area. The function prints the fixed mouse information, flushes
the mouse queue, and enters a loop.

During each cycle of the loop, the mouse thread reads an event from the
mouse queue and prints that information on the screen (see Figure 11). if a
mouse button was pressed, it also prints the mouse pointer location and the
time stamp. It checks the pointer location and time stamp for each mouse
button individually.

The mouse thread uses the ButtonPressed function to see if a mouse button
was pressed while the mouse pointer was over one of the push buttons. For
the scaling factor push buttons, it adjusts the scaling factors. For the
Input Mode push button, it toggles a variable that the keyboard thread will
use to change the input mode.

If the user has selected the Quit push button, the mouse thread will break
out of the loop. it will then close the mouse handle, clear the screen, and
call DosExit to terminate the program. The mouse thread function contains a
DosSleep at the bottom of the loop, which prevents the function from
stealing too many CPU cycles.

The keyboard thread function is even simpler than the mouse thread function.
It flushes the keyboard buffer and enters a loop. At the top of the loop it
examines a variable to see if the mouse thread has signaled a change in the
input mode. If so, it toggles the input mode bits and sets the keyboard
status with KbdSetStatus. Then it calls KbdGetStatus to retrieve the
keyboard state and calls KbdCharIn to wait for the next keyboard input
character.

When the keyboard thread receives a keyboard character, it prints the
character's Ascii value, the Scan code, and the state of the Shift keys (see
Figure 12). Then it returns to the top of the loop. The keyboard thread
never terminates; it dies when the mouse thread terminates the program. I
didn't add a DosSleep to this thread since it blocks on keyboard input.

This program should give you a good understanding of how the KBD and MOU
subsystems work. It certainly demonstrates how simple it is to use these
subsystems in an application. Moreover, you should be able to use some of
this code in your own programs.

Summary

This article emphasizes the lack of complexity of the KBD and MOU
subsystems. They are not only easy to use but allow an application to take
advantage of the OS/2 input facilities rather quickly. Of the two
subsystems, you'll probably use KBD the most but exploit it the least; that
is, you will probably use KbdCharIn most of the time. Although adding mouse
support to an application requires a little more work, you won't have a
problem if you keep the mouse code in a separate thread. In some
applications, you may want to have one thread that receives input from
either the keyboard or the mouse. This thread can then provide that input to
the rest of the program, hiding the details of the input process.

Now that we have completed our exploration of the three OS/2 subsystems,
it's time to move on. In the next article, we will explore some OS/2
Interprocess Communications (IPC) facilities other than threads, including
queues, monitors, signals, and pipes.


Everything You Always Wanted to Know About the MS-DOS EXEC Function...

 Ray Duncan

The MS-DOS(R) system loader, which brings COM or EXE files from disk into
memory and executes them, can be invoked by any program with the MS-DOS EXEC
function (Interrupt 21H Function 4BH). The default MS-DOS command
interpreter, COMMAND.COM, uses the EXEC function to load and run its
external commands, such as CHKDSK, as well as other application programs.
Many popular commercial programs, such as databases and word processors, use
EXEC to load and run subsidiary programs (spelling checkers, for example)
or to load and run a second copy of COMMAND.COM. This allows a user to run
subsidiary programs or enter MS-DOS commands without losing his or her
current working context.

When EXEC is used by one program (called the parent) to load and run another
(called the child), the parent can pass certain information to the child in
the form of a set of strings called the environment, a command line, and two
file control blocks. The child program also inherits the parent program's
handles for the MS-DOS standard devices and for any other files or character
devices the parent has opened (unless the open operation was performed with
the "noninheritance" option). Any operations performed by the child on
inherited handles, such as seeks or file I/O, also affect the file pointers
associated with the parent's handles. A child program can, in turn, load
another program, and the cycle can be repeated until the system's memory
area is exhausted.

Because MS-DOS1 is not a multitasking operating system, a child program has
complete control of the system until it has finished its work; the parent
program is suspended. This type of processing is sometimes called
synchronous execution. When the child terminates, the parent regains
control and can use another system function call (Interrupt 21H Function
4DH) to obtain the child's return code and determine whether the program
terminated normally, because of a critical hardware error, or because the
user entered a Control-C.

In addition to loading a child program, EXEC can also be used to load
subprograms and overlays for application programs written in assembly
language or in a high-level language that does not include an overlay
manager in its run-time library. Such overlays typically cannot be run as
self-contained programs; most require "helper" routines or data in the
application's root segment.

The EXEC function is available only with MS-DOS versions 2.0 and later. With
MS-DOS version 1.x, a parent program can use Interrupt 21H Function 26H to
create a program segment prefix for a child but must carry out the
loading, relocation, and execution of the child's code and data itself,
without any assistance from the operating system.

How EXEC Works

When the EXEC function receives a request to execute a program, it first
attempts to locate and open the specified program file. If the file cannot
be found, EXEC fails immediately and returns an error code to the caller.

If the file exists, EXEC opens the file, determines its size, and inspects
the first block of the file. If the first 2 bytes of the block are the ASCII
characters MZ, the file is assumed to contain an EXE load module, and the
sizes of the program's code, data, and stack segments are obtained from the
EXE file header. Otherwise, the entire file is assumed to be an absolute
load image (a COM program). The actual filename extension (COM or EXE) is
ignored in this determination.

At this point, the amount of memory needed to load the program is known,
so EXEC attempts to allocate two blocks of memory: one to hold the new
program's environment and one to contain the program's code, data, and stack
segments. Assuming that enough memory is available to hold the program
itself, the amount actually allocated to the program varies with its type.
Programs of the COM type are usually given all the free memory in the system
(unless the memory area has previously become fragmented), while the amount
assigned to an EXE program is controlled by two fields in the file header,
MINALLOC and MAXALLOC, that are set by the Microsoft(R) Object Linker
(LINK).

EXEC then copies the environment from the parent into the memory
allocated for the child's environment, builds a program segment prefix (PSP)
at the base of the child's program memory block, and copies into the child's
PSP the command tail and the two default file control blocks passed by the
parent. The previous contents of the terminate (Interrupt 22H), Control-C
(Interrupt 23H), and critical error (Interrupt 24H) vectors are saved in
the new PSP, and the terminate vector is updated so that control will return
to the parent program when the child terminates or is aborted.

The actual code and data portions of the child program are then read from
the disk file into the program memory block above the newly constructed PSP.
If the child is an EXE program, a relocation table in the file header is
used to fix up segment references within the program to reflect its
actual load address.

Finally, the EXEC function sets up the CPU registers and stack according to
the program type and transfers control to the program. The entry point for a
COM file is always offset 100H within the program memory block (the first
byte following the PSP). The entry point for an EXE file is specified in the
file header and can be anywhere within the program.

When EXEC is used to load and execute an overlay rather than a child
program, its operation is much simpler than described above. For an
overlay, EXEC does not attempt to allocate memory or build a PSP or
environment. It simply loads the contents of the file to the address
specified by the calling program and performs any necessary
relocations (if the overlay file has an EXE header), using a segment
value that is also supplied by the caller. EXEC then returns to the program
that invoked it, rather than transferring control to the code in the newly
loaded file. The requesting program is responsible for calling the
overlay at the appropriate location.

Using EXEC to Load  a Program

When one program loads and executes another, it must follow these steps:

1.    Ensure that enough free memory is available to hold the code, data,
and stack of the child program.

2.    Set up the information to be passed to EXEC and the child program.

3.    Call the MS-DOS EXEC function to run the child program.

4.    Recover and examine the child program's termination and return codes.

Making Memory Available

MS-DOS typically allocates all available memory to a COM or EXE program when
it is loaded. (The infrequent exceptions to this rule occur when the
transient program area is fragmented by the presence of resident data or
programs or when an EXE program is loaded that was linked with the
/CPARMAXALLOC switch or modified with EXEMOD.) Therefore, before a program
can load another program, it must free any memory it does not need for its
own code, data, and stack.

The extra memory is released with a call to the MS-DOS Resize Memory Block
function (Interrupt 21H Function 4AH). In this case, the segment address of
the parent's PSP is passed in the ES register, and the BX register holds
the number of paragraphs of memory the program must retain for its own
use. If the prospective parent is a COM program, it must be certain to move
its stack to a safe area if it is reducing its memory allocation to less
than 64Kb.

Preparing Parameters for EXEC

When used to load and execute a program, the EXEC function must be
supplied with two principal parameters:

■    the address of the child program's pathname

■    the address of a parameter block

The parameter block, in turn, contains the addresses of information to be
passed to the child program.

The Program Name

The pathname for the child program must be an unambiguous, null-terminated
(ASCIIZ) file specification (no wildcard characters). If a path is not
included, the current directory is searched for the program; if a drive
specifier is not present, the default drive is used.

The Parameter Block

The parameter block contains the addresses of four data items:

■    the environment block

■    the command tail

■    the two default file control blocks (FCBs)

The position reserved in the parameter block for the pointer to an
environment is only 2 bytes and contains a segment address, because an
environment is always paragraph aligned (its address is always evenly
divisible by 16); a value of 0000H indicates the parent program's
environment should be inherited unchanged. The remaining three addresses are
all doubleword addresses in the standard Intel format, with an offset value
in the lower word and a segment value in the upper word. (See Figure 1).

Figure 1

To Call

AH    =    4BH
AL    =    00H    load and execute child process
        03H    load overlay
DS:DX    = segment:offset of ASCIIZ pathname for an executable program file
ES:BX    = segment:offset of parameter block

Returns

If function is successful:

Carry flag is clear.

Other registers are preserved if MS-DOS version 3.0 or later, destroyed if
MS-DOS

versions 2.x.

If function is not successful:

Carry flag is set.

AX    =    error code

Parameter Block Format

Offset    Contents

If AL = 00H (load and execute program)

00H    Segment pointer of the environment to be passed

02H    Offset of command-line tail for the new PSP

04H    Segment of command-line tail for the new PSP

06H    Offset of first file control block, to be copied into new PSP at

    offset 5CH

08H    Segment of first file control block

0AH    Offset of second file control block, to be copied into new PSP at

    offset 6CH

0CH    Segment of second file control block

if AL = 03H (load overlay):

00H    Segment address where overlay is to be loaded

02H    Relocation factor to apply to loaded image

The Environment

An environment always begins on a paragraph boundary and is composed of a
series of null-terminated (ASCIIZ) strings of the form:

name=variable

The end of the entire set of strings is indicated by an additional null
byte.

If the environment pointer in the parameter block supplied to an EXEC call
contains zero, the child simply acquires a copy of the parent's environment.
The parent can, however, provide a segment pointer to a different or
expanded set of strings. In either case, under MS-DOS versions 3.0 and
later, EXEC appends the child program's fully qualified pathname to its
environment block. The maximum size of an environment is 32Kb, so very large
amounts of information can be passed between programs by this mechanism.

The original, or master, environment for the system is owned by the
command processor that is loaded when the system is turned on or restarted
(usually COMMAND.COM). Strings are placed into the system's master
environment by COMMAND.COM as a result of PATH, SHELL, PROMPT, and SET
commands, with default values always present for the first two. For
example, if an MS-DOS version 3.2 system is started from drive C and a PATH
command is not present in the AUTOEXEC.BAT file nor a SHELL command in the
CONFIG.SYS file, the master environment will contain the two strings:

PATH=
COMSPEC=C:\COMMAND.COM

These specifications are used by COMMAND.COM to search for executable
"external" commands and to find its own executable file on the disk so
that it can reload its transient portion when necessary. When the PROMPT
string is present (as a result of a previous PROMPT or SET PROMPT command),
COMMAND.COM uses it to tailor the prompt displayed to the user.

Other strings in the environment are used only for informational
purposes by transient programs and do not affect the operation of the
operating system proper. For example, the Microsoft C Compiler and the
Microsoft Object Linker look in the environment for INCLUDE, LIB, and TMP
strings that specify the location of include files, library files, and
temporary working files. Figure 2 contains a hex dump of a typical
environment block.

The Command Tail

The command tail to be passed to the child program takes the form of a byte
indicating the length of the remainder of the command tail, followed
by a string of ASCII characters terminated with an ASCII carriage return
(0DH); the carriage return is not included in the length byte. The
command tail can include switches, filenames, and other parameters that can
be inspected by the child program and used to influence its operation. It
is copied into the child program's PSP at offset 80H.

When COMMAND.COM uses EXEC to run a program, it passes a command tail that
includes everything the user typed in the command line except the name of
the program and any redirection parameters. I/O redirection is processed
within COMMAND.COM itself and is manifest in the behavior of the standard
device handles that are inherited by the child program. Any other program
that uses EXEC to run a child program must try to perform any necessary
redirection on its own and must supply an appropriate command tail so that
the child program will behave as though it had been loaded by COMMAND.COM.

The Default File Control Blocks

The two default FCBs pointed to by the EXEC parameter block are copied into
the child program's PSP at offsets 5CH and 6CH.

Few of the currently popular application programs use FCBs for file and
record I/O because FCBs do not support the hierarchical directory
structure. But some programs do inspect the default FCBs as a quick way to
isolate the first two switches or other parameters from the command tail.
Therefore, to make its own identity transparent to the child program, the
parent should emulate the action of COMMAND.COM by parsing the first two
parameters of the command tail into the default FCBs. This can be
conveniently accomplished with the MS-DOS function Parse Filename (Interrupt
21H Function 29H).

If the child program does not require one or both of the default FCBs, the
corresponding address in the parameter block can be initialized to point
to two dummy FCBs in the application's memory space. These dummy FCBs
should consist of 1 zero byte followed by 11 bytes containing ASCII blank
characters (20H).

Running the Child Program

After the parent program has constructed the necessary parameters, it
can invoke the EXEC function by issuing Interrupt 21H with the registers
set as follows:

AH    =    4BH

AL    =     00H (EXEC subfunction to load and execute program)

DS:DX    =    segment:offset of program pathname

ES:BX    =    segment:offset of parameter block

Upon return from the software interrupt, the parent must test the carry
flag to determine whether the child program did, in fact, run. If the carry
flag is clear, the child program was successfully loaded and given control.
If the carry flag is set, the EXEC function failed, and the error code
returned in AX can be examined to determine why. The usual reasons are:

■    the specified file could not be found

■    the file was found, but not enough memory was free to load it

Other causes are uncommon and can be symptoms of more severe problems in the
system as a whole (such as damage to disk files or to the memory image of
MS-DOS). With MS-DOS versions 3.0 and later, additional details about the
cause of an EXEC failure can be obtained by subsequently calling Interrupt
21H Function 59H (Get Extended Error Information).

In general, supplying either an invalid address for an EXEC parameter block
or invalid addresses within the parameter block itself does not cause a
failure of the EXEC function, but may result in the child program behaving
in unexpected ways.

Special Considerations

With MS-DOS versions 2.x, the previous contents of all the parent registers
except for CS:IP can be destroyed after an EXEC call, including the stack
pointer in SS:SP. Consequently, before issuing the EXEC call, the parent
must push onto the stack the contents of any registers that it needs to
preserve, and then it must save the stack segment and offset in a location
that is addressable with the CS segment register. Upon return, the stack
segment and offset can be loaded into SS:SP with code segment overrides, and
then the other registers can be restored by popping them off the stack. With
MS-DOS versions 3.0 and later, registers are preserved across an EXEC call
in the usual fashion.

Note: The code segments of Windows applications that use this technique
should be given the IMPURE attribute.

In addition, a bug in MS-DOS version 2.0 and in PC-DOS versions 2.0 and
2.1 causes an arbitrary doubleword in the parent's stack segment to be
destroyed during an EXEC call. When the parent is a COM program and SS=PSP,
the damaged location falls within the PSP and does no harm; however, in the
case of an EXE parent where DS = SS, the affected location may overlap the
data segment and cause aberrant behavior or even a crash after the return
from EXEC. This bug was fixed in MS-DOS versions 2.11 and later and in
PC-DOS versions 3.0 and later.

Examining the Child Program's Return Codes

If the EXEC function succeeds, the parent program can call Interrupt 21H
Function 4DH (Get Return Code of Child Process) to learn whether the child
executed normally to completion and passed back a return code or was
terminated by the operating system because of an external event. Function
4DH returns:

AH    =    termination type:

        00H    Child terminated normally (that is, exited via Interrupt 20H
or Interrupt 21H Function 00H or Function 4CH).

        01H    Child was terminated by user's entry of a Ctrl-C.

        02H    Child was terminated by critical error handler (either the
user responded with A to the Abort, Retry, Ignore prompt from the system's
default Interrupt 24H handler, or a     custom Interrupt 24H handler
returned to MS-DOS with action code = 02H in register AL).

        03H    Child terminated normally and stayed resident (that is,
exited via Interrupt 21H Function 31H or Interrupt 27H).

AL    =    return code:

        Value passed by the child program in register AL when it
terminated with Interrupt 21H Function 4CH or 31H.

        00H if the child terminated using Interrupt 20H, Interrupt 27H,
or Interrupt 21H Function 00H.

These values are only guaranteed to be returned once by Function 4DH.
Thus, a subsequent call to Function 4DH, without an intervening EXEC call,
does not necessarily return any useful information. Additionally, if
Function 4DH is called without a preceding successful EXEC call, the
returned values are meaningless.

Using COMMAND.COM with EXEC

An application program can "shell" to MS-DOS--that is, provide the user
with an MS-DOS prompt without terminating--by using EXEC to load and
execute a secondary copy of COMMAND.COM with an empty command tail. The
application can obtain the location of the COMMAND.COM disk file by
inspecting its own environment for the COMSPEC string. The user returns to
the application from the secondary command processor by typing exit at
the COMMAND.COM prompt.

Batch-file interpretation is carried out by COMMAND.COM, and a batch (BAT)
file cannot be called using the EXEC function directly. Similarly, the
sequential search for COM, EXE, and BAT files in all of the locations
specified in the environment's PATH variable is a function of COMMAND.COM,
rather than of EXEC. To execute a batch file or search the system path for a
program, an application program can use EXEC to load and execute a
secondary copy of COMMAND.COM to use as an intermediary. The application
finds the location of COMMAND.COM as described in the preceding
paragraph, but it passes a command tail in the form:

/C program parameter1 parameter2 ...

where program is the EXE, COM, or BAT file to be executed. When program
terminates, the secondary copy of COMMAND.COM exits and returns control to
the parent.

A Parent and Child Example

The source programs in Figures 3 and 4, PARENT.ASM and CHILD.ASM, illustrate
how one program uses EXEC to load another.

PARENT.ASM can be assembled and linked into the executable program
PARENT.EXE with the following commands:

C>MASM PARENT;  <Enter>

C>LINK PARENT;  <Enter>

Similarly, CHILD.ASM can be assembled and linked into the file CHILD.EXE as
follows:

C>MASM CHILD;  <Enter>

C>LINK CHILD;  <Enter>

When PARENT.EXE is executed with the command

C>PARENT  <Enter>

PARENT reduces the size of its main memory block with a call to Interrupt
21H Function 4AH, to maximize the amount of free memory in the system, and
then calls the EXEC function to load and execute CHILD.EXE.

CHILD.EXE runs exactly as though it had been loaded directly by
COMMAND.COM. CHILD resets the DS segment register to point to its own data
segment, uses Interrupt 21H Function 40H to display a message on standard
output, and then terminates using Interrupt 21H Function 4CH, passing a
return code of zero.

When PARENT.EXE regains control, it first checks the carry flag to determine
whether the EXEC call succeeded. If the EXEC call failed, PARENT displays an
error message and terminates with Interrupt 21H Function 4CH, itself passing
a nonzero return code to COMMAND.COM to indicate an error.

Otherwise, PARENT uses Interrupt 21H Function 4DH to obtain CHILD.EXE's
termination type and return code, which it converts to ASCII and displays.
PARENT then terminates using Interrupt 21H Function 4CH and passes a
return code of zero to COMMAND.COM to indicate success. COMMAND.COM in
turn receives control and displays a new user prompt.

Using EXEC to Load Overlays

Loading overlays with the EXEC function is much less complex than using EXEC
to run another program. The main program, called the root segment, must
carry out the following steps to load and execute an overlay:

1.    Make a memory block available to receive the overlay.

2.    Set up the overlay parameter block to be passed to the EXEC
function.

3.    Call the EXEC function to load the overlay.

4.    Execute the code within the overlay by transferring to it with a far
call.

The overlay itself can be constructed as either a memory image (COM) or a
relocatable (EXE) file and need not be the same type as the root program. In
either case, the overlay should be designed so that the entry point (or a
pointer to the entry point) is at the beginning of the module after it is
loaded. This allows the root and overlay modules to be maintained
separately and avoids a need for the root to have a "magical"
knowledge of addresses within the overlay.

To prevent users from inadvertently running an overlay directly from
the command line, overlay files should be assigned an extension other than
COM or EXE. The most convenient method relates overlays to their root
segment by assigning them the same filename but an extension like OVL or
OV1, OV2, and so on.

Making Memory Available

If EXEC is to load a child program successfully, the parent must release
memory. In contrast, EXEC loads an overlay into memory that belongs to the
calling program. If the root segment is a COM program and has not
explicitly released extra memory, the root segment program need only
ensure that the system contains enough memory to load the overlay and that
the overlay load address does not conflict with its own code, data, or stack
areas.

If the root segment program was loaded from an EXE file, no straightforward
way exists for it to determine unequivocally how much memory it already
owns. The simplest course is for the program to release all extra memory, as
discussed earlier in the section on loading a child program, and then use
the MS-DOS memory allocation function (Interrupt 21H Function 48H) to
obtain a new block of memory that is large enough to hold the overlay.

Preparing Overlay Parameters

When it is used to load an overlay, the EXEC function requires two major
parameters:

■    the address of the pathname for the overlay file

■    the address of an overlay parameter block

As for a child program, the pathname for the overlay file must be an
unambiguous ASCIIZ file specification (again, no wildcard characters), and
it must include an explicit extension. As before, if a path and/or disk
drive are not included in the pathname, the current directory and
default disk drive are used.

The overlay parameter block contains the segment address at which the
overlay should be loaded and a fixup value to be applied to any relocatable
items within the overlay file. If the overlay file is in EXE format, the
fixup value is typically the same as the load address; if the overlay is in
memory-image (COM) format, the fixup value should be zero. The EXEC
function does not attempt to validate the load address or the fixup value
or to ensure that the load address actually belongs to the calling program.

Loading and Executing the Overlay

After the root segment program has prepared the filename of the overlay
file and the overlay parameter block, it can invoke the EXEC function to
load the overlay by issuing an Interrupt 21H with the registers set as
follows:

AH    =    4BH

AL    =    03H (EXEC subfunction to load overlay)

DS:DX    =    segment:offset of overlay file pathname

ES:BX    =    segment:offset of overlay parameter block

Upon return from Interrupt 21H, the root segment must test the carry flag to
determine whether the overlay was loaded. If the carry flag is clear, the
overlay file was located and brought into memory at the requested address.
The overlay can then be entered by a far call, and should exit back to the
root segment with a far return.

If the carry flag is set, the overlay file was not found or some other
(probably severe) system problem was encountered, and the AX register
contains an error code. With MS-DOS versions 3.0 and later, Interrupt 21H
Function 59H can be used to get more information about the EXEC failure. An
invalid load address supplied in the overlay parameter block does not
(usually) cause the EXEC function itself to fail but may result in the
disconcerting message Memory Allocation Error, System Halted when the root
program terminates.

An Overlay Example

The source programs ROOT.ASM (see Figure 5) and OVERLAY.ASM (see Figure 6)
demonstrate the use of EXEC to load a program overlay. The program ROOT.EXE
is executable from the MS-DOS prompt; it represents the root segment of an
application. OVERLAY is constructed as an EXE file (although it is named
OVERLAY.OVL because it cannot be run alone) and represents a subprogram
that can be loaded by the root segment when and if it is needed.

ROOT.ASM can be assembled and linked into the executable program
ROOT.EXE with the following commands:

C>MASM ROOT;  <Enter>
C>LINK ROOT;  <Enter>

OVERLAY.ASM can be assembled and linked into the file OVERLAY.OVL by
typing:

C>MASM OVERLAY;  <Enter>
C>LINK OVERLAY,OVERLAY.OVL;  <Enter>
The Microsoft Object Linker will display the message

Warning:  no stack segment

but this message can be ignored.

When ROOT.EXE is executed with the command

C>ROOT  <Enter>

it first shrinks its main memory block with a call to Interrupt 21H
Function 4AH and then allocates a separate block for the overlay with
Interrupt 21H Function 48H. Next, ROOT calls the EXEC function to load the
file OVERLAY.OVL into the newly allocated memory block. If the EXEC function
fails, ROOT displays an error message and terminates with Interrupt 21H
Function 4CH, passing a nonzero return code to COMMAND.COM to indicate an
error. If the EXEC function succeeds, ROOT saves the contents of its DS
segment register and then enters the overlay through an indirect far call.

The overlay resets the DS segment register to point to its own data segment,
displays a message using Interrupt 21H Function 40H, and then returns. Note
that the main procedure of the overlay is declared with the far attribute in
order to force the assembler to generate the opcode for a far return.

When ROOT regains control, it restores the DS segment register to point to
its own data segment again and displays an additional message, also using
Interrupt 21H Function 40H, to indicate that the overlay executed
successfully. ROOT then terminates using Interrupt 21H Function 4CH,
passing a return code of zero to indicate success, and control returns to
COMMAND.COM.

Figure 3

name    parent
    title    'PARENT --- demonstrate EXEC call'
;
; PARENT.EXE --- demonstration of EXEC to run process
;
; Uses MS-DOS EXEC (Int 21H, Function 4BH Subfunction 00H)
; to load and execute a child process named CHILD.EXE,
; then displays CHILD's return code.
;
; Ray Duncan, June 1987
;

stdin    equ    0    ; standard input
stdout    equ    1    ; standard output
stderr    equ    2    ; standard error

stksize equ    128    ; size of stack

cr    equ    0dh    ; ASCII carriage return
lf    equ    0ah    ; ASCII linefeed


DGROUP    group    _DATA,_ENVIR,_STACK


_TEXT    segment byte public 'CODE'    ; executable code segment

    assume  cs:_TEXT,ds:_DATA,ss:_STACK


stk_seg dw    ?    ; original SS contents
stk_ptr dw    ?    ; original SP contents


main    proc    far    ; entry point from MS-DOS

    mov    ax,_DATA    ; set DS = our data segment
    mov    ds,ax

            ; now give back extra memory
            ; so child has somewhere to
            ; run...
    mov    ax,es    ; let AX = segment of PSP base
    mov    bx,ss    ; and BX = segment of stack base
    sub    bx,ax    ; reserve seg stack - seg psp
    add    bx,stksize/16    ; plus paragraphs of stack
    mov    ah,4ah    ; fxn 4ah = modify memory
            ; block
    int    21h
    jc    main1
            ; display parent message ...
    mov    dx,offset DGROUP:msg1    ; DS:DX = address of message
    mov    cx,msg1_len    ; CX = length of message
    call    pmsg

    push    ds    ; save parent's data segment
    mov    stk_seg,ss    ; save parent's stack pointer
    mov    stk_ptr,sp

            ; now EXEC the child process...
    mov    ax,ds    ; set ES = DS
    mov    es,ax
    mov    dx,offset DGROUP:cname    ; DS:DX = child pathname
    mov    bx,offset DGROUP:pars    ; EX:BX = parameter block
    mov    ax,4b00h    ; function 4BH, subfunction 00H
    int    21h    ; transfer to MS-DOS

    cli        ; (for bug in some early 8088s)
    mov    ss,stk_seg    ; restore parent's stack
            ; pointer
    mov    sp,stk_ptr
    sti        ; (for bug in some early 8088s)
    pop    ds    ; restore DS = our data segment

    jc    main2    ; jump if EXEC failed

            ; otherwise EXEC succeeded,
            ; convert and display child's
            ; termination and return
            ; codes...
    mov    ah,4dh    ; fxn 4dh = get return code
    int    21h    ; transfer to MS-DOS
    xchg    al,ah    ; convert termination code
    mov    bx,offset DGROUP:msg4a
    call    b2hex
    mov    al,ah    ; get back return code
    mov    bx,offset DGROUP:msg4b    ; and convert it
    call    b2hex
    mov    dx,offset DGROUP:msg4    ; DS:DX = address of message
    mov    cx,msg4_len    ; CX = length of message
    call    pmsg    ; display it

    mov    ax,4c00h    ; no error, terminate program
    int    21h    ; with return code = 0

main1:    mov    bx,offset DGROUP:msg2a    ; convert error code
    call    b2hex
    mov    dx,offset DGROUP:msg2    ; display message 'Memory
    mov    cx,msg2_len    ; resize failed...'
    call    pmsg
    jmp    main3

main2:    mov    bx,offset DGROUP:msg3a    ; convert error code
    call    b2hex
    mov    dx,offset DGROUP:msg3    ; display message 'EXEC
    mov    cx,msg3_len    ; call failed...'
    call    pmsg

main3:    mov    ax,4c01h    ; error, terminate program
    int    21h    ; with return code = 1

main    endp        ; end of main procedure


b2hex    proc    near    ; convert byte to hex ASCII
            ; call with AL=binary value
            ;    BX=addr to store string
    push    ax
    shr    al,1
    shr    al,1
    shr    al,1
    shr    al,1
    call    ascii    ; become first ASCII character
    mov    [bx],al    ; store it
    pop    ax
    and    al,0fh    ; isolate lower 4 bits, which
    call    ascii    ; become the second ASCII
            ; character
    mov    [bx+1],al    ; store it
    ret
b2hex    endp


ascii    proc    near    ; convert value 00-0FH in AL
    add    al,'0'    ; into a "hex ASCII" character
    cmp    al,'9'
    jle    ascii2    ; jump if in range 00-09H,
    add    al,'A'-'9'-1    ; offset it to range 0A-0FH,

ascii2:    ret        ; return ASCII char. in AL.
ascii   endp


pmsg    proc    near    ; displays message on
            ; standard output
            ; call with DS:DX = address,
            ;               CX = length

    mov    bx,stdout    ; BX = standard output handle
    mov    ah,40h    ; function 40h = write
            ; file/device
    int    21h    ; transfer to MS-DOS
    ret        ; back to caller

pmsg    endp

_TEXT    ends


_DATA    segment para public 'DATA'    ; static & variable data segment

cname    db    'CHILD.EXE',0    ; pathname of child process

pars    dw    _ENVIR    ; segment of environment block
    dd    tail    ; long address, command tail
    dd    fcb1    ; long address, default FCB #1
    dd    fcb2    ; long address, default FCB #2

tail    db    fcb1-tail-2    ; command tail for child
    db    'dummy command tail',cr

fcb1    db    0    ; copied into default FCB #1 in
    db    11 dup (' ')    ; child's program segment prefix
    db    25 dup (0)

fcb2    db    0    ; copied into default FCB #2 in
    db    11 dup (' ')    ; child's program segment prefix
    db    25 dup (0)

msg1    db    cr,lf,'Parent executing!',cr,lf
msg1_len equ     $-msg1

msg2    db    cr,lf,'Memory resize failed, error code='
msg2a    db    'xxh.',cr,lf
msg2_len equ     $-msg2

msg3    db    cr,lf,'EXEC call failed, error code='
msg3a    db    'xxh.',cr,lf
msg3_len equ     $-msg3

msg4    db    cr,lf,'Parent regained control!'
    db    cr,lf,'Child termination type='
msg4a     db    'xxh, return code='
msg4b    db    'xxh.',cr,lf
msg4_len equ     $-msg4

_DATA    ends


_ENVIR  segment para public 'DATA'    ; example environment block
            ; to be passed to child

    db    'PATH=',0    ; basic PATH, PROMPT,
    db    'PROMPT=$p$_$n$g',0    ; and COMSPEC strings
    db    'COMSPEC=C:\COMMAND.COM',0
    db    0    ; extra null terminates block

_ENVIR    ends


_STACK    segment para stack 'STACK'


    db    stksize dup (?)

_STACK    ends


    end    main    ; defines program entry point

Figure 4

name    child
    title    'CHILD process'
;
; CHILD.EXE --- a simple process loaded by PARENT.EXE
; to demonstrate the MS-DOS EXEC call, subfunction 00H.
;
; Ray Duncan, June 1987
;

stdin    equ    0    ; standard input
stdout    equ    1    ; standard outpu
stderr    equ    2    ; standard error

cr    equ    0dh    ; ASCII carriage return
lf    equ    0ah    ; ASCII linefeed


DGROUP    group    _DATA,STACK


_TEXT     segment byte public 'CODE'    ; executable code segment

    assume  cs:_TEXT,ds:_DATA,ss:STACK

main    proc    far    ; entry point from MS-DOS

    mov    ax,_DATA    ; set DS = our data segment
    mov    ds,ax

            ; display child message ...
    mov    dx,offset msg    ; DS:DX = address of message
    mov    cx,msg_len    ; CX = length of message
    mov    bx,stdout    ; BX = standard output handle
    mov    ah,40h    ; AH = fxn 40h, write
            ; file/device
    int    21h    ; transfer to MS-DOS
    jc    main2    ; jump if any error

    mov    ax,4c00h    ; no error, terminate child
    int    21h    ; with return code = 0

main2:    mov    ax,4c01h    ; error, terminate child
    int    21h    ; with return code = 1

main    endp        ; end of main procedure

_TEXT    ends


_DATA    segment para public 'DATA'    ; static & variable data
            ; segment

msg    db    cr,lf,'Child executing!',cr,lf
msg_len equ $-msg

_DATA    ends


STACK    segment para stack 'STACK'

    dw    64 dup (?)

STACK    ends


    end    main    ; defines program entry point

Figure 5

name    root
    title    'ROOT --- demonstrate EXEC overlay'
;
; ROOT.EXE --- demonstration of EXEC for overlays
;
; Uses MS-DOS EXEC (Int 21H, Function 4BH Subfunction 03H)
; to load an overlay named OVERLAY.OVL, calls a routine
; within the OVERLAY, then recovers control and terminates.
;
; Ray Duncan, June 1987
;

stdin    equ    0    ; standard input
stdout    equ    1    ; standard output
stderr    equ    2    ; standard error

stksize equ    128    ; size of stack

cr    equ    0dh    ; ASCII carriage return
lf    equ    0ah    ; ASCII linefeed


DGROUP    group   _DATA,_STACK


_TEXT    segment byte public 'CODE'    ; executable code segment

    assume  cs:_TEXT,ds:_DATA,ss:_STACK


stk_seg dw    ?    ; original SS contents
stk_ptr dw    ?    ; original SP contents


main    proc    far    ; entry point from MS-DOS

    mov    ax,_DATA    ; set DS = our data segment
    mov         ds,ax

            ; now give back extra memory
    mov    ax,es    ; AX = segment of PSP base
    mov    bx,ss    ; BX = segment of stack base
    sub    bx,ax    ; reserve seg stack - seg psp
    add    bx,stksize/16    ; plus paragraphs of stack
    mov    ah,4ah    ; fxn 4ah = modify memory
            ; block
    int    21h    ; transfer to MS-DOS
    jc    main1    ; jump if resize failed

            ; display message 'Root
            ; 'segment executing...'
    mov    dx,offset DGROUP:msg1    ; DS:DX = address of message
    mov    cx,msg1_len    ; CX = length of message
    call    pmsg

            ; allocate memory for overlay
    mov    bx,1000h    ; get 64 KB (4096 paragraphs)
    mov    ah,48h    ; fxn 48h, allocate mem block
    int    21h    ; transfer to MS-DOS
    jc    main2    ; jump if allocation failed

    mov    pars,ax    ; set load address for
            ; overlay
    mov    pars+2,ax    ; set relocation segment for
            ; overlay
    mov    word ptr entry+2,ax    ; set segment of entry point

    push    ds    ; save root's data segment
    mov    stk_seg,ss    ; save root's stack pointer
    mov    stk_ptr,sp

            ; now use EXEC to load
            ; overlay
    mov    ax,ds    ; set ES = DS
    mov    es,ax
    mov    dx,offset DGROUP:oname    ; DS:DX = overlay pathname
    mov    bx,offset DGROUP:pars    ; EX:BX = parameter block
    mov    ax,4b03h    ; function 4BH, subfunction
            ; 03H
    int    21h    ; transfer to MS-DOS

    cli        ; (for bug in some early
            ; 8088s)
    mov    ss,stk_seg    ; restore root's stack
            ; pointer
    mov    sp,stk_ptr
    sti        ; (for bug in some early 8088s)
    pop    ds    ; restore DS = our data
            ; segment

    jc    main3    ; jump if EXEC failed

            ; otherwise EXEC succeeded...
    push    ds    ; save our data segment
    call    dword ptr entry    ; now call the overlay
    pop    ds    ; restore our data segment

            ; display message that root
            ; segment regained control...
    mov    dx,offset DGROUP:msg5    ; DS:DX = address of message
    mov    cx,msg5_len    ; CX = length of message
    call    pmsg    ; display it

    mov    ax,4c00h    ; no error, terminate program
    int    21h    ; with return code = 0

main1:    mov    bx,offset DGROUP:msg2a    ; convert error code
    call    b2hex
    mov    dx,offset DGROUP:msg2    ; display message 'Memory
    mov    cx,msg2_len    ; resize failed...'
    call    pmsg
    jmp    main4

main2:    ov    bx,offset DGROUP:msg3a    ; convert error code
    call    b2hex
    mov    dx,offset DGROUP:msg3    ; display message 'Memory
    mov    cx,msg3_len    ; allocation failed...'
    call    pmsg
    jmp    main4

main3:    mov    bx,offset DGROUP:msg4a    ; convert error code
    call    b2hex
    mov    dx,offset DGROUP:msg4    ; display message 'EXEC
    mov    cx,msg4_len    ; call failed...'
    call    pmsg

main4:    mov    ax,4c01h    ; error, terminate program
    int    21h    ; with return code = 1

main    endp        ; end of main procedure


b2hex    proc    near    ; convert byte to hex ASCII
            ; call with AL = binary value
            ; BX = addr to store string
    push    ax
    shr    al,1
    shr    al,1
    shr    al,1
    shr    al,1
    call    ascii    ; become first ASCII
            ; character
    mov    [bx],al    ; store it
    pop    ax
    and    al,0fh    ; isolate lower 4 bits, which
    call    ascii    ; become the second ASCII
            ; character
    mov    [bx+1],al    ; store it
    ret
b2hex    endp


ascii    proc    near    ; convert value 00-0FH in AL
    add    al,'0'    ; into a "hex ASCII" character
    cmp    al,'9'
    jle    ascii2    ; jump if in range 00-09H,
    add    al,'A'-'9'-1    ; offset it to range 0A-0FH,
ascii2:    ret         ; return ASCII char. in AL.
ascii    endp


pmsg    proc    near    ; displays message on
            ; standard output
            ; call with DS:DX = address,
            ;              CX = length

    mov    bx,stdout    ; BX = standard output handle
    mov    ah,40h    ; function 40H = write
            ; file/device
    int    21h    ; transfer to MS-DOS
    ret        ; back to caller

pmsg    ndp

_TEXT    ends


_DATA    segment para public 'DATA'    ; static & variable data
            ; segment

oname    db    'OVERLAY.OVL',0    ; pathname of overlay file

pars    dw    0    ; load address (segment) for
            ; file
    dw    0    ; relocation (segment) for
            ; file

entry    dd    0    ; entry point for overlay

msg1    db    cr,lf,'Root segment executing!',cr,lf
msg1_len equ     $-msg1

msg2    db    cr,lf,'Memory resize failed, error code='
msg2a    db    'xxh.',cr,lf
msg2_len equ     $-msg2

msg3    db    cr,lf,'Memory allocation failed, error code='
msg3a    db    'xxh.',cr,lf
msg3_len equ     $-msg3

msg4    db    cr,lf,'EXEC call failed, error code='
msg4a    db    'xxh.',cr,lf
msg4_len equ     $-msg4

msg5    db    cr,lf,'Root segment regained control!',cr,lf
msg5_len equ     $-msg5

_DATA    ends


_STACK    segment para stack 'STACK'

    db    stksize dup (?)

_STACK  ends


    end    main    ; defines program entry point

Figure 6

name    overlay
    title    'OVERLAY segment'
;
; OVERLAY.OVL --- a simple overlay segment
; loaded by ROOT.EXE to demonstrate use of
; the MS-DOS EXEC call subfunction 03H.
;
; The overlay does not contain a STACK segment
; because it uses the ROOT segment's stack.
;
; Ray Duncan, June 1987
;

stdin    equ    0    ; standard input
stdout    equ    1    ; standard output
stderr    equ    2    ; standard error

cr    equ    0dh    ; ASCII carriage return
lf    equ    0ah    ; ASCII linefeed


_TEXT    segment byte public 'CODE'    ; executable code segment

    assume  cs:_TEXT,ds:_DATA

ovlay    proc    far    ; entry point from root segment

    mov    ax,_DATA    ; set DS = local data segment
    mov    ds,ax

            ; display overlay message ...
    mov    dx,offset msg    ; DS:DX = address of message
    mov    cx,msg_len    ; CX = length of message
    mov    bx,stdout    ; BX = standard output handle
    mov    ah,40h    ; AH = fxn 40h, write file/device
    int    21h    ; transfer to MS-DOS

    ret        ; return to root segment

ovlay    endp        ; end of ovlay procedure

_TEXT    ends


_DATA    segment para public 'DATA'    ; static & variable data segment

msg    db    cr,lf,'Overlay executing!',cr,lf
msg_len equ     $-msg

_DATA    ends

    end





Customizing a Microsoft Windows Dialog Box with New Control Classes

 Gregg L. Spaulding

Although the power inherent in Microsoft(R) Windows Presentation Manager is
seen by anyone who becomes acquainted with its programming environment,
some tools are just not provided with the interface. For one thing, although
one of the strengths of Windows is its controls--child windows
belonging to a predefined class and having predefined behaviors, which can
be used by any application or, more frequently, by a dialog box--this
feature does not handle all situations effectively.

Windows has five predefined control types: list box, button, static, edit,
and scrollbar. Each of these has a window procedure associated with it,
which is an integral part of Windows and takes a great deal of burden off
the programmer. But suppose, for example, that we have a list of items and
want a user to pick one. Typically, this would be the job of the list box,
which displays a list of items with a scroll bar on the right-hand side and
highlights the current selection. The list box is effective, especially
for a long list. If, however, a list has only three or four items, the list
box is cumbersome and takes up a lot of space on the screen. An alternative
would be to represent the choices as a group of radio buttons. But that
also uses a great deal of screen space and would be difficult to do if the
number of items and the items themselves were not known in advance.

What is needed therefore is a custom control. MSJ readers will remember that
an in-depth analysis on designing custom controls was presented by Kevin
Welch in "Creating User-Defined Controls for Your Own Windows Applications,"
MSJ (Vol. 3, No. 4). In this article we will focus on developing and
implementing a new and practical custom control class to fill the special
need defined above. We'll call this new control the select class. This new
class, once developed, can then be added to your toolkit of Windows tools
for use in any Windows program.

The Select Class

We begin by defining what the custom behavior of the new control, the select
class, will be so that we can write its window class procedure. For our
purposes, the control should display only the currently selected item at any
given moment and should be highlighted if it currently has the input
focus. The arrow keys or a click of the left mouse button should cause the
selection to change. A click of the right mouse button should pop up a list
of all available selections; the user should be able to select the desired
item from the list by clicking on it.

We will also need to provide an interface for the select class similar to
that of list box. This interface would consist of a message to add a new
item to the list, messages to set and get the index of the current
selection, a message to get the currently selected item, and a message to
get the count of items in the list. Additionally, the control should send a
message to its parent anytime the current selection is changed. The parent
can, of course, ignore this message if it desires. The control should also
contain a picture to indicate that it is our customized select control, not
just static text being displayed on the screen. The picture we will use is
that of two arrows chasing each other in a circle. These circular arrows
will be implemented as a 16-by 16-bitmap (see Figure 1).  Fragments from
the source code for the select control are shown in Figure 2.

Select and Windows

Since the select control has to be available to all applications, it must
reside in a dynamic-link library (DLL). The mere mention of a DLL often
strikes fear into the hearts of many Windows programmers, but it
shouldn't. There's nothing magical about a DLL. There are simply a few
extra rules to follow.

First, since SS != DS, the DLL must be compiled using the compiler switch
-Asnw for small models or -Alnw for medium models. All passed
pointers must be long for this same reason. Second, a DLL does not have an
entry point like WinMain in a Windows application. Instead, Windows
makes a call to a programmer-supplied initialization routine when the
library is first loaded into memory. This routine must be written in
assembly language although it can call to C to do most of its work. Figure 3
demonstrates how I've done that in the files LIBENTRY.ASM and LIBMAIN.C.

The LIBMAIN routine makes a call to LocalInit to initialize the local heap,
unlocks the data segment (which is left locked   by LocalInit), and calls
RegisterSelect, which will register the window class with Windows
itself. After all that is performed, the class becomes available to all
applications. In order to install the select class into Windows without any
application being aware of it, the DLL's EXE file must be placed into
the win.ini file on the load= line. This will force Windows to call its
initialization function when Windows is started.

The control will remain registered and available for the duration of
any given session in which it was loaded. The RegisterSelect function called
by LibMain looks much like any window class registration, except that the
cbWndExtra field of the class structure is set to 2*sizeof(WORD) instead of
0. When CreateWindow is called, Windows allocates a window structure
internally and places the parameters in it. Setting cbWndExtra in the
window class structure to a nonzero value instructs Windows to allocate
that many extra bytes at the end of each window structure created by
CreateWindow with this window class.

The two extra words that are requested in this case will be used by the
control class procedure to identify information unique to each instance
(or window) of the control. This will become more apparent later. The
procedure also creates a bitmap for the circular arrows, since it would be
wasteful for each iteration to create its own copy. Thus, only one is
created and shared by all controls of the select class.

Writing the Procedure

The first thing we need in order to write the window class procedure is a
structure defining the data that is unique to a particular instance of
the control. Since it is possible to define the control to be any size,
let's establish some standards. The bitmap will always appear flush
against the left edge of the control window, with the currently selected
text item appearing immediately to its right. Both items will be centered
vertically in the control.

To set this standard, the structure must have values indicating the y
starting positions for both the bitmap and the text relative to the top of
the window. To calculate the minimum bounding box around a text string,
the character width must also be retained. Since we want to be able to pop
up a list of all items, we will need to remember the height of the
characters. That way the window that is displaying them can be of
minimum size. We also will need a field to retain the handle to that
pop-up window.

The structure will need to store a rectangle defining the boundaries of the
bitmap and text, a Boolean flag indicating whether or not it has the current
input focus, two integers describing the number of items in the list and
the index of the current selection, and an array of memory handles to the
strings themselves. Notice that in the listing of select.c the array of
handles in the structure is declared to be of size 1 and is used only as a
placeholder. The size of the structure will be dynamically altered to
accommodate the actual array size at run time.

Responding to Messages

When the window procedure receives a WM_CREATE message from Windows, it
must allocate the control structure defined previously from the local
heap, initialize it, and place the memory handle into the window structure
using the SetWindowWord function. The GWW_MEMHANDLE argument, which is
defined as 0, instructs SetWindowWord to place the passed word at offset 0
from the bottom of the window structure. This is the first of the two extra
words that are allocated by the setting of cbWndExtra in RegisterSelect to
2*sizeof(WORD). The second word will be set to the current size of the data
structure since it can change as strings    are added.

The WM_DESTROY message must also be processed by the window procedure,
since we allocated memory from the local heap and must release it. Also, a
call to LocalShrink is made so as to reduce the size of the data segment in
case it was expanded by a large number of LocalAlloc calls. Data segments
are moveable but not discardable so we want to keep them as small as
possible.

Since the control is likely to be invoked by a dialog box, it should have
the standard dialog box interface (tab, back tab). The dialog manager takes
care of this automatically by capturing these keys, but note that it
captures the arrow keys as well. Because the procedure needs the arrow
messages in order to function properly, this is a problem. Fortunately, the
dia-log box manager sends a WM_GETDLGCODE message to all controls when it
creates them. By responding to it with DLGC_WANTARROWS, we inform the dialog
manager that it should pass WM_KEYUP and WM_KEYDOWN messages for the arrow
keys directly to the procedure without acting upon them itself.

Since we want the control to highlight the current selection when it has the
input focus, we will have to capture the WM_SETFOCUS and the WM_KILLFOCUS
messages. Rather than actually performing the highlighting in response to
these messages, let's defer that task to the paint routine. When focus
messages are received, all that is necessary is to set or reset the bFocus
flag in the structure and call InvalidateRect to force a repaint. This
prevents duplication of the paint code.

Next, we need to think about how the selection can be changed. It must
change in response to an arrow keystroke, to mouse clicks, and to the
SL_SETCURSEL message, which is defined in select.h (refer back to Figure 2).
Rather than duplicating the code to change selection, it is easier to write
a general purpose routine, ChangeSelection, which will perform the action.
This function must accept the window handle of the control, a Boolean flag
indicating whether the change is to be made to an absolute selection or
relative to the current selection, and an integer indicating the change to
make.

The routine only needs to modify the index value in the control's structure
to reflect the new selection and to call InvalidateRect to force a repaint
of the control. Since we decided that the control should inform its parent
of any change to the selection, this would be the best place to do it. We
must therefore call PostMessage with the message value equal to WM_COMMAND
and the high word of lParam equal to SLN_SELCHANGE. This conforms to the
same standard used by list boxes. With that done, the processing of
WM_KEYUP, WM_LBUTTONUP, and SL_SETCURSEL messages becomes almost trivial.
They need only make calls to ChangeSelection. It is also worth remembering
that the WM_LBUTTONDOWN and WM_RBUTTONDOWN commands should cause the
control to gain the input focus so these are captured and a simple call to
SetFocus is made.

Processing the message  WM_RBUTTONUP is a little more complicated. This is
the case in which we want to pop up a full list of the available selections
for the user. This pop-up window should appear directly below the select
control and be the same width. Conveniently, the control structure
contains the size of the control in the frect field. This will give us the
window's width. The control structure also contains the number of items in
the list and the characters' height, from which we can easily calculate the
height of the window.

All of the values, however, are in client coordinates. Since we want the
pop-up window to be able to overlap the edges of the dialog box in which it
appears, it must have the WS_POPUP rather than WS_CHILD style. This implies
that the x and y coordinates that are sent to CreateWindow must be in screen
rather than client coordinates. This problem is easily solved by calling
the Windows routine ClientToScreen to map our calculated coordinates to the
screen.

Since the functionality desired of the pop-up window is basically that of a
list box, it makes sense to use one. We will create a list box with styles
WS_POPUP, WS_DLGFRAME, and LBS_NOTIFY. We don't need the WS_VSCROLL type
since the box will be exactly large enough to hold all the choices. Once the
window is created, it must be filled in the standard fashion by sending it
LB_ADDSTRING messages. And its current selection should be set to the same
as the select control. To prevent any possible confusion, we must also
disable the parent window at this time. Since we have given the list box the
LBS_NOTIFY style, it will send us a WM_COMMAND message when the selection is
changed. When this is received, we need only update the selection in the
select control, destroy the list box, and reenable the parent window.

In response to a WM_PAINT message, the select control must, as with any
window procedure, paint its window. Since the locations for the bitmap
and text were saved in the control structure during the processing of the
WM_CREATE message, this task will involve a simple call to BitBlt to draw
the bitmap and a call to TextOut to write the currently selected text.
Additionally, if the control has the input focus as defined by the
bFocus flag in the control structure, a call to InvertRect is made to
highlight the selection. What could be simpler?

To complete our window class procedure, we must process the additional
user-defined messages that can be sent by an application or a dialog box
procedure. The SL_ADDSTRING message must add the string pointed to by
lParam to the current list of strings associated with the receiving
control. This will involve allocating local memory for the string and
copying it, then placing the handle to that local memory into the array of
handles stored in the control's structure.

The structure, however, is only large enough to hold the number of handles
that are currently defined in the array. For this reason, we must use
LocalReAlloc to expand the size of the structure to add the new handle.
Since this function requires the desired new size (not the increment or
decrement) as an argument, we must retrieve the current size from the
Window structure using GetWindowWord with the GWW_MEMSIZE argument defined
at the top of select.c.

Once the control structure has been expanded and the new handle added, the
count field must be incremented. The SL_GETCURSEL and SL_GETCOUNT messages
return the value found in the index and count fields of the control
structure, respectively. Note that a value returned by a window function
is returned as the value of SendMessage or SendDlgItemMessage.
Processing the SL_GETTEXT message is relatively straightforward. The
string pointed to by handle[wParam] in the control structure is copied to
the location pointed to by lParam.

In some cases the lParam value is used to represent a long pointer to a
string. Therefore, in order to prevent a caller's data segment from being
moved before the message is processed and the pointer made invalid,  the
messages should be sent using either SendMessage or SendDlgItemMessage
rather than PostMessage. The former two calls cause Windows to call the
window class procedure directly as a function, whereas the latter places the
message in a queue and eventually will call the control function via
DispatchMessage after allowing the memory manager to move things around.

Memory Considerations

A few words should be said about select class's make file and def file. I
compiled this as a medium model (although it would also work as a small
model) to minimize the memory it uses at any given instant. Since the
initialization code in LIBMAIN.C is used only when Windows is started up,
it is in a separate discardable segment named _INIT. Then, after
initialization is completed, only the _SELECT segment with the actual
window class procedure needs to be in memory--and its size is less than
2Kb.

Note that in the def file, HEAPSIZE is specified as 200 bytes. Certainly
this will not be enough, but Windows will be more than happy to relocate the
entire data segment in global memory and expand the heap as necessary.
Unfortunately, it will never shrink it back down again. This is why (as
mentioned previously), in response to the WM_DESTROY message, the control
procedure attempts to shrink the segment back to its initial size manually
using the LocalShrink function. Windows will never shrink the heap smaller
than the currently allocated amount so this process is always safe.

DEMO Program

Figure 4 lists the source code for a very simple Windows program, DEMO,
that uses the select control, and Figures 5, 6, and 7  show DEMO making use
of the select control. To run DEMO, edit the win.ini file to include
select.exe on the "LOAD=" line. Next, run demo.exe from Windows. Finally,
clicking on the dialog menu in DEMO brings up the new select control.

As DEMO and the newly developed select class show, writing customized
controls is not as difficult as it is often perceived to be. In fact, once
there is a clearly defined need and a clearly defined set of design goals
and parameters, all that is left is the implementation. As shown here,
that's not all that difficult. All it takes is a little patience and a
willingness to explore the environment and take advantage of the powerful
features provided by Windows.

Figure 2

SELECT.C - Source File

                    ■
                    ■
                    ■
typedef struct {
    int yBitStart;        /* y location to start bitmap at */
    int yCharStart;        /* y location to start text at */
    int xCharWidth;        /* width of a character */
    int yCharHeight;    /* height of character */
    RECT frect;        /* formatting rectangle for text */
    BOOL bFocus;        /* this control has the input focus
    HWND hWndList;
    int index, count;    /* which item is selected and how
                    * many items total */
    HANDLE handle[1];    /* expandable array of handles to
                    * strings */
} HSELECT;
typedef HSELECT NEAR * NPHSELECT;
                    ■
                    ■
                    ■
long FAR PASCAL SelectWndProc(HWND hWnd, unsigned message,
                WORD wParam, LONG lParam)
{
    HDC hDC, hMemDC;    /* display contexts for drawing */
    PAINTSTRUCT ps;        /* for BeginPaint and EndPaint */
    TEXTMETRIC tm;        /* need info about character
                    * sizes */
    RECT rect;            /* generic */
    HANDLE hSelect, hString;    /* memory handles */
    NPHSELECT pSelect;    /* pointer to structure header */
    NPSTR pString;        /* pointer to a string */
    POINT pt1,pt2;
    int i;            /* generic */

switch (message)
    {
        case WM_CREATE:

    /* calculate size of initial node not including any */
    /* handles to strings, allocate memory and lock it */

        i = sizeof(HSELECT)-sizeof(HANDLE);
        if ((hSelect=LocalAlloc( LMEM_MOVEABLE,
                    i ) ) == NULL) {
            MessageBox(hWnd, szMemErr, NULL,
                MB_ICONEXCLAMATION);
            break;
        }
        pSelect = (NPHSELECT)LocalLock( hSelect );

                    ■
                    ■
                    ■
        /* fill in the structure */

        GetClientRect ( hWnd, &rect );
        pSelect->yCharStart = (rect.bottom - i) / 2;
        pSelect->xCharWidth = tm.tmAveCharWidth;
        pSelect->yBitStart = (rect.bottom - YBITMAP)/2;
        rect.left = XBITMAP + 4;
        rect.top = pSelect->yCharStart - 2;
        rect.bottom = rect.top + i + 4;
        pSelect->frect = rect;
        pSelect->count = 0;
        pSelect->index = -1;
        pSelect->hWndList = pSelect->bFocus = FALSE;

                    ■
                    ■
                    ■
case WM_DESTROY:

/* all we have to do is free up all associated memory */

                    ■
                    ■
                    ■
case WM_PAINT:

    hDC = BeginPaint ( hWnd, &ps );

/* lock down structure associated with this control */

    hSelect = GetWindowWord ( hWnd, GWW_MEMHANDLE );
    pSelect = (NPHSELECT)LocalLock ( hSelect );

/* draw the bitmap'd circular arrows */

    hMemDC = CreateCompatibleDC ( hDC );
    SelectObject ( hMemDC, hBitmap );
    BitBlt ( hDC, 2, pSelect->yBitStart,
        XBITMAP, YBITMAP, hMemDC, 0, 0, SRCCOPY );
    DeleteObject ( hMemDC );

    /* draw the current string selection */

    rect = pSelect->frect;
    if (pSelect->index > -1) {
        hString = pSelect->handle[pSelect->index];
        pString = LocalLock ( hString );
        i = lstrlen(pString);
        TextOut ( hDC, rect.left+2,
            pSelect->yCharStart,
            pString, i );
        LocalUnlock( hString );

                    ■
                    ■
                    ■
case WM_RBUTTONUP:

    /* pop up list box with all choices */

    hSelect = GetWindowWord ( hWnd, GWW_MEMHANDLE );
    pSelect = (NPHSELECT)LocalLock ( hSelect );
    GetClientRect( hWnd,&rect );
/* calculate corners of box in screen coordinates */
    pt1.x = pSelect->frect.left;
    pt1.y = rect.bottom;
    pt2.x = rect.right;
    pt2.y = pt1.y +
        pSelect->yCharHeight * pSelect->count;
    ClientToScreen(hWnd,&pt1);
    ClientToScreen(hWnd,&pt2);
    pt2.x += 2*GetSystemMetrics( SM_CXDLGFRAME );
    pt2.y += 2*GetSystemMetrics( SM_CYDLGFRAME );
    pSelect->hWndList = CreateWindow (
        "listbox", "", WS_POPUP | WS_DLGFRAME |
        LBS_NOTIFY, pt1.x, pt1.y, pt2.x - pt1.x,
        pt2.y - pt1.y, hWnd, NULL, hInstLib, NULL );

/* WINDOWS has set parent to be the dialog manager; */
/* we must make this call to regain our child */

    SetWindowWord ( pSelect->hWndList,
            GWW_HWNDPARENT, hWnd);

                        ■
    ■
    ■
case WM_COMMAND:
/* listbox informing us that something has happened */
    hSelect = GetWindowWord(hWnd, GWW_MEMHANDLE );
    pSelect = (NPHSELECT)LocalLock ( hSelect );
    PostMessage ( hWnd, SL_SETCURSEL,
        SendMessage ( pSelect->hWndList,
        LB_GETCURSEL, 0, 0L), 0L );
    if (HIWORD(lParam) == LBN_SELCHANGE) {
        DestroyWindow(pSelect->hWndList);
        pSelect->hWndList = NULL;
        EnableWindow (GetParent(hWnd), TRUE);
        SetFocus(hWnd);
    }
    LocalUnlock(hSelect);
    break;
    ■
    ■
    ■
static void near ChangeSelection (HWND hWnd, BOOL bRel,
                    int sel)
{
    HANDLE hSelect;
    NPHSELECT pSelect;

    /* get the handle to the structure and lock it down */
    hSelect = GetWindowWord ( hWnd, GWW_MEMHANDLE );
    pSelect = (NPHSELECT)LocalLock( hSelect );

    /* if change is relative, take care of it */
    if (bRel) {
        if (sel > 0) pSelect->index++;
        else if (sel < 0) pSelect->index--;
        if (sel > 0 && pSelect->index == pSelect->count)
            pSelect->index = 0;
        else if (sel < 0 && pSelect->index < 0)
            pSelect->index = pSelect->count - 1;
    }
    /* if change is absolute, set it */
    else if(sel >= -1 && sel < pSelect->count) pSelect->index = sel;

    /* force a repaint */
    InvalidateRect ( hWnd, &(pSelect->frect), TRUE );
    LocalUnlock( hSelect );

    /* inform our parent that we are changing selection */
    PostMessage ( GetParent(hWnd), WM_COMMAND,
        GetWindowWord (hWnd, GWW_ID), MAKELONG(hWnd,
                        SLN_SELCHANGE));
}

Figure 3

Source code for LIBENTRY.ASM

    Extrn        LibMain:far
_TEXT        SEGMENT BYTE PUBLIC 'CODE'
    ASSUME CS:_TEXT
    PUBLIC LibEntry

LibEntry PROC FAR
    push di    ; hInstance
    push ds    ; data segment
    push cx        ; heap size
    push es
    push si
    call LibMain
    ret
LibEntry    ENDP
_TEXT        ENDS
End        LibEntry

Source code for LIBMAIN.C

#include <windows.h>

HANDLE hInstLib;
HANDLE hBitmap;
#define XBITMAP        16
#define YBITMAP        16

static BYTE byBits[] = {
0xFE, 0xFF, 0xFE, 0x7F, 0xF8, 0x3F, 0xE6, 0x7F,
0xDE, 0xF7, 0xBF, 0xFB, 0x7F, 0xFD, 0x7F, 0xFD,
0x7F, 0xFD, 0x7F, 0xFD, 0xBF, 0xFB, 0xDE, 0xF7,
0xFC, 0xCF, 0xF8, 0x3F, 0xFC, 0xFF, 0xFE, 0xFF
};

/* some prototypes */
BOOL NEAR RegisterSelect (HANDLE hInstance);
long FAR PASCAL SelectWndProc( HWND hWnd, unsigned message,
            WORD wParam, LONG lParam);

/*
this function is called from the assembler
initialization in libentry.asm
*/
int FAR PASCAL LibMain(HANDLE hInstance, WORD wDataSeg, WORD
wHeapSize, LPSTR lpCmdLine)
{
    int iret;
    if (wHeapSize == 0) return(0);
    iret = LocalInit(wDataSeg, NULL, (NPSTR)wHeapSize);
    UnlockData(0);
    RegisterSelect ( hInstLib = hInstance );
    return iret;
}

/*
this is called by LibMain to register the class
and initialize the bitmap
*/
static BOOL NEAR RegisterSelect (HANDLE hInstance)
{
    WNDCLASS wndclass;

    wndclass.style        = CS_HREDRAW | CS_VREDRAW;
    wndclass.lpfnWndProc    = SelectWndProc;
    wndclass.cbClsExtra    = 0;
    wndclass.cbWndExtra    = 2*sizeof(WORD); /* space for
* control info */
    wndclass.hIcon        = NULL;
    wndclass.hInstance    = hInstance;
    wndclass.hCursor    = LoadCursor(NULL, IDC_ARROW);
    wndclass.hbrBackground    = COLOR_WINDOW+1;
    wndclass.lpszMenuName    = NULL;
    wndclass.lpszClassName    = "select";

    hBitmap=CreateBitmap ( XBITMAP, YBITMAP, 1, 1, byBits );
    return RegisterClass(&wndclass);
}

Figure 4

DEMO - Make file

demo.res: demo.dlg demo.rc demo.h
    rc -r demo.rc

demo.obj: demo.c demo.h
    cl -c -AS -Gsw -G2 -Ownlt -Zpe -W2 demo.c

demo.exe: demo.obj demo.res demo.def
    link4 demo, demo.exe/NOD, demo.map/map, slibw slibcew, demo.def
    rc demo.res

DEMO.DLG - Dialog file

Note:     The highlighted lines below must be entered as
        part of each preceding line - these lines
        cannot be broken in the actual file

TEST DIALOG LOADONCALL MOVEABLE DISCARDABLE 26, 54, 132, 65
STYLE WS_DLGFRAME | WS_VISIBLE | WS_POPUP
BEGIN
CONTROL "Ok", IDD_OK, "button", BS_DEFPUSHBUTTON | WS_TABSTOP |
                WS_CHILD, 47, 42, 37, 14
CONTROL "", IDD_COLORS, "select", WS_TABSTOP |
                WS_CHILD, 68, 6, 40, 14
CONTROL "", IDD_STRINGS, "select", WS_TABSTOP |
                WS_CHILD, 68, 25, 55, 14
CONTROL "Color", IDD_NULL, "static", SS_RIGHT |
                WS_CHILD, 4, 9, 61, 8
CONTROL "Company", IDD_NULL, "static", SS_RIGHT |
                WS_CHILD, 8, 27, 57, 8
END

DEMO.DEF - Definition file

NAME demo

STUB 'WINSTUB.EXE'

CODE    LOADONCALL MOVEABLE DISCARDABLE
DATA    MOVEABLE MULTIPLE

STACKSIZE    2048
HEAPSIZE    1024

EXPORTS
    DemoWndProc    @1
    DemoDlg        @2

DEMO.RC - Resource File

#include <windows.h>
#include "demo.h"

demo MENU
BEGIN
    MENUITEM "Dialog...",    IDM_TEST
END

STRINGTABLE
BEGIN
IDS_COLOR1,    "RED"
IDS_COLOR2,    "BLUE"
IDS_COLOR3,    "GREEN"
IDS_COLOR4,    "ORANGE"
IDS_COLOR5,    "YELLOW"
IDS_COLOR6,    "PURPLE"
IDS_COLOR7,    "BLACK"
IDS_COLOR8,    "WHITE"
IDS_STRING1,    "IBM"
IDS_STRING2,    "Microsoft"
IDS_STRING3,    "Tencor"
IDS_STRING4,    "Varian"
IDS_STRING5,    "Other"
END

rcinclude demo.dlg

DEMO.H - Header File

/* dialog box id's */
#define IDD_OK        1
#define IDD_NULL    -1
#define IDD_COLORS    10
#define IDD_STRINGS    11

/* menu id's */
#define IDM_TEST        100

/* string id's */
#define IDS_COLOR1    1
#define IDS_COLOR2    2
#define IDS_COLOR3    3
#define IDS_COLOR4    4
#define IDS_COLOR5    5
#define IDS_COLOR6    6
#define IDS_COLOR7    7
#define IDS_COLOR8    8
#define IDS_STRING1    9
#define IDS_STRING2    10
#define IDS_STRING3    11
#define IDS_STRING4    12
#define IDS_STRING5    13

DEMO.C - Source Code Listing

#include <windows.h>
#include "select.h"
#include <stdio.h>
#include <string.h>
#include "demo.h"

HWND hWndDemo;
HANDLE hInstDemo;

static char szClassName[] = "demo";
static int nTrueFalse = 0;
static int nString = 0;

int PASCAL WinMain ( HANDLE hInstance, HANDLE hPrevInstance,
    LPSTR lpszCmdLine, int cmdShow );
long FAR PASCAL DemoWndProc ( HWND hWnd, unsigned message,
    WORD wParam, LONG lParam );
void DemoCommand ( HWND hWnd, WORD wCmd );
BOOL FAR PASCAL DemoDlg ( HWND hWndDlg, unsigned message,
    WORD wParam, LONG lParam );



int PASCAL WinMain ( HANDLE hInstance, HANDLE hPrevInstance,
    LPSTR lpszCmdLine, int cmdShow )
{
    MSG msg;
    WNDCLASS Class;

    if (hPrevInstance) return TRUE;
    Class.hCursor    = LoadCursor(NULL,IDC_ARROW);
    Class.hIcon        = NULL;
    Class.cbClsExtra    = 0;
    Class.cbWndExtra    = 0;
    Class.lpszMenuName    = szClassName;
    Class.lpszClassName    = szClassName;
    Class.hbrBackground    = (HBRUSH)GetStockObject(WHITE_BRUSH);
    Class.hInstance    = hInstance;
    Class.style        = CS_VREDRAW | CS_HREDRAW;
    Class.lpfnWndProc    = DemoWndProc;
if (!RegisterClass(&Class)) return FALSE;

    hWndDemo = CreateWindow(
        szClassName, szClassName, WS_OVERLAPPEDWINDOW,
        30, 50, 200, 150,
        NULL, NULL, hInstDemo=hInstance, NULL);

    ShowWindow ( hWndDemo, cmdShow );
    UpdateWindow ( hWndDemo );

    while (GetMessage( &msg, NULL, 0, 0 ))    {
        TranslateMessage((LPMSG)&msg);
        DispatchMessage((LPMSG)&msg);
    }

    return msg.wParam;
}

long FAR PASCAL DemoWndProc ( HWND hWnd, unsigned message,
    WORD wParam, LONG lParam )
{
    switch (message) {

    case WM_COMMAND:
        DemoCommand ( hWnd, wParam );
        break;

    default:
        return DefWindowProc (hWnd, message, wParam, lParam) ;
        }
        return (long) 0 ;
}


void DemoCommand ( HWND hWnd, WORD wCmd )
{
    int i;
    HMENU hMenu;
    FARPROC lpfnDlg;

    switch (wCmd) {

    case IDM_TEST:
        lpfnDlg = MakeProcInstance( DemoDlg, hInstDemo );
        DialogBox( hInstDemo, "TEST", hWnd, lpfnDlg );
        FreeProcInstance ( lpfnDlg );
        break;

    default:
        break;
    }
}


BOOL FAR PASCAL DemoDlg ( HWND hWndDlg, unsigned message,
    WORD wParam, LONG lParam )
{
    char szBuf[40];
    int i;
    switch ( message ) {
    case WM_INITDIALOG:
        for ( i = IDS_COLOR1 ; i <= IDS_COLOR8 ; i++ ) {
            LoadString( hInstDemo, i, szBuf, 40 );
            SendDlgItemMessage( hWndDlg, IDD_COLORS,
                SL_ADDSTRING, 0, (LONG)(LPSTR)szBuf );
        }
        for ( i = IDS_STRING1 ; i <= IDS_STRING5 ; i++ ) {
            LoadString( hInstDemo, i, szBuf, 40 );
            SendDlgItemMessage( hWndDlg, IDD_STRINGS ,
                SL_ADDSTRING, 0, (LONG)(LPSTR)szBuf);
        }
        SendDlgItemMessage( hWndDlg, IDD_COLORS,
               SL_SETCURSEL, nTrueFalse, 0L );
        SendDlgItemMessage( hWndDlg, IDD_STRINGS, SL_SETCURSEL,
            nString, 0L );
        return TRUE;


    case WM_COMMAND:
        if (wParam == IDD_OK) {
        nTrueFalse = (int)SendDlgItemMessage( hWndDlg,
        IDD_COLORS, SL_GETCURSEL, 0, 0L );
    nString = (int)SendDlgItemMessage( hWndDlg, IDD_STRINGS,
            SL_GETCURSEL, 0, 0L );
        EndDialog( hWndDlg, TRUE );
    }
    break;

    default:
        return FALSE;
    }
    return TRUE;
}





Techniques for Calling OS/2 System Services from BASIC Programs

 Ethan Winer

One of the great myths about programming for OS/2 operating systems is
that it must be done using either C or assembly language. Nearly all of the
books and magazine articles that describe OS/2 system services rely on these
two languages exclusively for their examples. In this article I will
disprove that myth and show you how to access a variety of useful OS/2
services using the powerful Microsoft(R) BASIC Compiler Version 6.0.

Of all the high-level languages currently available, BASIC remains the most
immediately accessible to the largest number of people. It is the most
English-like and intuitive language and uses a command syntax that is simple
and easy to understand. With the introduction of Microsoft QuickBASIC
Version 4.5 and BASIC 6, no one can dispute that BASIC is now suitable for
serious program development.

This article will look at two different methods for implementing OS/2
services. The first uses purely BASIC commands in which an appropriate data
structure is defined and the various system functions are called by name.
This allows the BASIC programmer to immediately set up and call any OS/2
service using only BASIC statements. The second method provides an assembly
language interface between the BASIC program and the OS/2 operating systems.
The advantage of using assembly language is a reduction in the total amount
of code and data space needed in a program. Although its use does require a
bit more effort at the outset, the final result will clearly be worth it to
programmers who want to create the smallest and fastest programs possible.

The OS/2 functions presented in this article can serve as utilities or
routines for use in BASIC programs. For example, I will not cover opening
and reading files, because BASIC already provides those as part of the
language. But I will present programs that perform useful disk-related
functions, such as getting and setting the current default drive and
obtaining a list of file names.

OS/2 Essentials

Many OS/2 services require the address of a data structure in which the
various parameters will be passed between it and the calling program. There
are, however, several different types of structures used, and setting aside
memory for every possible variation will impinge on the available data
space. By using local stack variables in the assembler routines, this space
can be set aside before calling OS/21 and then recovered when it is no
longer needed. Using the stack for variable storage also ensures that these
routines will support reentrant code if and when that feature is added to
the BASIC run-time libraries. Similarly, all of the BASIC
subprograms and functions presented here do not use the STATIC keyword.
Therefore, they too will use the stack for all of their working variables.

One minor complication in interfacing BASIC to OS/2 services arises
from the way in which BASIC maintains string variables and constants.
While OS/2 expects all strings to be terminated with a CHR$(0) 0 byte,
BASIC uses a 4-byte descriptor to show where in memory a string is located
and how long it is. This incompatibility is easily resolved by
simply appending a 0 byte to the string before passing its address to the
OS/2 routine. Likewise, strings that are returned by OS/2 are terminated
with a 0 byte. Thus, the BASIC program must perform an extra step to locate
that byte in order to determine where the string ends.

Unlike DOS services, which are accessed via Interrupt 21h, the OS/2
operating system allows you to call system services by name. This is a
tremendous improvement from the programmer's point of view and provides
the most natural way to extend a high-level language.

No matter which OS/2 service is going to be called, each expects one or more
parameters in a particular order. There are a number of sources that list
all of the OS/2 functions and the parameters they expect. I recommend
Advanced OS/2 Programming by Ray Duncan (Microsoft Press) and the OS/2
Technical Reference manual published by IBM. BASIC 6.0 can't use all of the
OS/2 functions that are described in these manuals, however. The reader
should refer to the Include files provided with BASIC 6.0, which include all
the functions BASIC 6.0 does support.

Nearly all OS/2 services are set up to be declared and invoked as functions
from BASIC. Although they could just as easily be called as subroutines,
there would be no way to determine the success or failure of the call.
Integer functions that will be used with BASIC are designed to return their
value in the AX register. Because OS/2 also uses AX to indicate the success
of a given system call, this provides a simple and natural link between
BASIC and OS/2.

It is important to understand that external functions must be declared
before they can be used in a program. BASIC and assembler subprograms can
be invoked with the CALL keyword, which eliminates the requirement of
declaring them. With functions, however, BASIC would have no way of knowing
the difference between a function reference and an ordinary variable or
array element. For example, the GetDrive$ function that you will see in a
moment returns the current drive as a character. But if it hasn't been
declared, it will appear to be an ordinary string variable to the compiler:

CurDrive$ = GetDrive$

Disk Services

The first function to consider is GetDir$, which returns a string containing
the current directory for any valid drive. It will first be presented as a
purely BASIC function, and then the steps required to translate it into
assembly language will be shown. In both of these cases it will be relying
on the OS/2 DosQCurDir (Dos Query Current Directory) service, which does
the real work.

GETDIR.BAS, the BASIC program shown in Figure 1, is intended to be
compiled and linked with your main programs as a separate module. We will
take a look at each program statement in turn.

To offer the most flexibility, the GetDir$ function has been designed to
accept either a drive letter or a null string to indicate the current
default drive. Therefore, the first step is to see which was used. If the
string is not null, then the drive number is derived by subtracting 64 from
an uppercase version of the letter. Otherwise, the drive number is
assigned to 0. This corresponds to the OS/2 system, where drive A is
indicated with a 1, B with a 2, and so forth.

The next step is to create a buffer string in which OS/2 will place the name
of the current directory. The maximum length for a directory name is 64
characters, which is the same as the DOS operating system. If this changes
in a future version of OS/2, simply modify the value 64 in this program.
Path$ is then assigned to that many spaces.

As mentioned earlier, DosQCurDir is invoked as a function rather than being
called, because it returns any error codes as a function value. Thus,
DOSError will be set to 0 if no error occurred, or it will hold an OS/2
error code. If an error does occur, GetDir$ will return a null string to
indicate this. You could, of course, modify the function to return the
error, perhaps in an additional passed parameter.

Figure 1

'***** GetDir.Bas - BASIC function that returns the current directory

'syntax: Directory$ = GetDir$(Drive$)
' where: Drive$ is "A", "C", etc, or a null string meaning
'        the current drive

DEFINT A-Z
DECLARE FUNCTION DosQCurDir%(BYVAL Drive, BYVAL StrSeg, _
                             BYVAL StrAdr, SEG Length)


FUNCTION GetDir$(Drive$)

   IF LEN(Drive$) THEN    'see if a drive letter was specified
      DriveNum = ASC(UCASE$(Drive$)) - ASC("A") + 1
    'convert to a drive number
   ELSE
      DriveNum = 0    'else, tell OS/2 to use the current
    'drive
   END IF

   BufLen = 64    'define the buffer's length
   Path$ = SPACE$(BufLen)    'create a buffer to receive the
    'directory
    'invoke the function
   ErrorCode = DosQCurDir%(DriveNum, VARSEG(Path$), SADD(Path$), _
    BufLen)

   IF ErrorCode THEN    'see if there was an error
      GetDir$ = ""    'return null string to indicate the
    'error
   ELSE    'otherwise return what precedes the
    'CHR$(0)
      GetDir$ = "\" + LEFT$(Path$, INSTR(Path$, CHR$(0)) - 1)
   END IF

END FUNCTION

Variable Passing Methods

The DosQCurDir function requires three parameters to be passed to it. The
first is the drive number as described above. A point worth mentioning here
is that DosQCurDir expects the actual value of the drive number, as opposed
to an address of a variable set to that value. This is why the BYVAL
statement is used in the function declaration. When a subprogram or function
is declared with BYVAL preceding a parameter's name, BASIC pushes the
value of the parameter onto the stack, instead of its address, which is the
default method.

In general, OS/2 parameters that will be modified by a function must be
passed as a segmented address, whereas parameters that will only be read
by  the routine are passed by value. By default, BASIC passes variables by
reference so it will always be necessary to use BYVAL when calling OS/2
services. Since all OS/2 calls are FAR calls, the SEG keyword will be
required as well.

The second parameter is the string that was already assigned to a length
capable of holding the returned directory name. BASIC uses string
descriptors to indicate where and how long a given string is. OS/2, however,
expects both the segment and the address of the string data, as opposed to
the address of a BASIC string descriptor. Therefore, both VARSEG (Variable
Segment) and SADD (String Address) must be used when sending a BASIC string
to an OS/2 function.

The third parameter tells DosQCurDir how long a string you are sending to
it. If the string is too short, the function will return an error; but it
will also assign the BufLen variable to the required length. This way
DOSQCurDir may be invoked again with the string set to the proper length. In
that case, though, Path$ will have been assigned to a length that you know
will be sufficient before calling DosQCurDir.

The final step in the GetDir function is to locate the CHR$(0) 0 byte that
marks the end of the returned string and keep only those characters that
precede it. This is accomplished with a combination of BASIC's INSTR and
LEFT$ functions. Notice that OS/2 does not return the leading backslash, so
it is added in the GetDir$ function.

With an understanding of how GetDir$ works as a foundation, you can now
proceed to several other useful routines. Indeed, none of the remaining
routines to be created in BASIC are any more difficult than GetDir$; several
of them are, in fact, much simpler.

GetDrive and SetDrive

Related to obtaining the current directory (and indeed much simpler to
implement) is getting and setting the current default drive. The GetDrive
routine is designed as a function, while SetDrive is intended to be called
as a subprogram. The routines  are shown in Figures 2 and 5 respectively.

DosQCurDisk, the OS/2 service to obtain the current default drive, expects
the segmented addresses of two variables. The first is an integer that
will receive the drive number; the second is a long integer that receives a
drive "map." The drive is returned as 1 for drive A, 2 for B, and so forth.
Thus, 64 must be added to this value so the GetDrive function can return a
value that is equivalent to an ASCII character.

The drive map variable is bit coded to indicate all of the valid drives in
the host PC (see Figure 3). Even though GetDrive does not use the drive map
information, it is easy to write a new function that would indicate if any
given drive is legal. The lowest 26 bits in the long integer are set to
either 1 or 0 to indicate if a given drive is present.

The Valid program shown in Figure 4 will accept any drive letter and returns
a value to indicate if the drive exists. Valid is set up to return either
0 or -1, which allows the optional use of the NOT operator:

IF Valid%(Drive$) THEN

or

IF NOT Valid%("C") THEN

Setting the drive is equally simple, which you can see by examining the
SetDrive routine that is shown in Figure 5. Because the DosSelectDisk
service does not return information, the new drive to be assigned is passed
as a value. As with the other routines presented thus far, any errors that
are encountered are discarded. That is, if you try to assign a drive
that doesn't exist, the request will simply be ignored.

Figure 2

'***** GetDrive.Bas - BASIC function that returns the current drive

'syntax: Drive$ = CHR$(GetDrive%)
' where: Drive$ is returned as "A", "B", and so forth

DEFINT A-Z
DECLARE SUB DosQCurDisk(SEG Drive, SEG DriveMap&)

FUNCTION GetDrive%

   CALL DosQCurDisk(Drive, DriveMap&)
   GetDrive% = (Drive - 1) + ASC("A")

END FUNCTION

Figure 4

'***** Valid.Bas - BASIC function that reports if a drive
'                  letter is valid

'syntax: Okay = Valid%(Drive$)
' where: Okay receives -1 if the drive is valid, or zero if it is not

DEFINT A-Z
DECLARE SUB DosQCurDisk(SEG Temp, SEG DriveMap&)

FUNCTION Valid%(Drive$)

   Call DosQCurDisk(Temp, DriveMap&)
   Drive = ASC(Drive$) - ASC("A")
   IF DriveMap& AND 2 ^ Drive THEN
    'Check the appropriate bit in the mask
      Valid% = -1
   ELSE
      Valid% = 0
   END IF

END FUNCTION

Figure 5

'***** SetDrive.Bas - BASIC subprogram that sets the current drive

'syntax: CALL SetDrive(Drive$)
' where: Drive$ is specified as "A", "B", and so forth

DEFINT A-Z
DECLARE FUNCTION DosSelectDisk%(BYVAL Drive)

SUB SetDrive(Drive$)

   Drive = ASC(UCASE$(Drive$)) - ASC("A") + 1
               'now "A" = 1, "B" = 2, etc.
   ErrorCode = DosSelectDisk%(Drive)

END SUB

Reading File Names

One feature that nearly every application must provide is the ability to
obtain a list of file names from disk, perhaps before presenting them in a
menu. The approach taken here is to create two routines--the first is a
function that returns a count of the files that match a given search
specification, and the second is a subprogram that fills a string array with
all of their matching filenames. It is important to know how many matching
files exist ahead of time so that the array may be sufficiently dimensioned.

The FileCount function shown in Figure 6 accepts a file specification such
as *.BAS or C:\SUBDIR\*.* and returns the number of files that match. Once
the string array has been dimensioned, the same file specification
is passed to ReadNames (Figure 7), along with the array that will be filled
with each name. Notice that by specifying a complete name, FileCount may
also be used merely to determine whether a file exists. For example, if
FileCount returns 1 for \XYZ.DAT, you know such a file exists.

Both of these routines--FileCount and ReadNames--call upon the
DosFindFirst and DosFindNext OS/2 services to do the real work. Not unlike
their DOS counterparts, these services accept a file name or specification
and begin or continue a directory search, respectively. Once a matching
file has been located, an area of memory that the calling program supplies
is filled with information about the file.

When the OS/2 operating system fills the buffer with the file's information,
it includes much more than just the file's name. The date, time, size, and
attribute are also included, and these may be accessed by constructing a
suitable TYPE variable. A TYPE definition that organizes all of the
returned information is shown in Figure 8. Notice that with FAT-based
disks, the Create and Access portions of the buffer are always returned
holding 0.

Besides an incoming file specification and buffer address,
DosFindFirst also requires some additional items. One is a search handle,
which facilitates subsequent searches. To initiate a new search, this
handle will be assigned to -1 before calling DosFindFirst.
DosFindFirst then modifies this variable by assigning a handle that will be
used for all subsequent searches. When DosFindNext reports that there are no
more matching files by returning a nonzero value in AX, the DosFindClose
function is called to release the handle.

The calling program may also specify a search attribute that will include or
exclude certain types of files. A file search may, for example, be expanded
to include hidden, read-only, or system files or subdirectories. These
attributes are combined by OS/2, thus returning files that match one or more
of the search attributes. That is, if you ask for read-only files, you will
get normal files as well. Therefore, if you are searching for, say,
directory names, you must inspect each file's attribute that is returned to
be sure it really is a directory.

A list of bit settings for each of the possible file attributes is shown in
Figure 9. To search for directory names only, you would assign the attribute
variable to 10000b, which is 16 decimal. Then you must also inspect the
attribute portion of the buffer for that same value. If it matches, the
count would be incremented (in the case of FileCount) or the name would be
transferred to the string array (in the case of ReadNames).

In addition to providing a buffer for the returned information, the
calling program also tells DosFindFirst how large the buffer is. The
minimum length is 36 bytes, which is sufficient to hold information about a
single file. But you can also ask DosFindFirst to process several files at
once. You could, for example, create a buffer of 360 bytes and then assign
MaxCount to 10. OS/2 will then return up to 10 matching files at once.
Unfortunately, the information that is returned is not truly
record-oriented. Instead of padding each file name to the same length,
information about the next file immediately follows the end of the previous
file. This is unfortunate, since it precludes loading multiple elements
into a BASIC TYPE array.

Once the call to DosFindFirst has been set up and the first file has been
found, then subsequent calls to DosFindNext are handled in a simple DO
loop. In the FileCount function, for each matching file that is found the
Count variable is incremented. If an error occurs within the loop, you know
that there are no more files and you should exit the loop. The same logic is
used in the ReadNames routine to know when it has read the last available
file name.

Notice in ReadNames the devious method that is used to obtain the length of
each file name. Because BASIC does not provide a "byte-sized" variable type,
the FileFindBuf TYPE variable must instead use a string with a length of
1. Then the ASCII value of the string will tell how long the name is.
Because the file names are terminated with a 0 byte, INSTR could also be
used to locate that byte and determine the length. But the method used here
is more to the point and requires less code to implement.

As you can see, FileCount and ReadNames are nearly identical, differing
only in what they do with the results of the OS/2 system calls. FileCount
tracks the number of matches found, and ReadNames copies each name from the
buffer into an array. But because FileCount doesn't use any of the
information about the files that is returned, a single string is used
for the file information buffer. ReadNames, however, needs access to the
name and length portion of the buffer, so a TYPE is defined that allows
ready access to just those portions.

A complete demonstration program is shown in Figure 10 using each of the
routines discussed here in context. It begins by asking for a file
specification and then obtains a count of the matching files. The count is
used to dimension a string array; then ReadNames is called to read them.
Following that, GetDrive returns the current drive, which is saved. The
program then prompts for a new drive to which to change. This drive is again
retrieved just to prove that it worked. Finally, the original drive is
restored.

Assembly Language Calls

Now that you have an understanding of how the OS/2 services are
called, you can look at the steps required to translate them into assembly
language. As I mentioned, routines that are coded in assembler will require
much less code space than an equivalent BASIC subprogram or function. All of
the examples presented here are designed to be assembled with Microsoft
Macro Assembler (MASM) Version 5.1 or later.

I'll begin with the GetDir routine, which is shown in Figure 11. Like
its BASIC counterpart, GetDir has been designed as a string function.
The Mixed Languages Programming Guide that comes with MASM Version 5.0 and
later provides a brief discussion of creating integer and long integer
functions. Unfortunately, it does not describe how to create a string
function; the technique is, however, quite easy.

Any assembler routine can return a string by first creating a string
descriptor in BASIC's default data segment and then loading AX with the
address of the descriptor before returning to BASIC. Of course, the data
that is pointed to by the descriptor must also be in BASIC's data segment.

The first two items in the GetDir.Asm source listing are, therefore, a
length word followed by an address word. The string data itself
immediately follows the descriptor, although it could be located
anywhere in near memory. The Length portion of the descriptor is left
uninitialized, but the address part is assigned to point to the directory
name area. Because OS/2 does not return a leading backslash as part of a
directory name, this is included within the name data.

GetDir.Asm is designed to accept either an uppercase or lowercase drive
letter or a null string to indicate the default drive. The first step is to
load the address of the Drive$ descriptor into DI and move its length into
DX. If after ORing DX against itself the result is 0, you know a null string
was sent and the default drive should be used. If the length of Drive$ is
not 0, the first character is placed into DL and then capitalized if
necessary. The final step is to subtract 64 from the ASCII value to
convert A to 1, B to 2, and so forth.

For each variable expected by the DosQCurDir service, you must obtain and
pass either a segmented address or the value of the parameter. For example,
the first parameter is the drive number in DX, which is passed by value. But
in the case of the buffer that receives the directory name, its segment (DS)
must first be pushed, followed by its address, which was loaded into DI
earlier.

Likewise, the LenName variable is a local stack variable, so the SS (Stack
Segment) register rather than DS is pushed. BASIC 6.0 programs currently use
the same segment for DS and the stack. This is likely to change in the
future, however, so it is wise to push the correct segment. In order to get
the address for any local variables, you must use the Lea (Load Effective
Address) instruction instead of Offset.

Whereas Mov Offset is the most efficient way to obtain the address of a
normal assembler variable, the address of a local variable must be computed
at run time. Internally, MASM replaces your references to a local variable
with an expression such as [BP-2] or [BP-14]. But an instruction such as
Mov AX,Offset [BP-2] is obviously not legal. Therefore, Lea must be used to
load the effective address into a register (in this case CX), then that
register may be pushed.

Once DosQCurDir has been called, it will return the status in the AX
register. Note that, Or AX,AX is slightly faster and smaller than Cmp AX,0.
If the result is nonzero, you can assume that either an invalid drive was
given or perhaps the drive door was open. In either case, the only
reasonable action is to exit, leaving a null string to indicate the error.

If there was no error, a short loop is used to increment the length portion
of the descriptor while searching for the 0 byte that marks the end of the
directory name. Once that is done, the address of the descriptor is placed
into AX, where BASIC expects it.

In order to simplify the amount of coding and show an integer function in
context, the GetDrive routine has been designed to return an ASCII value
that corresponds to the current drive. It will therefore be declared as an
integer function with no incoming arguments. This is shown in Figure 12.
First, DosQCurDisk is called to place the drive into the local DriveNum
variable, which is then moved into AX. After adding 64 to convert it to
the equivalent ASCII drive letter, it is left in AX for the function's
output. The SetDrive routine in Figure 13 is equally simple to understand,
so I won't belabor its operation.

The assembly language routines that count and read file names are shown in
Figures 14 and 15, respectively. Like their BASIC counterparts, they are
similar in operation, except that FileCount merely counts the matches and
ReadNames transfers each name to the appropriate string array element.

Although an assembly language function can return a single string by
leaving a descriptor address in AX, it cannot actually allocate string
space or assign strings to a new length. This means that before ReadNames
can be used, the calling program must assign each string to a length of at
least 12. The demonstration program uses SPACE$(12) in a FOR/NEXT loop to
accomplish this. To prevent a possible crash, ReadNames examines the length
portion of each string descriptor and exits if the string isn't at least 12
characters in length. Notice that to simplify these routines and keep them
to the point, it is up to your program to append the required 0 byte to
the end of the file specification string.

Summing Up

As you have seen, accessing OS/2 system services directly can allow your
programs to include features that simply would not be possible using BASIC
commands alone. Nearly all of the available OS/2 services are called in a
manner similar to the ones covered here. Of course, BASIC offers nearly all
of the file and video capabilities that most programs need as part
of the language. But in many cases, calling the operating system
directly can result in either an improvement in performance or an easy
way to avoid disk errors without having to resort to the use of ON ERROR.

I hope that this brief introduction will encourage you to experiment
on your own and will expand your understanding of what OS/2 is all about. As
the OS/2 environment becomes more prevalent, there is no reason that
programmers using BASIC should miss out on all the fun.

Figure 6

'***** FCount.Bas - contains BASIC function returning number of
'                   matching file names


'syntax: Count = FileCount%(FileSpec$)
' where: FileSpec$ is in the form "*.BAS" or "A:\subdir\*.*",
'        and so forth
'  note: You may also specify a complete file name to see if
'        it exists

DEFINT A-Z

TYPE FileFindBuf    'this receives the date, time,
    'size, etc.
    MiscInfo AS STRING * 36
END TYPE

DECLARE FUNCTION _
      DosFindFirst%(BYVAL SpecSeg, BYVAL SpecAdr, _
                    SEG Handle, BYVAL Attrib, SEG Buffer _
                    AS FileFindBuf, BYVAL BufLen, SEG MaxCount, _
                    BYVAL Reserved&)

DECLARE FUNCTION DosFindNext%(BYVAL Handle, SEG Buffer AS _
                              FileFindBuf,  BYVAL BufLen, SEG _
                              MaxCount)

DECLARE SUB DosFindClose(BYVAL Handle)

FUNCTION FileCount%(FileSpec$)


   FileCount% = 0    'default to no matching file
    'names

   FSpec$ = FileSpec$ + CHR$(0)    'create an ASCIIZ string from
    'the file spec
   Handle = -1    'request a new search handle
   Attrib = 39    'matches all file types except
    'subdirectory
   DIM Buffer AS FileFindBuf    'create a buffer to hold the
    'returned info
   BufLen = 36    'tells OS/2 how big the buffer is
   MaxCount = 1    'get just one file
   Reserved& = 0    'this is needed, but not used

    'call DosFindFirst
   ErrorCode = DosFindFirst%(VARSEG(FSpec$), SADD(FSpec$), Handle, _
               Attrib, Buffer, BufLen, MaxCount, Reserved&)

   IF ErrorCode THEN EXIT FUNCTION    'no matching files or another
    'error, exit

   Count = 0    'we got one, increment at next
    'step

   DO
      Count = Count + 1    'we got another one
   LOOP WHILE (DosFindNext%(Handle, Buffer, BufLen, MaxCount) = 0)

   DosFindClose%(Handle)    'close the file handle
   FileCount% = Count    'assign the function output

END FUNCTION

Figure 7

'***** ReadName.Bas - contains BASIC function returning an array of 'file
names

'syntax: Count = FileCount%(FileSpec$)
'        DIM Array$(Count)
'        CALL ReadNames(FileSpec$, Array$())
' where: FileSpec$ is in the form "*.BAS" or "A:\subdir\*.*", and so
'        forth, and Array$() is filled in with each file's name

DEFINT A-Z

TYPE FileFindBuf
    MiscInfo AS STRING * 22    'Receives date, time, size, etc.
    FileLen  AS STRING * 1    'Receives the length of the name
    FileName AS STRING * 13    'Receives the name
END TYPE


DECLARE FUNCTION _
      DosFindFirst%(BYVAL SpecSeg, BYVAL SpecAdr, _
                    SEG Handle, BYVAL Attrib, SEG Buffer _
                    AS FileFindBuf, BYVAL BufLen, SEG MaxCount, _
                    BYVAL Reserved&)

DECLARE FUNCTION DosFindNext%(BYVAL Handle, SEG Buffer AS _
                              FileFindBuf, BYVAL BufLen, _
                              SEG MaxCount)

DECLARE SUB DosFindClose(BYVAL Handle)

SUB ReadNames(FileSpec$, Array$())

   FSpec$ = FileSpec$ + CHR$(0)    'create an ASCIIZ string from
    'the file spec
   Handle = -1    'request a new search handle
   Attrib = 39    'matches all file types except
    'subdirectory
   DIM Buffer AS FileFindBuf    'create a buffer to hold the
    'returned info
   BufLen = 36    'tells OS/2 how big the buffer
    'is
   MaxCount = 1    'get just one file
   Reserved& = 0    'this is needed, but not used

    'call DosFindFirst
   ErrorCode = DosFindFirst%(VARSEG(FSpec$), SADD(FSpec$), Handle, _
               Attrib, Buffer, BufLen, MaxCount, Reserved&)

   IF ErrorCode THEN EXIT SUB    'no matching files, exit
   Count = 0    'initialize counter

   DO
      Count = Count + 1    'we got another one
      Length = ASC(Buffer.FileLen)    'get the name's length
      Array$(Count) = LEFT$(Buffer.FileName, Length) 'assign the name
   LOOP WHILE (DosFindNext%(Handle, Buffer, BufLen, MaxCount) = 0)

   DosFindClose(Handle)    'close the handle

END SUB

Figure 8

TYPE FileBuffer
    CreateDate    AS INTEGER    'date file was originally created
    CreateTime    AS INTEGER    'time file was originally created
    AccessDate    AS INTEGER    'date of last file access
    AccessTime    AS INTEGER    'time of last file access
    WriteDate    AS INTEGER    'date file was last written to
    WriteTime    AS INTEGER    'time file was last written to
    FileSize    AS LONG        'the size of the file in bytes
    Allocation    AS LONG        ' memory allocation for the file in bytes
    Attribute    AS INTEGER    'the file's attribute
    NameLength    AS STRING * 1    'the length of the file's name
    FileName    AS STRING * 13    'the file's name, terminated with
                    'a CHR$(0)
END TYPE

Figure 10

'***** DEMO.BAS - demonstrates the BASIC and assembler OS/2 routines
'
'This routine can call either assembly language or BASIC modules.
'Include the statements marked "*** BASIC only" if using BASIC
'modules,and those marked "*** Assembly only" if using assembly
'language routines.

DEFINT A-Z

DECLARE FUNCTION FileCount%(FileSpec$)
DECLARE FUNCTION GetDir$(Drive$)
DECLARE FUNCTION GetDrive%()

DECLARE SUB ReadNames(Spec$, Array$())`    '*** BASIC only
DECLARE SUB ReadNames(Spec$, BYVAL Element)    '*** Assembly only
DECLARE SUB SetDrive(Drive$)

INPUT "Enter a file specification or press Enter for *.*:  ", Spec$

IF (Spec$ = "") THEN    'Pressed Enter, use
    '*.*
    Spec$ = "*.*"
ENDIF

Spec$ = Spec$ + CHR$(0)    '*** Assembly only
    '-- append NULL

Count = FileCount%(Spec$)    'count the number
    of matching files

IF (Count > 0) THEN

    DIM Array$(1 TO Count)    'make an array to
    'hold them

    FOR X = 1 TO Count    '*** Assembly only
        Array$(X) = SPACE$(12)    'Make array 12
    characters long
    NEXT

    ReadNames Spec$, Array$()    '*** BASIC only
    ReadNames Spec$, VARPTR(Array$(1))    '*** Assembly only

    FOR X = 1 TO Count    'print the
    'filenames
        PRINT Array$(X),
    NEXT

ELSE

    PRINT "No files found"    'No matching files

ENDIF

SaveDrive$ = CHR$(GetDrive%)    'save the current
    'drive
PRINT
PRINT "The current drive is " SaveDrive$    'print it
PRINT "and its directory is " GetDir$(SaveDrive$)
    'print current
    'directory
INPUT "Enter a new drive to move to: ", NewDrive$ 'prompt for a new
    'drive
SetDrive NewDrive$    'set it

PRINT "The current drive is: " CHR$(GetDrive%)    'print the new
    'drive

PRINT "Moving back to "SaveDrive$    'inform user
SetDrive SaveDrive$    'restore original     'drive

Figure 11

;----- GetDir.Asm - retrieves current directory

;Syntax -
;
; Drive$ = "A"
; Dir$ = GetDir$(Drive$)
; PRINT "Current directory is: " Dir$
;
; Where Drive$ = "" for the default drive, or "A" or "a" or "B", etc.
; and Dir$ receives the current directory path. If an error occurs,
; Dir$ will be assigned to a null string.

.286
.Model Medium, Basic
.Data
    DescrLen   DW ?
    DescrAdr   DW Offset DirName
    DirName    DB "\", 64 Dup (?)

.Code
    Extrn DosQCurDir:Proc    ;declare the OS/2 call as external

GetDir Proc, Drive:Ptr

    Local LenName:Word    ;create a local variable

    Cld    ;clear direction flag so
    ;string moves are forward
    Mov  DescrLen, 0    ;assume a null string in case
    ;there's an error
    Mov  LenName,65    ;tells OS/2 the available buffer length


    Mov  DI,Drive    ;put address of Drive$ into DI
    Mov  DX,[DI]    ;assume they want the default
    ;drive for now
    Or   DX,DX    ;check LEN(Drive$)
    Je   Do_It    ;it's null, leave DX holding 0
    ;and skip ahead

    Mov  DI,[DI+02]    ;put address of first character
    ;in Drive$ into DI
    Mov  DL,[DI]    ;put ASC(Drive$) into DL
    Cmp  DL,"a"    ;is it less than "a"?
    Jb   Continue    ;yes, skip
    Sub  DL,32    ;no, convert to upper case

Continue:
    Sub  DL,64    ;"A" now equals 1, "B" = 2, etc.

Do_It:
    Mov  DI,Offset DirName    ;get address for the string descriptor
    Inc  DI    ;bump past the leading backslash
    Mov  SI,DI    ;save it in SI too for later

    Push DX    ;pass the drive number
    Push DS    ;show which segment receives the name

    Push DI    ;and which address
    Push SS    ;pass segment for LenName
    ;(SS because it's local)
    Lea  CX,LenName    ;get address for LenName
    Push CX    ;and pass that too

    Call DosQCurDir    ;ask OS/2 to do the dirty work
    Or   AX,AX    ;was there an error?
    Jnz  Exit    ;yes, get out with DescrLen
    ;showing a null string

AddLen:    ;scan for zero byte that marks the end
    Inc  DescrLen    ;show the string as being one longer
    Lodsb    ;get a character from directory string
    Or   AL,AL    ;are we at the end of directory name?
    Jnz  AddLen    ;no, keep searching

Exit:
    Mov  AX,Offset DescrLen    ;tell BASIC where to find the descriptor
    Ret    ;return to BASIC

GetDir Endp
End

Figure 12

;----- GetDrive.Asm - retrieves the default drive

;Syntax -
;
;   PRINT "The current drive is "; CHR$(GetDrive%)

.286
.Model Medium, BASIC
.Code
    Extrn DosQCurDisk:Proc    ;declare the OS/2 call as external

GetDrive Proc

    Local DriveNum:Word    ;drive number
    Local DriveMap:DWord    ;logical drive map (not used here)

    Push SS    ;push segment for DriveNumber
    Lea  AX,DriveNum    ;get address
    Push AX    ;and push that
    Push SS    ;same for DriveMap segment
    Lea  AX,DriveMap    ;and address
    Push AX
    Call DosQCurDisk    ;call OS/2

    Mov  AX,DriveNum    ;load AX with the returned drive number
    Add  AX,64    ;now 1 = "A", 2 = "B", and so forth
    Ret    ;return to BASIC with the function
    ;output in AX

GetDrive Endp
End

Figure 13

;----- SetDrive.Asm - sets the default drive

;Syntax -
;

;   CALL SetDrive(Drive$)
;   where Drive$ = "A" or "a" or "B", etc.

.286
.Model Medium, Basic
.Code
    Extrn DosSelectDisk:Proc     ;declare the OS/2 call as external

SetDrive Proc, Drive:Ptr

    Mov  SI,Drive    ;put Drive$ descriptor address into SI
    Cmp  Word Ptr [SI],0    ;is Drive$ null?
    Jz   Exit    ;yes, ignore the request

    Mov  SI,[SI+02]    ;put address of Drive$ data in SI
    Mov  DL,[SI]    ;put ASC(Drive$) into DL

    Cmp  DL,'a'    ;is it below "a"?
    Jb   Continue    ;yes, skip
    Sub  DL,32    ;no, convert to upper case

Continue:
    Sub  DL,64    ;"A" now equals 1, "B" = 2, etc.
    Xor  DH,DH    ;clear DH so we can use all of DX
    Push DX    ;push drive number
    Call DosSelectDisk    ;call OS/2

Exit:
    Ret    ;return to BASIC

SetDrive Endp
End

Figure 14

; FCount.Asm - counts the number of files that match a specification

;Syntax -
;
; Spec$ = "*.*" + CHR$(0)
; Count = FileCount%(Spec$)
; Where Spec$ holds the file specification, and Count is assigned the
; number of files that match.

.286
.Model Medium, Basic
.Code
    Extrn DosFindFirst:Proc    ;Declare the OS/2 calls as external
    Extrn DosFindNext: Proc
    Extrn DosFindClose:Proc

FileCount Proc, Spec:Ptr

    Local Handle:      Word
    Local Buffer [18]: Word    ;36 bytes
    Local SearchCount: Word

    Xor  BX,BX    ;zero the count accumulator
    Mov  SI,Spec    ;SI holds address of Spec$ descriptor
    Mov  DX,[SI+02]    ;now DX points to the first character

    Mov  Handle, -1    ;directory handle of -1 to initiate
    ;search
    Mov  SearchCount,1    ;number of entries to find

    Push DS    ;push segment of Spec$
    Push DX    ;push offset of Spec$
    Push SS    ;push segment of local Handle
    Lea  AX,Handle    ;get Handle's address
    Push AX    ;push it too
    Push 00100111b    ;push attribute - all file types
    ;(except directory)
    Push SS    ;segment of buffer
    Lea  AX,Buffer [18]    ;get address of buffer
    Push AX    ;push it
    Push 36    ;push length of buffer
    Push SS    ;push segment of SearchCount
    Lea  AX,SearchCount    ;get address of SearchCount
    Push AX    ;push it
    Push 0    ;push a DWord of 0's (Reserved& in
    ;BASIC version)
    Push 0
    Call DosFindFirst    ;make call to find first occurrence of
    ;file
    Or   AX,AX    ;was there a file by that name?
    Jnz  Exit    ;if AX is not zero, then no file was
    ;found

Next_File:
    Inc  BX    ;show that we got another matching
    ;file

    Push Handle    ;push handle
    Push SS    ;push segment of buffer
    Lea  AX,Buffer [18]    ;get address of buffer
    Push AX    ;push it
    Push 36    ;push length of buffer
    Push SS    ;push segment of SearchCount
    Lea  AX,SearchCount    ;get address of SearchCount
    Push AX    ;push it
    Call DosFindNext    ;make call to find first occurrence of
    ;file
    Or   AX,AX    ;was there a file by that name?
    Jz   Next_File    ;if not carry, keep looking

Done:
    Push Handle    ;push handle
    Call DosFindClose    ;close it

Exit:
    Mov  AX,BX    ;assign the function output
    Ret    ;return to BASIC

FileCount Endp
End

Figure 15

; ReadName.Asm - reads a list of file names into a BASIC string array

;Syntax -
;
; Spec$ = "*.*" + CHR$(0)
; CALL ReadFile(Spec$, BYVAL VARPTR(FileName$(0)))
;
; Where Spec$ holds the directory specification, and elements in the
; FileName$() array receive the directory entries.

.286
.Model Medium, Basic
.Data

    FileJunk DB 22 Dup (?)
    NameLen  DB ?
    FileName DB 13 Dup (?)

.Code
    Extrn DosFindFirst: Proc     ;declare the OS/2 calls as external
    Extrn DosFindNext:  Proc
    Extrn DosFindClose: Proc

ReadNames Proc, Spec:Ptr, Array:Ptr

    Local Handle:   Word
    Local MaxCount: Word

    Mov  SI,Spec    ;SI holds address of Spec$ descriptor
    Mov  SI,[SI+02]    ;now SI points to the first character
    Mov  BX,Array    ;get address of FileName$(1) for later

    Mov  Handle, -1    ;directory handle of -1 to initiate
    ;search
    Mov  MaxCount,1    ;maximum number of entries to find

    Push DS    ;push segment of Spec$
    Push SI    ;push offset of Spec$ data
    Push SS    ;push segment of Handle in local
    ;storage
    Lea  AX,Handle    ;get address of Handle
    Push AX    ;push it
    Push 00100111b    ;the attribute for any type of file
    Push DS    ;segment for the buffer
    Push Offset FileJunk    ;push buffer address
    Push 36    ;push length of buffer
    Push SS    ;push segment of MaxCount
    Lea  AX,MaxCount    ;get address of MaxCount
    Push AX    ;push it
    Push 0    ;push a DWord of 0's
    Push 0
    Call DosFindFirst    ;make call to find first matching file
    ;name
    Or   AX,AX    ;was there a file by that name?
    Jnz  Exit    ;no, exit

    Push DS    ;ensure that ES points to BASIC's
    ;string
    Pop  ES    ;space for Movsb instructions below

NextFile:
    Cmp  Word Ptr [BX],12    ;is the string at least 12 characters
    ;long?
    Jb   Done    ;no, so get out now
    Mov  DI,[BX+02]    ;DI holds address of first character in
    ;FileName$()
    Lea  SI,FileName    ;get address of the name portion of
    ;buffer
    Mov  CL,NameLen    ;load CL with the number of characters to
    ;transfer
    Xor  CH,CH    ;clear CH so we can use all of CX
    Rep  Movsb    ;copy the string
    Add  BX,4    ;point BX to next array element for later

    Push SS:Handle    ;push handle
    Push DS    ;push segment of buffer
    Push Offset FileJunk    ;and buffer address
    Push 36    ;push length of result buffer
    Push SS    ;push segment of MaxCount
    Lea  AX,MaxCount    ;get address of MaxCount
    Push AX    ;push it
    Call DosFindNext    ;make call to find next matching file name
    Or   AX,AX    ;did we get another one?
    Jz   NextFile    ;if so, keep looking

Done:
    Push SS:Handle    ;push the handle number
    Call DosFindClose    ;free it up

Exit:
    Ret    ;return to BASIC

ReadNames Endp
End

Figure 16

; ReadName.Asm - reads a list of file names into a BASIC string array

;Syntax -
;
; Spec$ = "*.*" + CHR$(0)
; CALL ReadFile(Spec$, BYVAL VARPTR(FileName$(0)))
;
; Where Spec$ holds the directory specification, and elements in the
; FileName$() array receive the directory entries.

.286
.Model Medium, Basic
.Data

    FileJunk DB 22 Dup (?)
    NameLen  DB ?
    FileName DB 13 Dup (?)

.Code
    Extrn DosFindFirst: Proc     ;declare the OS/2 calls as external
    Extrn DosFindNext:  Proc
    Extrn DosFindClose: Proc

ReadNames Proc, Spec:Ptr, Array:Ptr

    Local Handle:   Word
    Local MaxCount: Word

    Mov  SI,Spec    ;SI holds address of Spec$ descriptor
    Mov  SI,[SI+02]    ;now SI points to the first character
    Mov  BX,Array    ;get address of FileName$(1) for later

    Mov  Handle, -1    ;directory handle of -1 to initiate
    ;search
    Mov  MaxCount,1    ;maximum number of entries to find

    Push DS    ;push segment of Spec$
    Push SI    ;push offset of Spec$ data
    Push SS    ;push segment of Handle in local
    ;storage
    Lea  AX,Handle    ;get address of Handle
    Push AX    ;push it
    Push 00100111b    ;the attribute for any type of file
    Push DS    ;segment for the buffer
    Push Offset FileJunk    ;push buffer address
    Push 36    ;push length of buffer
    Push SS    ;push segment of MaxCount
    Lea  AX,MaxCount    ;get address of MaxCount
    Push AX    ;push it
    Push 0    ;push a DWord of 0's
    Push 0
    Call DosFindFirst    ;make call to find first matching file
    ;name
    Or   AX,AX    ;was there a file by that name?
    Jnz  Exit    ;no, exit

    Push DS    ;ensure that ES points to BASIC's
    ;string
    Pop  ES    ;space for Movsb instructions below

NextFile:
    Cmp  Word Ptr [BX],12    ;is the string at least 12 characters
    ;long?
    Jb   Done    ;no, so get out now
    Mov  DI,[BX+02]    ;DI holds address of first character in
    ;FileName$()
    Lea  SI,FileName    ;get address of the name portion of
    ;buffer
    Mov  CL,NameLen    ;load CL with the number of characters to
    ;transfer
    Xor  CH,CH    ;clear CH so we can use all of CX
    Rep  Movsb    ;copy the string
    Add  BX,4    ;point BX to next array element for later

    Push SS:Handle    ;push handle
    Push DS    ;push segment of buffer
    Push Offset FileJunk    ;and buffer address
    Push 36    ;push length of result buffer
    Push SS    ;push segment of MaxCount
    Lea  AX,MaxCount    ;get address of MaxCount
    Push AX    ;push it
    Call DosFindNext    ;make call to find next matching file name
    Or   AX,AX    ;did we get another one?
    Jz   NextFile    ;if so, keep looking

Done:
    Push SS:Handle    ;push the handle number
    Call DosFindClose    ;free it up

Exit:
    Ret    ;return to BASIC

ReadNames Endp
End



Pointers 101: Understanding and Using Pointers  in the C Language

 Greg Comeau

Pointers are responsible for a good deal of the power in the C language.
They are often the only way to express a calculation and are used to produce
more compact and efficient code. On the other hand, pointers account for
most of the problems that occur while learning C, as well as most of the
run-time errors. In addition, most debugging sessions are spent deciphering
code involving them.

It is safe to say that most programmers have never completely understood
pointers. The terminology is confusing, and most of the explanatory material
found in the literature is, at best, incomplete. Hence, many programmers
develop an inconsistent and often incorrect understanding of them. My own
experience with pointers certainly proves this. Time and again I've said
that I've had to learn C twice; in reality, it's more like three times.
Regrettably, that means I've lost a lot of time writing incorrect and poorly
constructed programs.

This article and the following ones in this series will discuss some of the
more pressing issues involving pointers. The discussion will begin at the
simplest level, but one thing it won't do is cover the kind of pointer
basics most texts provide. As we turn our attention to some "heavy" details,
we will gain a solid grasp of fundamental issues, as well as cover topics
that still cause raging debates even among experts. Let's start by looking
at some of the definitions involved with pointer usage.

Identifiers and Variables

Many computer languages use the terms variable and identifier
interchangeably. While this practice is often followed when we describe our
C programs, it is incorrect to make such a generalization with C. An
identifier is a symbolic name given to an entity such as a label, a
function, a union, or some other object. An identifier provides both the
programmer and the compiler with an easy way of manipulating a given entity.

Variables, on the other hand, are unnamed objects that are locations in
storage. These objects might be absolute memory references, for example, or
a manipulation of data returned from routines such as malloc. Programmers
manipulate these variables by using pointers. Be aware that identifiers,
like unnamed objects, are symbolic values. Therefore a variable can be an
identifier and an identifier can be a variable.

Both variables and identifiers usually have storage allocated to them. While
identifiers are names we give to specific memory locations, there are also
memory locations that we don't give names to. That doesn't mean that they
don't exist--they do, and they are as real and important as any other
memory that a program uses. For instance, most of you have at least seen
code that uses a far pointer to poke into screen memory; or, in some cases,
the compiler may generate code that makes use of temporary stack locations
that you know nothing about. All these are instances of unnamed variables.

Constants

Identifiers refer not only to variables, but to such things as labels or
function names. One somewhat obtuse issue that is directly related to
references of these types involves the const keyword. Const is a term
borrowed from C++, though it has a slightly different meaning in C: in C
const can be reasonably defined as meaning "read only." This new qualifier
was added to the C language in the ANSI draft proposal.

The const keyword underscores this discussion. A variable is simply
something that changes; it can be read from and written to. That's why it's
a variable--it is able to change values. A constant does not change; it
can only be read from. For instance, let's say that you have an external
declaration such as



int someextern = 5;



whose value you do not want to change through the life of your program.
Unfortunately, the programmer is responsible for enforcing that the value
of someextern never changes. This is error-prone and in some cases
inefficient since you're only looking for a read-only identifier. Draft
ANSI C allows you to specify that an identifier serve as a constant rather
than a variable with the const keyword qualifier. As a simple example you
would code



const int someextern = 5;



to ensure that the compiler enforces the criteria outlined above.

Once you have coded someextern as a constant, it becomes a syntax error to
assign a different value to someextern later in the program. For other
examples consider the declarations in Figure 1.

Anything labeled as a constant cannot be assigned to. Considering this, it
must be agreed that an identifier such as ci and cpci can be given a const
attribute. Also, any unnamed memory location that you've pointed to, such as
*pci and *cpci, should be able to inherit such a quality as well. Remember
constants are in direct contrast to variables. For more information
on this point, see "A Guide to Understanding Even the Most Complex C
Declarations," MSJ (Vol. 3, No. 5).

Figure 1

const int    ci;    /* ci is a constant int */
const int    *pci;    /* pci is a pointer to a constant int */
int * const cpi;    /* cpi is a constant pointer to an int */
const int * const cpci;    /* cpi is a constant pointer to a
    constant int */

Objects

An object is defined as "a manipulatable region of storage" in the first
edition of The C Programming Language (referred to as K&R), Brian W.
Kernighan and Dennis M. Ritchie (Prentice Hall, 1978), p. 183. In the second
edition (Prentice Hall, 1988), p. 197, this definition changed somewhat to
"an object is a named region of storage," which seems to be an ambiguous pun
on terminology. I say that because names seem to have nothing to do
with objects alone. As I noted, unnamed objects can and do exist. I suspect
the definition in the second edition was intended to imply that if one can
pinpoint some object in memory, then it's been named (since it can be
accessed); such pinpointing, however, is akin to children being able to
find their way home even though they may not know the exact street and house
number. To a child, home is just a place--it doesn't necessarily
require a label (or address) to be identified. In this sense, the house is
similar to an unnamed object. It is accessible, yet we do not need to know
where it is or be able to describe it in all its contexts.

Therefore, even though we can't label every object--say, an integer at
memory location 8--with a precise identifier name, we can name an
object "by location" (the 8) or by means of a pointer. I admit that this
definition is perhaps a bit on the esoteric side. The definition in The ANSI
C December 7, 1988 Draft (X3 Secretariat, Computer and Business Equipment
Manufacturers Association), on p. 3, section 1.6, says "an object is a
region of data storage in the execution environment, the contents of
which can represent values. Except for bitfields, objects are composed of
contiguous sequences of one or more bytes, the number, order, and encoding
of which are either explicitly specified or implementation-defined. When
referenced, an object may be interpreted as having a particular type."
This definition is comparable to the first edition of K&R, except that the
contents of an object can represent values.

Note that every object is simply some location and area of storage whose
type is of no concern until it is accessed. The type of an object is
determined by the lvalue that refers to the object. A union is the perfect
example--when an expression references a member of a union, the region
of storage is interpreted as if it had the type of the member.

Lvalues

We've all been faced at one time or another with the humiliating and
apparently meaningless "lvalue required" error message. Most of us think
that an lvalue's definition is something along the lines of "an lvalue
goes on the left-hand side of an equal sign." Certainly, it has to deal with
an equal sign in part, but let's investigate further, since it turns out
that C has its own view of this.

First, let's get straight what an lvalue isn't. An lvalue is not a constant.
Nor is it an rvalue (a translation does occur, but more on that below). Nor
does it need to be an identifier--and not all identifiers need to be
lvalues.

One possible definition of an lvalue might be "an expression referring to
the location of an object." See Figure 2 for an elaboration of this
definition and keep it in mind as we proceed.

Figure 2 indicates that an lvalue can be an identifier. The next possible
lvalue is an expression that is allowed to have indirection applied to it.
Most of you are familiar with a situation like that shown in Figure 3a.
You should be able to recognize that the *p here is indeed an lvalue.
Unfortunately, for many C programmers this is the end of comprehending
lvalues involving dereferences. The line in Figure 3b is really no different
from the one in Figure 3a, although some of you may frown upon the fact, or
not be aware, that such a possibility exists.

Structure names are next on the list. Note that we are not talking about a
structure tag here, but about the actual name of a structure. A structure
name must be an lvalue and there are several reasons why. First, structure
assignments are allowed to occur. Second, since a struct.member construct
also refers to an object, the struct part itself must be one. Therefore, the
constraint here is that the struct must be an lvalue, which is a reasonable
requirement because the member is really a part of a bigger object that is
locatable through the struct lvalue.

A common derivation of structure usage is by means of pointers, as in the
ptr->member syntax. This syntax has the same constraint as above. That makes
sense, since such syntax is translatable and equivalent to (*ptr).member
syntax, which once again is another valid lvalue. Note that the definitions
involving structures hold true for unions as well.

The last lvalue category relates to arrays and is typically a major source
of confusion among both new and experienced C programmers. We will
consider this category below; for now, let's just say that an array[element]
is analogous to an identifier or variable, and I am sure you agree that it
is an lvalue as well.

If we apply the fact that a[i] is translatable into *(a + i), I think it's
fairly obvious that some of the information earlier in this section ties
together rather well. Also, since the translation above actually
transforms the array into a pointer expression, then *(ptr + i) and ptr[i]
are both valid lvalues as well. The use of & in constructs such as &array[i]
always yields an rvalue.

Figure 2

An lvalue can be:

    an identifier

    an expression using indirection

    a structure name

    structure.member

    ptr_to_structure->member

    (*ptr_to_structure).member

    array[element]

    *(array + index) or *(pointer + index)

An lvalue is not:

    a function name

    an array name

    enumerator names

    a function call

    a result of an assignment

    a result of a cast

Operators requiring lvalue as operands:

&, + +, - -, ., left of =

Operators that do not produce lvalues:

+ +, - -, &, (cast), =

(there are others, such as + or -, but I think

 their nonparticipation here is obvious)

Operators that can produce lvalues:

., ->, *, [ ]

Figure 3

3a

char    far *p = (char far *)0xB0000000;

*p = 'a';

3b

*(char far *)0xB0000000 = 'a';



Rvalues

The use of the term lvalue in C has been borrowed directly from compiler
terminology, along with the term rvalue. Consider the definitions in
Compilers, Principles, Techniques, and Tools, Alfred V. Aho, Ravi Sethi,
and Jeffrey D. Ullman (Addison-Wesley, 1986) p. 65: "The terms l-value and
r-value refer to values that are appropriate on the left and right sides of
an assignment, respectively. That is, r-values are what we usually think
of as 'values,' while l-values are locations." This is the source of the
equal sign idea you often hear about when texts attempt to define an
lvalue. The authors do provide a better description on p. 424, "the term
lvalue refers to the storage represented by an expression and an rvalue
refers to the value contained in that storage." Simply, that means that an
rvalue is the result obtained from a partial or full expression. In C
this definition is modified slightly to include constants and the other
entries in Figure 2 that are not lvalues. Also, note that when an lvalue is
used in a context where its value is being taken, it is translatable into an
rvalue. In other words, we know that the comments in the lines of code shown
in Figure 4 are true.

Think, though, about what happens when we see something like:

i = j;
c = *p;

Figure 4

i = 0;    /* i is the lvalue, 0 is the rvalue */
p = malloc(somenumber);    /* p is lvalue, rc from malloc is rvalue */

If we view all of i, j, c, and *p as individual entities in and of
themselves, they all are clearly lvalues. However, the contextual
situation of j and *p alert us (and the compiler) that, once the object we
are interested in has been located, we will be interested solely in the
value of that object, and the lvalue therefore must become an rvalue. That
is, we need to take the rvalue of the lvalue. This is an important piece of
information that is missing from such documents as K&R and is buried in a
footnote on p. 37 in the ANSI draft.

By the way, unnamed variables are not rvalues in and of themselves. To
repeat, the existence of an object determines its lvalue and rvalue, not a
quality such as whether that object has a "real" name that we can use.

Lvalues--A Second Look

Now that rvalues are out of the way, let's return to the definition of
lvalue presented in Figure 2. We can build upon the definition from
Compilers: Principles, Techniques, and Tools and say that an lvalue is an
expression that refers to an address. This reference is really to some
object in storage. Simplifying, we see that an lvalue refers to an object
and is therefore an "object locator." Thus, basically any variable
reference is an lvalue. There's no trick to it.

Modifiable vs. Unmodifiable Lvalues

Now it's time to get to some of the more confusing issues. Both K&R and the
ANSI draft fail to provide enough information. Instead of encapsulating the
discussion about lvalues, either they don't mention it at all (which is
almost the case with K&R), or one is forced to play an almost "goto: See
goto" game in what seems to be a mistaken attempt to avoid redundancy.

What this section really comes down to is the question of whether or not
array names are lvalues. C purists, of which I am one, would argue that they
are not. We purists feel that because an array name is usually
synonymous with its address or implicitly translatable into a pointer,
there shouldn't be any discussion about this question. In the former case,
an address is really a constant, which is really an rvalue, and the latter
case should represent an rvalue as well, since there is an implicit cast.

Because of the terseness of C and the definitions presented above, some
confusion exists that ANSI (not K&R) felt was necessary to consider and
correct. They believe that the number of C programmers confused over the
fact that array[index] is an lvalue whereas an array name is not was enough
justification to declare that an array name is now an lvalue as well. This
is quite silly. Rather than developing a more proper definition, they seem
to have created another problem--because what we said in the previous
paragraph no longer applies.

The problem is that now we can say &array without any regrets. Purists
would say this expression is incorrect since array is an address (constant)
and it is no different from saying &5, for instance, which is obviously
meaningless.

I should note, as an example of the kind of confusion making an array name
an lvalue causes, that at least one C text, C, A Reference Manual, Second
Edition, Samuel P. Harbison and Guy L. Steele, Jr. (Prentice-Hall, 1987),
and the Lattice C compiler work under the assumption that &array means
"pointer to array of type" rather than "pointer of type"--which are not
the same. I don't blame the authors for their interpretation, since it is
a natural expectation of the & operator. However, I think that we need time
to see what the real definition will be.

Amazingly, anomalies such as ending up with the equivalent of &5, don't seem
to matter much. First, the consensus was that it can't hurt to allow &array,
since the context is totally unambiguous and it's clear what the code
intended.

Second, situations arise, especially when typedef'ing or using macros,
where it may not be clear what type a given piece of data is, but you need
to use its address. For instance, when dealing with objects (that is, in the
object-oriented programming sense) like gizmos and widgets, you may or may
not care what the underlying internal representation of the gizmo or
widget is. Therefore, the following simplified code (and the compiler)
must act consistently:

typedef int anint;
typedef int array[10];

anint gizmo;
array widget;

func(&gizmo);
func(&widget);

Note that func( ) must be prototyped to accept a parameter with void *
type.

You may argue that the array should be encapsulated within a struct so long
as we're dealing with abstract data. However, such use has other side
effects, and others would say that such a measure would only be a kludge.

Finally, to put the frosting on the cake, ANSI decided to endorse two
official definitions of lvalue. One deals strictly with the object locator
terminology mentioned above, and the other deals only with the left-hand
side of an equal sign. Giving two definitions is not ambiguous, because
they've labeled the second one as "modifiable lvalue." They haven't
introduced the terminology of "unmodifiable lvalue," since all
modifiable lvalues are also "normal" lvalues. However, without that
terminology we can't talk about arrays as well as we should be able to.

One last piece of information tying all this together involves the const
keyword. Notice that a const object (not a pointer to one) is not a
modifiable lvalue either. It cannot appear on the left-hand side of an equal
sign (except during initialization). It really belongs in the
unmodifiable lvalue category along with arrays. Is this fact
actually a blessing in disguise? While an array name is also a constant, use
of const in C really means "read only" rather than "constant" (somewhat
contrary to it's use in C++).

Incomplete Types

A type is an attribute of an object or function return value that allows you
to make use of an access (read and/or write) of that object or function. The
object types consist of base types, such as integer, floating-point,
enumeration, void, and derived types as well. Derived types include
combinations of arrays, functions, pointers, structures, and unions.

There is one final category of types; the incomplete type, which uses
underlying bases types in most cases but does not yet contain enough
information about the object in question so that its size can be determined.
For instance, in the structure discussions in "Organizing Data in Your C
Programs with Structures, Unions, and Typedefs," MSJ (Vol. 4, No. 2), and
"Advanced Techniques for Using Structures and Unions in Your C Code," MSJ
(Vol. 4, No. 3), mention is made of how you can refer to a structure tag in
a declaration of a pointer even though that structure tag has not yet
been described in the code. Another example of an incomplete type would
be a common declaration such as:

extern sometype identifier[];

where the bounds of the array are not specified in the source file (in this
case, of course, it should be specified in at least one source file or
you'll get a link error complaining about an unresolved identifier).

Perhaps the best example of an incomplete type is the object pointed to by a
void *. We are stating without a doubt that, as a generic pointer, the void
* cannot be dereferenced, since that's part of what the generic pointer is
all about. Char* used to serve this purpose in part, but such overloaded
functionality was considered inappropriate.

As with all incomplete types, a good test of its completeness is to check
(at least mentally) if you can take the sizeof a given potential object.
This test can be particularly deceptive when dealing with structures
containing other incomplete structures (not pointers to them, though),
so watch out for this. A type based on another incomplete type is incomplete
as well--in fact, it is so incomplete that it would become a syntax
error and an invalid type.

Arrays and Pointers

The definition of array[type] is simple and is no different than in other
languages: it is a group of like objects that are contiguously adjacent to
each other in memory. Each adjacent object is an element of the array set,
and its type lacks the immediate array attribute that it is an element of.

In general, a pointer is defined as a variable that contains the address of
another variable. In practice, this definition is mostly true. However we
must also consider pointers to functions and pointers to incomplete types.
Because of these pointers, a more refined definition is required: a
pointer is a variable that contains the address of a function or another
object (in other words, things that will exist at run time). Note the use of
objects: their use means that pointers cannot point to labels anymore. If
you want to do that, you will need to take the risk and use setjmp and
longjmp.

The relationships among the multitude of terms discussed above are
important. One question that may bother you is whether the "lvalue
required" message is related to the difference between a pointer
reference to an array and a subscript reference to an array, as well as to
the way that the array is declared. The answer is, in part, yes, there is
a relationship. Details of this relationship will be covered in a future
article.

For the time being, suffice it to say that you must use each entity
correctly or it will be bad news. Most times the bad news is in the form of
a compiler error message, but it could also result in a run-time problem.

Pointer Mismatches

There are two rules of pointer use that you must constantly be aware of.
They are:

■    A pointer is not an integer. It never has been, and it never will be.

■    A pointer of type x is not equal to and has nothing to do with, a
pointer of type y. It never has, and it never will.

Some of you may feel that these rules are incorrect since you may have seen
code that performs operations contrary to them. No doubt you have, but that
code will be highly nonportable; it depends upon a common yet in no way
universal feature of many CPUs. This feature is that in many machines
pointers have the same size and internal representation as integers (or
"natural words"). However, in the IBM(R) PC, for one, they do not--a
program making use of both a near pointer and a far pointer breaks both of
these rules for one of the pointers (which one depends upon the memory model
you're in).

That is not to say that you should stay away from such "tricks" totally, but
that you should look at the alternatives before doing so. In general, you
might find such code existing in a device driver, where portability
might be silly to worry about, since the code is machine-specific anyway.
Furthermore, intimate knowledge of the machine leads to highly optimized
drivers. Just make sure you're aware that the use of such techniques is
termed undefined behavior. Under the right circumstances, though, you
simply may not care about this constraint.

Casts

Although most C programmers are aware that a cast coerces one type into
another, they do not always understand why you might want to do that. The
result is either too many casts or too few. You should definitely use casts
in some cases to ensure that arithmetic type expressions perform as desired
and with the correct precisions. This is probably the most portable use of a
cast, even though it is still dependent on the architecture and the result
is not always what you expect.

Another situation where there is a high occurrence of casts is when
comparing a pointer to zero. You will often see code that either casts zero
or NULL to the type of the pointer being compared. First, neither of the
casts is actually required, since C guarantees that the integer zero is a
special case and may be used in comparisons and assignments to pointers.

Be aware that you should really use NULL (typically found in <stdio.h> and
<stddef.h>) as it does represent an implementation-defined null pointer
constant. If you hide the value within the NULL macro, you can maintain a
machine-tailored representation for it with very little effort. In any
event, casts of the null pointer constant are more of a documentation
or style issue than a C requirement.

Even though a cast from signed to unsigned is fine (without a given sized
type), casting in the other direction is not. You are only guaranteed
success when moving to larger types (for example, char to int). However, the
determination of the two sizes of the type is not static and hence is not
portable either. Therefore most uses of casts, especially those involving
arithmetic calculation, imply nonportable behavior. The ramifications
of such constructs can be either system-dependent behavior or undefined
behavior. In the early days of C, type checking or rather the reporting of
it was almost nonexistent. To write code such as:

int   *ip;
char  *cp;

ip = cp;

wouldn't produce a single warning. Today similar code would generate an
indirection to different types warning. You can quickly shut up the compiler
with a cast:

ip = (int *)cp;

However, is that always the correct thing to do? A large group of
programmers and shops insist that warnings are as good (or shall we say as
bad?) as errors and that no code should be brought into production if it
contains warnings. Such an attitude is not always the proper one. If your
car is running out of oil, disconnecting the low-oil light doesn't make the
problem go away; the mismatched pointer message is not much different. It's
telling you that you're doing something that has the potential for side
effects, and that even if you can get it to work on your machine the way you
wrote it (and you may not even be able to), there is a good possibility
it may not work on another machine or under a different memory model. With
the warning list, you'll always be able to run through it and recheck the
lines the compiler complained about. Finally, before leaving casts, I want
to make it perfectly clear that casting does not guarantee portability;
casts only guarantee a quiet compiler! The bottom line is to use them
intelligently.


Bitfields

C started as a systems development language. Even though it is now used in
many other areas, it still serves the systems programming community well by
letting systems programmers toy with the underlying hardware of the machine
in many situations. One of the ways that programmers can do this is by
using bitfields.

Bitfield implementation can be compiler and system architecture specific and
the differing implementations can limit their usefulness. If you examine the
definition of bitfields and look at the strict rules governing their use,
you will be surprised at how uncompromising bitfields really are.

In short, a bitfield is a set of adjacent bits within a storage unit called
a word. Its definition and access are based on structures. For example

struct {
    signed int reserved : 4;
    signed int reserved : 2;
    signed int reset : 1;
    signed int rwflag : 1;
    } example;

defines a variable called example that contains bitfields of various sizes.
The number following the colon is an integer type specifying the number of
bits in the field. Bitfields are also subject to the following conditions:

■    a bitfield may be only type int, unsigned int, or

    signed int

■    an optional identifier, such as reserved in the above example, names
the bitfield

■    any sized area may be allocated for the field

■    adjacent fields are merged in an area if bits are available

■    nonbitfield members and 0 length bitfield members guarantee that
storage for the member following it in the member declaration list begin on
an int boundary.

Individual bitfields are referenced in the same way as other structure
members: example.reserved, example.speed, and so on. Bitfields behave like
integers and may participate in arithmetic expressions like other integers.

However there are a few caveats you should be aware of when using bitfields.
Bitfields are not arrays and do not have addresses, so you cannot use & with
them. You cannot use pointers to bitfields either, only to the addressable
area that they use. In addition, you cannot take the address or the size of
a bitfield. In fact, you may even question whether all this bitfield
terminology is a misnomer given the limits discussed throughout this
sidebar.

In general, a programmer is usually conscious of portability and will code
with different concerns in mind. However, with bitfields, not only are there
limitations to their use, but each C compiler is at liberty to choose its
own algorithm for implementing bitfields without regard to another compiler,
even if it is running on the same hardware.

Some of the implementation-defined aspects of bitfields that you should be
aware of are:

■    the high-order bit of an int field is a sign bit

■    adjacent bitfields occupy two areas (merged and new) if bits are
unavailable in the area

■    adjacent bitfields take over new area if bits are unavailable

■    the direction of bits ordering, for example low to high

■    field area alignment requirements

■    nonbitfield member alignment requirements

Of course, things like word ordering probably wouldn't change from compiler
to compiler on the same machine. However, let's say you are trying to add
something to a group of fields that is a member of a structure. This
structure is being pointed to in such a way that it uses direct memory
access (DMA) to write into a special register on a device. If you change
compilers, you don't want to find that the bitfields in your code are
accessed in a slightly different manner.

For example, the code in Figure A is unpredictable. The programmer of such a
piece of code would like the bitfields to be placed within the same word.
There is, however, no guarantee that this will happen. An implementation of
the C compiler is free to place a bitfield right next to another bitfield in
the same word, but if it doesn't want to, it doesn't have to. Assuming the
worst case, struct _example may actually take 20 bytes of space (5 bitfields
using one 4-byte int each); the best case is of course only 2 bytes. Code
such as that shown in Figure A usually expects the 2-byte situation and
therefore anything more, although valid C, can be detrimental.

There is something else to worry about as well. You may not know precisely
how the generated code in your executable file will access the bitfields.
Since you might be dealing with a device's register, the bitfield memory
references must occur in a specific fashion for the device to respond in a
reasonable manner. In this case, you will probably need to resort to
allocating the space for the register(s) to a proper size and then applying
the & (and) and | (or) operators (at least) to that memory area as required,
instead of using a bitfield.

The code in Figure B, for example, may cause the memory being pointed to by
pbits to be read from and written to eight times for each bit. For hardware
using strobe lines or where the access of one register affects the value of
another, all this reading and writing could produce meaningless results even
if the bits refer to the right memory locations. That may sound
unattractive, and it is.

You should realize that once you leave the hardware-specific problems of
bitfields, their nonportable aspects become much less important and in
general you are now faced with the typical space-versus-time trade-offs. For
instance, you may find the need to use bitfields when encoding a large
database to save precious disk or memory space. However, the encoding of the
fields in a smaller area will most likely produce a lot of bit twiddling and
therefore the program will run slower. Depending on your application, the
amount of code generated to manipulate a bitfield can be very lengthy. Look
at the assembler code generated from a piece of code using bitfields and you
will see what I mean.

One unsatisfying experience I went through when learning C was creating an
array of bitfields to mimic a bitmap. At first try, you expect to be able to
create an array of bits much like the one shown in Figure C. Unfortunately
this code produces a syntax error since you cannot have an array of
bitfields. The only other "natural" choice is to use code like that shown in
Figure D; however, you will quickly notice that the program begins to
consume an enormous amount of memory. The reason is that bitmap[1].bit is
not 1 bit away from bitmap[0].bit but is 1 word away (or possibly more if
alignment requirements must be considered). The only real option you have
here is to forget about the bitfield altogether and make the field a char.
You can forget about the struct as well and allocate an array of chars along
with a bunch of byte and bit access macros or functions making use of the &
and | operators. This, however, becomes tedious for programmers to code.

I was disappointed that you can't use the & (address) operator on bitfields
as you can with other members of structures. Bitfields are not addressable
entities. That makes sense when you realize that one or more bitfields could
appear within the same word. Instead of returning the same address for the
fields, the operation is simply not allowed. In Figure E, the assignments of
the address of bit1 and bit2 to pi are therefore both syntax errors.

I have covered the problems of bitfields and why they are avoided. For most
systems programming, bitfields should be used cautiously. I would advise
writing a small piece of code that uses them and executing that code while
in a debugger so you can verify what is happening to that memory. In
application programming, though, most of the implementation-specific
pitfalls of bitfields can be ignored. Under these circumstances, the
notational convenience of bitfields is well worth their use.

Figure A

#define    RESET    0
#define    NORESET    1
#define    READ     0
#define    WRITE     1

struct _example {
    int     reserved : 4;
    int     speed : 2;
    int     reset : 1;
    int     rwflag : 1;
    int     data : 8;
};

main()
{
    struct _example *pexample;

    /* code initializing pexample to an absolute address */

    pexample->speed = 96;
    pexample->reset = NORESET;
    pexample->rwflag = WRITE;
    pexample->data = 'a';

    /* ... */
}

Figure B

#define DMA_ADDR   0 /* dummy addr for now */

struct _bits {
    int     bit1 : 1;
    int     bit2 : 1;
    int     bit3 : 1;
    int     bit4 : 1;
    int     bit5 : 1;
    int     bit6 : 1;
    int     bit7 : 1;
    int     bit8 : 1;
};

main()
{
    struct _bits *pbits = (struct _bits *)DMA_ADDR;

    pbits->bit1 = 1;
    pbits->bit2 = 1;
    pbits->bit3 = 1;
    pbits->bit4 = 1;
    pbits->bit5 = 1;
    pbits->bit6 = 1;
    pbits->bit7 = 1;
    pbits->bit8 = 1;
}

Figure C

struct _bitmap {
    int     bits[1024] : 1;
} bitmap;

main()
{
    int     i;

    /* initialize the bitmap */
    for (i = 0; i < 1024; i++)
        bitmap.bits[i] = 0;
}

Figure D

struct _bitmap {
    int     bit : 1;
} bitmap[1024];

main()
{
    int     i;

    /* initialize the bitmap */
    for (i = 0; i < 1024; i++)
        bitmap[i].bit = 0;

    printf("%d\n", sizeof(bitmap));
}

Figure E

struct _example {
    int     xyz;
    int     bit1 : 1;
    /* .......... */
    int     bit2 : 1;
} example;

main()
{
    int     *pi;

    pi = &example.xyz; /* fine */
    pi = &example.bit1; /* not fine */
    pi = &example.bit2; /* not fine */
}


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

Volume 4 - Number 5

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


Design Goals and Implementation of the New High Performance File System

Ray Duncan

The High Performance File System (hereafter HPFS), which is making its first
appearance in the OS/2 operating system Version 1.2, had its genesis in the
network division of Microsoft and was designed by Gordon Letwin, the chief
architect of the OS/2 operating system. The HPFS has been designed to meet
the demands of increasingly powerful PCs, fixed disks, and networks for many
years to come and to serve as a suitable platform for object-oriented
languages, applications, and user interfaces.

The HPFS is a complex topic because it incorporates three distinct yet
interrelated file system issues. First, the HPFS is a way of organizing data
on a random access block storage device. Second, it is a software module
that translates file-oriented requests from an application program into more
primitive requests that a device driver can understand, using a variety of
creative techniques to maximize performance. Third, the HPFS is a practical
illustration of an important new OS/2 feature known as installable file
systems.

This article introduces the three aspects of the HPFS. But first, it puts
the HPFS in perspective by reviewing some of the problems that led to the
system's existence.

FAT File System

The so-called FAT file system, which is the file system used in all versions
of the MS-DOS(R) operating system to date and in the first two releases of
OS/21 (Versions 1.0 and 1.1), has a dual heritage in Microsoft's earliest
programming language products and the Digital Research(R) CP/M(R) operating
system--software originally written for 8080-based and Z-80-based
microcomputers. It inherited characteristics from both ancestors that have
progressively turned into handicaps in this new era of multitasking,
protected mode, virtual memory, and huge fixed disks.

The FAT file system revolves around the File Allocation Table for which it
is named. Each logical volume has its own FAT, which serves two important
functions: it contains the allocation information for each file on the
volume in the form of linked lists of allocation units (clusters, which are
power-of-2 multiples of sectors) and it indicates which allocation units are
free for assignment to a file that is being created or extended.

The FAT was invented by Bill Gates and Marc McDonald in 1977 as a method of
managing disk space in the NCR version of standalone Microsoft(R) Disk
BASIC. Tim Paterson, at that time an employee of Seattle Computer Products
(SCP), was introduced to the FAT concept when his company shared a booth
with Microsoft at the National Computer Conference in 1979. Paterson
subsequently incorporated FATs into the file system of 86-DOS, an operating
system for SCP's S-100 bus 8086 CPU boards. 86-DOS was eventually purchased
by Microsoft and became the starting point for MS-DOS2 Version 1.0, which
was released for the original IBM(R) PC in August 1981.

When the FAT was conceived, it was an excellent solution to disk management,
mainly because the floppy disks on which it was used were rarely larger than
1Mb. On such disks, the FAT was small enough to be held in memory at all
times, allowing very fast random access to any part of any file. This proved
far superior to the CP/M method of tracking disk space, in which the
information about the sectors assigned to a file might be spread across many
directory entries, which were in turn scattered randomly throughout the disk
directory.

When applied to fixed disks, however, the FAT began to look more like a bug
than a feature. It became too large to be held entirely resident and had to
be paged into memory in pieces; this paging resulted in many superfluous
disk head movements as a program was reading through a file and degraded
system throughput. In addition, because the information about free disk
space was dispersed across many sectors of FAT, it was impractical to
allocate file space contiguously, and file fragmentation became another
obstacle to good performance. Moreover, the use of relatively large clusters
on fixed disks resulted in a lot of dead space, since an average of one-half
cluster was wasted for each file. (Some network servers use clusters as
large as 64Kb.)

The FAT file system's restrictions on naming files and directories are
inherited from CP/M. When Paterson was writing 86-DOS, one of his primary
objectives was to make programs easy to port from CP/M to his new operating
system. He therefore adopted CP/M's limits on filenames and extensions so
the critical fields of 86-DOS File Control Blocks (FCBs) would look almost
exactly like those of CP/M. The sizes of the FCB filename and extension
fields were also propagated into the structure of disk directory entries. In
due time, 86-DOS became MS-DOS and application programs for MS-DOS
proliferated beyond anyone's wildest dreams. Since most of the early
programs depended on the structure of FCBs, the 8.3 format for filenames
became irrevocably locked into the system.

During the last couple of years, Microsoft and IBM have made valiant
attempts to prolong the useful life of the FAT file system by lifting the
restrictions on volume sizes, improving allocation strategies, caching
pathnames, and moving tables and buffers into expanded memory. But these can
only be regarded as temporizing measures, because the fundamental data
structures used by the FAT file system are simply not well suited to large
random access devices.

The HPFS solves the FAT file system problems mentioned here and many others,
but it is not derived in any way from the FAT file system. The architect of
the HPFS started with a clean sheet of paper and designed a file system that
can take full advantage of a multitasking environment, and that will be able
to cope with any sort of disk device likely to arrive on microcomputers
during the next decade.

HPFS Volume Structure

HPFS volumes are a new partition type--type 7--and can exist on a
fixed disk alongside of the several previously defined FAT partition types.
IBM-compatible HPFS volumes use a sector size of 512 bytes and have a
maximum size of 2199Gb (232 sectors). Although there is no particular reason
why floppy disks can't be formatted as HPFS volumes, Microsoft plans to
stick with FAT file systems on floppy disks for the foreseeable future.
(This ensures that users will be able to transport files easily between
MS-DOS and OS/2 systems.)

An HPFS volume has very few fixed structures (Figure 1). Sectors 0-15
of a volume (8Kb) are the BootBlock and contain a volume name, 32-bit volume
ID, and a disk bootstrap program. The bootstrap is relatively sophisticated
(by MS-DOS standards) and can use the HPFS in a restricted mode to locate
and read the operating system files wherever they might be found.

Sectors 16 and 17 are known as the SuperBlock and the SpareBlock
respectively. The SuperBlock is only modified by disk maintenance utilities.
It contains pointers to the free space bitmaps, the bad block list, the
directory block band, and the root directory. It also contains the date that
the volume was last checked out and repaired with CHKDSK /F. The SpareBlock
contains various flags and pointers that will be discussed later; it is
modified, although infrequently, as the system executes.

The remainder of the disk is divided into 8Mb bands. Each band has its own
free space bitmap in which a bit represents each sector. A bit is 0 if the
sector is in use and 1 if the sector is available. The bitmaps are located
at the head or tail of a band so that two bitmaps are adjacent between
alternate bands. This allows the maximum contiguous free space that can be
allocated to a file to be 16Mb. One band, located at or toward the seek
center of the disk, is called the directory block band and receives special
treatment (more about this later). Note that the band size is a
characteristic of the current implementation and may be changed in later
versions of the file system.

Files and Fnodes

Every file or directory on an HPFS volume is anchored on a fundamental file
system object called an Fnode (pronounced "eff node"). Each Fnode occupies a
single sector and contains control and access history information used
internally by the file system, extended attributes and access control lists
(more about this later), the length and the first 15 characters of the name
of the associated file or directory, and an allocation structure (Figure 2).
An Fnode is always stored near the file or directory that it represents.

The allocation structure in the Fnode can take several forms, depending on
the size and degree of contiguity of the file or directory. The HPFS views a
file as a collection of one or more runs or extents of one or more
contiguous sectors. Each run is symbolized by a pair of doublewords--a
32-bit starting sector number and a 32-bit length in sectors (this is
referred to as run-length encoding). From an application program's point of
view, the extents are invisible; the file appears as a seamless stream of
bytes.

The space reserved for allocation information in an Fnode can hold pointers
to as many as eight runs of sectors of up to 16Mb each. (This maximum run
size is a result of the band size and free space bitmap placement only; it
is not an inherent limitation of the file system.) Reasonably  small files
or highly contiguous files can therefore be described completely within the
Fnode (Figure 3).

HPFS uses a new method to represent the location of files that are too large
or too fragmented for the Fnode and consist of more than eight runs. The
Fnode's allocation structure becomes the root for a B+ Tree of allocation
sectors, which in turn contain the actual pointers to the file's sector runs
(see Figure 4 and the sidebar, "B- Trees and B+ Trees"). The Fnode's root
has room for 12 elements. Each allocation sector can contain, in addition to
various control information, as many as 40 pointers to sector runs.
Therefore, a two-level allocation B+ Tree can describe a file of 480 (12*40)
sector runs, with a theoretical maximum size of 7.68Gb (12*40*16 Mb) in the
current implementation (although the 32-bit signed offset parameter for
DosChgFilePtr effectively limits file sizes to 2Gb).

In the unlikely event that a two-level B+ Tree is not sufficient to describe
a highly fragmented file, the file system will introduce additional levels
in the tree as needed. Allocation sectors in the intermediate levels can
hold as many as 60 internal (nonterminal) B+ Tree nodes, which means that
the descriptive ability of this structure rapidly grows to numbers that are
nearly beyond comprehension. For example, a three-level allocation B+ Tree
can describe a file with as many as 28,800 (12*60*40) sector runs.

Run-length encoding and B+ Trees of allocation sectors are a
memory-efficient way to specify a file's size and location, but they have
other significant advantages. Translating a logical file offset into a
sector number is extremely fast: the file system just needs to traverse the
list (or B+ Tree of lists) of run pointers until it finds the correct range.
It can then identify the sector within the run with a simple calculation.
Run-length encoding also makes it trivial to extend the file logically if
the newly assigned sector is contiguous with the file's previous last
sector; the file system merely needs to increment the size doubleword of the
file's last run pointer and clear the sector's bit in the appropriate
freespace bitmap.

Directories

Directories, like files, are anchored on Fnodes. A pointer to the Fnode for
the root directory is found in the SuperBlock. The Fnodes for directories
other than the root are reached through subdirectory entries in their parent
directories.

Directories  can grow to any size and are built up from 2Kb directory
blocks, which are allocated as four consecutive sectors on the disk. The
file system attempts to allocate directory blocks in the directory band,
which is located at or near the seek center of the disk. Once the directory
band is full, the directory blocks are allocated wherever space is
available.

Each 2Kb directory block contains from one to many directory entries. A
directory entry contains several fields, including time and date stamps, an
Fnode pointer, a usage count for use by disk maintenance programs, the
length of the file or directory name, the name itself, and a B-Tree pointer.
Each entry begins with a word that contains the length of the entry. This
provides for a variable amount of flex space at the end of each entry, which
can be used by special versions of the file system and allows the directory
block to be traversed very quickly (Figure 5).

The number of entries in a directory block varies with the length of names.
If the average filename length is 13 characters, an average directory block
will hold about 40 entries. The entries in a directory block are sorted by
the binary lexical order of their name fields (this happens to put them in
alphabetical order for the U.S. alphabet). The last entry in a directory
block is a dummy record that marks the end of the block.

When a directory gets too large to be stored in one block, it increases in
size by the addition of 2Kb blocks that are organized as a B-Tree (see
"B-Trees and B+ Trees"). When searching for a specific name, the file system
traverses a directory block until it either finds a match or finds a name
that is lexically greater than the target. In the latter case, the file
system extracts the B- Tree pointer from the entry. If there is no pointer,
the search failed; otherwise the file system follows the pointer to the next
directory block in the tree and continues the search.

A little back-of-the-envelope arithmetic yields some impressive statistics.
Assuming 40 entries per block, a two-level tree of directory blocks can hold
1640 directory entries and a three-level tree can hold an astonishing 65,640
entries. In other words, a particular file can be found (or shown not to
exist) in a typical directory of 65,640 files with a maximum of three disk
hits--the actual number of disk accesses depending on cache contents
and the location of the file's name in the directory block B-Tree. That's
quite a contrast to the FAT file system, where in the worst case more than
4000 sectors would have to be read to establish that a file was or was not
present in a directory containing the same number of files.

The B-Tree directory structure has interesting implications beyond its
effect on open and find operations. A file creation, renaming, or deletion
may result in a cascade of complex operations, as directory blocks are added
or freed or names are moved from one block to the other to keep the tree
balanced. In fact, a rename operation could theoretically fail for lack of
disk space even though the file itself is not growing. In order to avoid
this sort of disaster, the HPFS maintains a small pool of free blocks that
can be drawn from in a directory emergency; a pointer to this pool of free
blocks is stored in the SpareBlock.

Extended Attributes

File attributes are information about a file that is maintained by the
operating system outside the file's overt storage area. The FAT file system
supports only a few simple attributes (read only, system, hidden, and
archive) that are actually stored as bit flags in the file's directory
entry; these attributes are inspected or modified by special function calls
and are not accessible through the normal file open, read, and write calls.

The HPFS supports the same attributes as the FAT file system for historical
reasons, but it also supports a new form of file-associated, highly
generalized information called Extended Attributes (EAs). Each EA is
conceptually similar to an environment variable, taking the form

name=value

except that the value portion can be either a null-terminated (ASCIIZ)
string or binary data. In OS/2 1.2, each file or directory can have a
maximum of 64Kb of EAs attached to it. This limit may be lifted in a later
release of OS/2.

The storage method for EAs can vary. If the EAs associated with a given file
or directory are small enough, they will be stored right in the Fnode. If
the total size of the EAs is too large, they are stored outside the Fnode in
sector runs, and a B+ Tree of allocation sectors can be created to describe
the runs. If a single EA gets too large, it can be pushed outside the Fnode
into a B+ Tree of its own.

The kernel API functions DosQFileInfo and DosSetFileInfo have been expanded
with new information levels that allow application programs to manipulate
extended attributes for files. The new functions DosQPathInfo and
DosSetPathInfo are used to read or write the EAs associated with arbitrary
pathnames. An application program can either ask for the value of a specific
EA (supplying a name to be matched) or can obtain all of  the EAs for the
file or directory at once.

Although application programs can begin to take advantage of EAs as soon as
the HPFS is released, support for EAs is an essential component in
Microsoft's long-range plans for object-oriented file systems. Information
of almost any type can be stored in EAs, ranging from the name of the
application that owns the file to names of dependent files to icons to
executable code. As the HPFS evolves, its facilities for manipulating EAs
are likely to become much more sophisticated. It's easy to imagine, for
example, that in future versions the API might be extended with EA functions
that are analogous to DosFindFirst and DosFindNext and EA data might get
organized into B-Trees.

I should note here that in addition to EAs, the LAN Manager version of HPFS
will support another class of file-associated information called Access
Control Lists (ACLs). ACLs have the same general appearance as EAs and are
manipulated in a similar manner, but they are used to store access rights,
passwords, and other information of interest in a networking multiuser
environment.

Installable File Systems

Support for installable file systems has been one of the most eagerly
anticipated features of OS/2 Version 1.2. It will make it possible to access
multiple incompatible volume structures--FAT, HPFS, CD ROM, and perhaps
even UNIX(R)--on the same OS/2 system at the same time, will simplify
the life of network implementors, and will open the door to rapid file
system evolution and innovation. Installable file systems are, however, only
relevant to the HPFS insofar as they make use of the HPFS optional. The FAT
file system is still embedded in the OS/2 kernel, as it was in OS/2 1.0 and
1.1, and will remain there as the compatibility file system for some time to
come.

An installable file system driver (FSD) is analogous in many ways to a
device driver. An FSD resides on the disk in a file that is structured like
a dynamic-link library (DLL), typically with a SYS or IFS extension, and is
loaded during system initialization by IFS= statements in the CONFIG.SYS
file. IFS= directives are processed in the order they are encountered and
are also sensitive to the order of DEVICE=  statements for device drivers.
This lets you load a device driver for a nonstandard device, load a file
system driver from a volume on that device, and so on.

Once an FSD is installed and initialized, the kernel communicates with it in
terms of logical requests for file opens, reads, writes, seeks, closes, and
so on. The FSD translates these requests--using control structures and
tables found on the volume itself--into requests for sector reads and
writes for which it can call special kernel entry points called File System
Helpers (FsHlps). The kernel passes the demands for sector I/O to the
appropriate device driver and returns the results to the FSD (Figure 6).

The procedure used by the operating system to associate volumes with FSDs is
called dynamic mounting and works as follows. Whenever a volume is first
accessed, or after it has been locked for direct access and then unlocked
(for example, by a FORMAT operation), OS/2 presents identifying information
from the volume to each of the FSDs in turn until one of them recognizes the
information. When an FSD claims the volume, the volume is mounted and all
subsequent file I/O requests for the volume are routed to that FSD.

Performance Issues

The HPFS attacks potential bottlenecks in disk throughput at multiple
levels. It uses advanced data structures, contiguous sector allocation,
intelligent caching, read-ahead, and deferred writes in order to boost
performance.

First, the HPFS matches its data structures to the task at hand:
sophisticated data structures (B-Trees and B+ Trees) for fast random access
to filenames, directory names, and lists of sectors allocated to files or
directories, and simple compact data structures (bitmaps) for locating
chunks of free space of the appropriate size. The routines that manipulate
these data structures are written in assembly language and have been
painstakingly tuned, with special focus on the routines that search the
freespace  bitmaps for patterns of set bits (unused sectors).

Next, the HPFS's main goal --its prime directive, if you will-- is
to assign consecutive sectors to files whenever possible. The time required
to move the disk's read/write head from one track to another far outweighs
the other possible delays, so the HPFS works hard to avoid or minimize such
head movements by allocating file space contiguously and by keeping control
structures such as Fnodes and freespace bitmaps near the things they
control. Highly contiguous files also help the file system make fewer
requests of the disk driver for more sectors at a time, allow the disk
driver to exploit the multisector transfer capabilities of the disk
controller, and reduce the number of disk completion interrupts that must be
serviced.

Of course, trying to keep files from becoming fragmented in a multitasking
system in which many files are being updated concurrently is no easy chore.
One strategy the HPFS uses is to scatter newly created files across the
disk--in separate bands, if possible--so that the sectors
allocated to the files as they are extended will not be interleaved. Another
strategy is to preallocate approximately 4Kb of contiguous space to the file
each time it must be extended and give back any excess when the file is
closed.

If an application knows the ultimate size of a new file in advance, it can
assist the file system by specifying an initial file allocation when it
creates the file. The system will then search all the free space bitmaps to
find a run of consecutive sectors large enough to hold the file. That
failing, it will search for two runs that are half the size of the file, and
so on.

The HPFS relies on several different kinds of caching to minimize the number
of physical disk transfers it must request. Naturally, it caches sectors, as
did the FAT file system. But unlike the FAT file system, the HPFS can manage
very large caches efficiently and adjusts sector caching on a per-handle
basis to the manner in which a file is used. The HPFS also caches pathnames
and directories, transforming disk directory entries into an even more
compact and efficient in-memory representation.

Another technique that the HPFS uses to improve performance is to preread
data it believes the program is likely to need. For example, when a file is
opened, the file system will preread and cache the Fnode and the first few
sectors of the file's contents. If the file is an executable program or the
history information in the file's Fnode shows that an open operation has
typically been followed by an immediate sequential read of the entire file,
the file system will preread and cache much more of the file's contents.
When a program issues relatively small read requests, the file system always
fetches data from the file in 2Kb chunks and caches the excess, allowing
most read operations to be satisfied from the cache.

Finally, the OS/2 operating system's support for multitasking makes it
possible for the HPFS to rely heavily on lazy writes (sometimes called
deferred writes or write behind) to improve performance. When a program
requests a disk write, the data is placed in the cache and the cache buffer
is flagged as dirty (that is, inconsistent with the state of the data on
disk). When the disk becomes idle or the cache becomes saturated with dirty
buffers, the file system uses a captive thread from a daemon process to
write the buffers to disk, starting with the oldest data.

In general, lazy writes mean that programs run faster because their read
requests will almost never be stalled waiting for a write request to
complete. For programs that repeatedly read, modify, and write a small
working set of records, it also means that many unnecessary or redundant
physical disk writes may be avoided. Lazy writes have their dangers, of
course, so a program can defeat them on a per-handle basis by setting the
write-through flag in the OpenMode parameter for DosOpen, or it can commit
data to disk on a per-handle basis with the DosBufReset function.

Fault Tolerance

The HPFS's extensive use of lazy writes makes it imperative for the HPFS to
be able to recover gracefully from write errors under any but the most dire
circumstances. After all, by the time a write is known to have failed, the
application has long since gone on its way under the illusion that it has
safely shipped the data into disk storage. The errors may be detected by the
hardware (such as a "sector not found" error returned by the disk adapter),
or they may be detected by the disk driver in spite of the hardware during a
read-after-write verification of the data.

The primary mechanism for handling write errors is called a hotfix. When an
error is detected, the file system takes a free block out of a reserved
hotfix pool, writes the data to that block, and updates the hotfix map. (The
hotfix map is simply a series of pairs of doublewords, with each pair
containing the number of a bad sector associated with the number of its
hotfix replacement. A pointer to the hotfix map is maintained in the
SpareBlock.) A copy of the hotfix map is then written to disk, and a warning
message is displayed to let the user know that all is not well with the disk
device.

Each time the file system requests a sector read or write from the disk
driver, it scans the hotfix map and replaces any bad sector numbers with the
corresponding good sector holding the actual data. This lookaside
translation of sector numbers is not as expensive as it sounds, since the
hotfix list need only be scanned when a sector is physically read or
written, not each time it is accessed in the cache.

One of CHKDSK's duties is to empty the hotfix map. For each replacement
block on the hotfix map, it allocates a new sector that is in a favorable
location for the file that owns the data, moves the data from the hotfix
block to the newly allocated sector, and updates the file's allocation
information (which may involve rebalancing allocation trees and other
elaborate operations). It then adds the bad sector to the bad block list,
releases the replacement sector back to the hotfix pool, deletes the hotfix
entry from the hotfix map, and writes the updated hotfix map to disk.

Of course, write errors that can be detected and fixed on the fly are not
the only calamity that can befall a file system. The HPFS designers also had
to consider the inevitable damage to be wreaked by power failures, program
crashes, malicious viruses and Trojan horses, and those users who turn off
the machine without selecting Shutdown in the Presentation Manager Shell.
(Shutdown notifies the file system to flush the disk cache, update
directories, and do whatever else is necessary to bring the disk to a
consistent state.)

The HPFS defends itself against the user who is too abrupt with the Big Red
Switch by maintaining a Dirty FS flag in the SpareBlock of each HPFS volume.
The flag is only cleared when all files on the volume have been closed and
all dirty buffers in the cache have been written out or, in the case of the
boot volume (since OS2.INI and the swap file are never closed), when
Shutdown has been selected and has completed its work.

During the OS/2 boot sequence, the file system inspects the DirtyFS flag on
each HPFS volume and, if the flag is set, will not allow further access to
that volume until CHKDSK has been run. If the DirtyFS flag is set on the
boot volume, the system will refuse to boot; the user must boot OS/2 in
maintenance mode from a diskette and run CHKDSK to check and possibly repair
the boot volume.

In the event of a truly major catastrophe, such as loss of the SuperBlock or
the root directory, the HPFS is designed to give data recovery the best
possible chance of success. Every type of crucial file
object--including Fnodes, allocation sectors, and directory
blocks--is doubly linked to both its parent and its children and
contains a unique 32-bit signature. Fnodes also contain the initial portion
of the name of their file or directory. Consequently, CHKDSK can rebuild an
entire volume by methodically scanning the disk for Fnodes, allocation
sectors, and directory blocks, using them to reconstruct the files and
directories and finally regenerating the freespace bitmaps.

Application Programs and the HPFS

Each of the OS/2 releases thus far have carried with them a major
discontinuity for application programmers who learned their trade in the
MS-DOS environment. In OS/2 1.0, such programmers were faced for the first
time with virtual memory, multitasking, interprocess communications, and the
protected mode restrictions on addressing and direct control of the hardware
and were challenged to master powerful new concepts such as threading and
dynamic linking. In OS/2 Version 1.1, the stakes were raised even further.
Programmers were offered a powerful hardware-independent graphical user
interface but had to restructure their applications drastically for an
event-driven environment based on objects and message passing.

In OS/2 Version 1.2, it is time for many of the file-oriented programming
habits and assumptions carried forward from the MS-DOS environment to fall
by the wayside. An application that wishes to take full advantage of the
HPFS must allow for long, free-form, mixed-case filenames and paths with few
restrictions on punctuation and must be sensitive to the presence of EAs and
ACLs. After all, if EAs are to be of any use, it won't suffice for
applications to update a file by renaming the old file and creating a new
one without also copying the EAs.

But the necessary changes for OS/2 Version 1.2 are not tricky to make. A new
API function, DosCopy, helps applications create backups--it
essentially duplicates an existing file together with its EAs.  EAs can also
be manipulated explicitly with DosQFileInfo, DosSetFileInfo, DosQPathInfo,
and DosSetPathInfo. A program should call DosQSysInfo at run time to find
the maximum possible path length for the system, and ensure that all buffers
used by DosChDir, DosQCurDir, and related functions are sufficiently large.
Similarly, the buffers used by DosOpen, DosMove, DosGetModName,
DosFindFirst, DosFindNext, and like functions must allow for longer
filenames. Any logic that folds cases in filenames or tests for the
occurrence of only one dot delimiter in a filename must be rethought or
eliminated.

The other changes in the API will not affect the average application. The
functions DosQFileInfo, DosFindFirst, and DosFindNext now return all three
sets of times and dates (created, last accessed, last modified) for a file
on an HPFS volume, but few programs are concerned with time and date stamps
anyway. DosQFsInfo is used to obtain volume labels or disk characteristics
just as before, and the use of DosSetFsInfo for volume labels is unchanged.
There are a few totally new API functions such as DosFsCtl (analogous to
DosDevIOCtl but used for communication between an application and an FSD),
DosFsAttach (a sort of explicit mount call), and DosQFsAttach (determines
which FSD owns a volume); these are intended mainly for use by disk utility
programs.

In order to prevent old OS/2 applications and MS-DOS applications running in
the DOS box from inadvertently damaging HPFS files, a new flag bit has been
defined in the EXE file header that indicates whether an application is
HPFS-aware. If this bit is not set, the application will only be able to
search for, open, or create files on HPFS volumes that are compatible with
the FAT file system's 8.3 naming conventions. If the bit is set, OS/2 allows
access to all files on an HPFS volume, because it assumes that the program
knows how to handle long, free-form filenames and will take the
responsibility of conserving a file's EAs and ACLs.

Summary

The HPFS solves all of the historical problems of the FAT file system. It
achieves excellent throughput even in extreme cases--many very small
files or a few very large files--by means of advanced data structures
and techniques such as intelligent caching, read-ahead, and write-behind.
Disk space is used economically because it is managed on a sector basis.
Existing application programs will need modification to take advantage of
the HPFS's support for extended attributes and long filenames, but these
changes will not be difficult. All application programs will benefit from
the HPFS's improved performance and decreased CPU use whether they are
modified or not. This article is based on a prerelease version of the HPFS
that was still undergoing modification and tuning. Therefore, the final
release of the HPFS may differ in some details from the description given
here--Ed.


B-Trees and B+ Trees

Most programmers are at least passingly familiar with the data structure
known as a binary tree. Binary trees are a technique for imposing a logical
ordering on a collection of data items by means of pointers, without regard
to the physical order of the data.

In a simple binary tree, each node contains some data, including a key value
that determines the node's logical position in the tree, as well as pointers
to the node's left and right subtrees. The node that begins the tree is
known as the root; the nodes that sit at the ends of the tree's branches are
sometimes called the leaves.

To find a particular piece of data, the binary tree is traversed from the
root. At each node, the desired key is compared with the node's key; if they
don't match, one branch of the node's subtree or another is selected based
on whether the desired key is less than or greater than the node's key. This
process continues until a match is found or an empty subtree is encountered
(see Figure A).

Such simple binary trees, although easy to understand and implement, have
disadvantages in practice. If keys are not well distributed or are added to
the tree in a non-random fashion, the tree can become quite asymmetric,
leading to wide variations in tree traversal times.

In order to make access times uniform, many programmers prefer a particular
type of balanced tree known as a B- Tree. For the purposes of this
discussion, the important points about a B-Tree are that data is stored in
all nodes, more than one data item might be stored in a node, and all of the
branches of the tree are of identical length (see Figure B).

The worst-case behavior of a B-Tree is predictable and much better than that
of a simple binary tree, but the maintenance of a B-Tree is correspondingly
more complex. Adding a new data item, changing a key value, or deleting a
data item may result in the splitting or merging of a node, which in turn
forces a cascade of other operations on the tree to rebalance it.

A B+ Tree is a specialized form of B-Tree that has two types of nodes:
internal, which only point to other nodes, and external, which contain the
actual data (see Figure C).

The advantage of a B+ Tree over a B- Tree is that the internal nodes of the
B+ Tree can hold many more decision values than the intermediate-level nodes
of a B-Tree, so the fan out of the tree is faster and the average length of
a branch is shorter. This makes up for the fact that you must always follow
a B+ Tree branch to its end to get the data for which you are looking,
whereas in a B-Tree you may discover the data at an intermediate node or
even at the root.

                        FAT File System         High Performance

                                                File System

Maximum filename        11 (in 8.3 format)      254

length

Number of dot (.)       One                     Multiple

delimiters allowed

File attributes         Bit flags               Bit flags plus up to

                                                64Kb of free-form ASCII

                                                or binary information

Maximum path            64                      260

length

Minimum disk space      Directory entry        Directory entry (length

overhead per file        (32 bytes)            varies) + Fnode

                                                (512 bytes)

Average wasted          1/2 cluster            1/2 sector (256 bytes)

space per file          (typically 2Kb

                          or more)

Minimum allocation      Cluster (typically     Sector (512 bytes)

unit                     4Kb or more)

Allocation info         Centralized in FAT     Located nearby each

for files                 on home track         file in its Fnode

Free disk space info    Centralized in FAT     Located near free

                          on home track          space in bitmaps

Free disk space         2Kb (1/2 cluster       4Kb (8 sectors)

described per byte       @ 8 sectors/cluster)

Directory structure     Unsorted linear list,  Sorted B-Tree

                          must be searched

                          exhaustively

Directory location      Root directory on      Localized near seek

                         home track, others     center of volume

                         scattered

Cache replacement       Simple LRU             Modified LRU,

strategy                                        sensitive to data type

                                                and usage history

Read-ahead              None in MS-DOS 3.3 or  Always present,

                         earlier, primitive     sensitive to data type

                         read-ahead optional    and usage history

                         in  MS-DOS 4

Write-behind            Not available          Used by default, but

                         can be defeated on

                         per-handle basis



Getting the Most from Expanded Memory with an EMS Function Library

Douglas Boling

The future of personal computing may ultimately belong to OS/2 operating
systems. There are, however, tens of millions of DOS1 operating system users
who are demanding more from their applications than ever before. To quench
the thirst of these users, application developers need to maximize the use
of the available resources of DOS-based machines. One of the resources that
must be maximized is the Lotus(R), Intel(R), Microsoft(R), AST(R) (LIM)
Expanded Memory Specification.

The Expanded Memory Specification, or EMS as it has become known, documents
a standard way to implement up to 32Mb of paged RAM in a DOS environment.
What is exciting about EMS is that it not only provides access to additional
RAM, it also introduces a method of managing that additional memory.

Expanded memory has undergone many revisions. As the specification has
matured, many of the basic tasks have been implemented in more than one
function. The job of the developer is to choose the proper function for each
situation.

This article discusses the groups of functions defined in EMS Version 4.0
and compares different functions performing similar tasks. It also presents
a library of routines that makes the use of expanded memory simpler.
Included in the toolkit are routines designed to implement functions of EMS
Version 4.0 using a Version 3.2 driver. All routines are written using the
Microsoft Macro Assembler (MASM) Version 5.0.

The functions designated by the EMS are implemented by a device driver
called the Expanded Memory Manager (EMM). These functions will be discussed
by functional category. The categories, which are listed in Figure 1, are
information, allocation, mapping, name support, save/restore mapping
context, operating system/environment, and warm boot. One function, Move
Exchange Memory Region (function 24), does not fit into any of the
categories, so it will be discussed separately.

Information Functions

The flexibility of the EMS means that it can be implemented in many
different ways. A particular implementation can vary in the number and
location of the physical pages, the size of the page frame segment, and the
amount of memory available. This variability makes it vitally important for
you to know the expanded memory environment of any application. The EMM has
nine functions that provide information about the expanded memory
environment (see Figure 1).

The first action an application should take after determining that the EMM
is loaded is to determine the status and version of the driver. Function 1,
Get Status, lets an application know that the EMM is functioning properly.
As with all functions, the status code is returned in AH. A value of 0
indicates the function was completed with no errors. The EMM version is
determined by calling function 7, Get Version. The version number is
returned as a BCD byte in AL.

The page frame segment can be found by calling Get Page Frame Address
(function 2). The page frame is an artifact of earlier versions of the EMS.
Versions before EMS 4.0 supported only four consecutive physical pages,
which were designated as the page frame. By calling function 2, an
application could locate all four physical pages. A physical page is 16Kb
and a page frame is a 64Kb location.

Version 4.0 allows physical pages to reside anywhere in the first 1Mb of
memory. In some instances, the EMM may not be able to find a free 64Kb block
to use for the page frame. In that circumstance, the EMM will return an
error when function 2 is called.

Because of the additional flexibility of physical pages in EMS 4.0, function
2 has been superseded by function 25, Get Mappable Physical Address Array.
Therefore when using EMS 4.0, function 25 is the preferred method of
determining the location of each physical page in the system. Function 25
returns an array containing the segment address and physical page number of
all physical pages in the system. The array is sorted by the segment address
in ascending order. This does not imply that the physical page numbers are
in ascending order.

Physical pages are numbered sequentially from page 0, located at the page
frame segment, continuing through the highest address. Any physical pages
mapped in conventional memory are numbered in order after pages above 640Kb
(see Figure 2). Physical pages mapped in conventional memory are mapped with
logical pages from the operating system handle (handle 0). This, however, is
not true across all EMMs. The physical page numbers are defined by their
call. Only pages 0-3 are guaranteed to be the same in all
implementations.

Function 3, Get Unallocated Page Count, returns the amount of expanded
memory in the system along with the amount of expanded memory that is
available for use. The ability to differentiate between not enough free
memory and not enough total memory allows an application to accurately
inform the user that other applications are using too much memory or that
the machine simply does not have enough memory.

Get Handle Pages (function 13) and Get All Handle Pages (function 14)
provide a way to determine how many pages are assigned to each handle.
Function 13 returns the number of pages assigned to a handle specified in
DX, whereas function 14 returns an array that contains the number of pages
assigned for all open handles. The array holds a two-word structure for each
open handle. The structure contains the handle value and the number of pages
assigned to that handle. The size of the array can be determined by calling
function 12, Get Handle Count, to see how many handles are currently open.

Function 26, Get Expanded Memory Hardware Information, shows how the
expanded memory is implemented. This function is officially considered an
operating system function and is therefore password protected. In reality,
there is no reason for applications to need the information returned by this
function. The function returns the raw page size, the context save area
size, the number of alternate mapping sets, and the number of DMA register
sets. The only item that an application might need is the size of the
context save area. That number, however, is available by calling function 15
subfunction 2. In general, applications should not use function 26 since it
is considered part of the operating system functions of the EMM.

Allocation Functions

Allocating memory can be done by calling either function 4, Allocate Pages,
or function 27, Allocate Standard/Raw Pages. With a few exceptions, both
functions perform identically. Function 4 requires that an application
request at least one logical page, whereas function 27 allows an application
to request a handle without reserving any pages. Requesting a handle with
zero pages is useful for applications that use the handle name features of
the EMM but do not need any additional memory.

Function 27 also allows applications to reserve "raw" pages for use. The EMS
3.2 functions are always 16Kb pages. All additional 4.0 functions use raw
pages. Each EMM has a raw page size of 16Kb. The size of the raw page is not
defined by EMS 4.0 except that it is smaller than the standard page. Since
the only way to determine the raw page size is to use the restricted
function 26, it is advisable to avoid using raw pages.

The Reallocate Pages function (function 18) lets an application adjust the
number of pages reserved by a handle. This function enables applications to
use expanded memory more efficiently since memory can be reserved as it is
needed instead of all at once. If the number of pages is being reduced, the
EMM uses function 6, Deallocate Pages, which deallocates logical pages
starting with the highest logical page number. If the request is to increase
the number of pages, the new page numbers are added sequentially after the
highest logical page number. Because of this allocation strategy,
applications expecting to use this function should use the lower numbered
logical pages for resident code and permanent data.

Mapping Functions

Some of the most welcome enhancements made in Version 4.0 are the new
methods for mapping logical pages, especially since this task is made more
difficult now that EMS 4.0 allows more than four physical pages. Under EMS
3.2, the only way to map a page of memory was to call function 5, Map/Unmap
Handle Page, to map one logical page to one physical page. Function 5 is
still available under EMS 4.0; additionally, three new functions have been
added to make logical page mapping easier.

Function 17, Map/Unmap Multiple Handle Pages, allows an application to map
more than one logical page in one call. A convenience of function 17 is the
ability to select the physical page by its segment address instead of the
physical page number. The segment address for each page is found using
function 25. Logical pages can be mapped to physical pages located anywhere
in memory; mapping pages in conventional memory, however, requires some
caution.

Physical pages located in conventional memory are subject to DOS memory
management procedures. Applications must use DOS to allocate the block of
memory where the physical page is located before that page can be remapped.
Before a block of memory is released to DOS, the original mapping of that
page must be restored. To restore the mapping, applications can either save
and restore the mapping context of the pages or use the mapping functions to
restore the mapping of the operating system logical pages. The logical pages
mapped in conventional memory are owned by the operating system handle
(handle 0). The pages are mapped so that the lowest logical page number is
mapped to the lowest addressed physical page.

An interesting feature of function 17 is the ability to unmap a logical
page. Under some conditions it is advisable for applications to restrict
access to their expanded memory pages. By specifying the logical page FFFFh
to be mapped to a physical page, the logical page currently being mapped at
that physical page is unmapped without replacing it with another logical
page. The unmapped page can be remapped by using the standard map page
functions or by restoring a context in which the page was previously mapped.

Two additional mapping functions available in Version 4.0 are Alter Page Map
and Jump (function 22) and Alter Page Map and Call (function 23). These
functions enable an application to execute a module of code with the
expanded memory page map initialized to a known state. These functions are
especially useful in size-critical applications.

Alter Page Map and Jump transfers control to a specified address after
mapping logical pages listed in a mapping array. The control is transferred
to the new address, with the state of the flags and the contents of all
registers except AX being retained. Likewise, Alter Page Map and Call calls
a subroutine after mapping a set of logical pages. On return from the
subroutine, the EMM maps a second set of logical pages specified in another
mapping array. The state of the flags and the contents of all registers
except AX are retained across both the call and the return.

The Alter Page Map and Call description in the EMS 4.0 documentation is
ambiguous about one point, thus causing some confusion about the specified
function of Alter Page Map and Call. The EMS 4.0 specification compares the
function to a far call in 8086 assembly code, stating that the context of
the page map is saved and restored before and after the call. The
description of the calling parameters, however, makes no mention of the save
and restore, only that the page map can be altered using the two map
multiple structures referenced in the call.

Because of this some board manufacturers have interpreted this function as
not performing a page map save before the subroutine is called and therefore
do not save and restore the mapping context during this function. Therefore,
while the specification states that the context of the page map is saved,
most drivers do not implement this portion of function 23. Developers must
be careful not to assume that the context of the page map is saved by this
function--applications should not assume that the mapping context has
been saved before the subroutine call.

Name Support Functions

The handle name support added in EMS 4.0 provides a method of sharing
expanded memory pages among different applications. Families of applications
can use a standard handle name for pages that are shared among the
applications.

Get/Set Handle Name, Function 20, assigns a name consisting of an 8-byte
string provided by the application to handles. The values of the bytes can
range from 0 to 256. A handle name of all binary zeros is considered not to
have a name. Handle names are cleared when first assigned or reassigned by
the EMM. The name of a handle can be read by calling the function Get/Set
Handle Name. Handle names must be unique; if an attempt is made to assign a
name that is currently being used by another handle, the EMM will reject the
name assignment. When a handle is deallocated, its name is then  reset to
zeros.

To determine the names of all handles in the system, use function 21, Get
Handle Directory. Subfunction 0 returns an array containing all active
handles and their associated names. Handles without names have a name field
containing zeros. The size of the array varies with the number of active
handles. To determine the size of the array, multiply the number of open
handles supported by the EMM by nine. The number of open handles is returned
by Get Handle Count (function 12).

To determine the handle for a particular name, an application can call the
Search for Named Handle subfunction (function 21, subfunction 1). This
subfunction gives applications a convenient way to determine if a handle
exists for a shared memory application.

Save/Restore Functions

Any application that can interrupt another application must always save and
restore the mapping context of the EMM. Some examples are interrupt
handlers, device drivers, and resident utilities. There are three ways for
applications to save and restore the mapping context under EMS 4.0.

The simplest way to save and restore the mapping context is to use the
matching functions Save Page Map (function 8) and Restore Page Map (function
9). Unlike the methods used by functions 15 and 16 (discussed below),
functions 8 and 9 do not require the application to provide a space for the
mapping register contents. To use these functions, an application simply
calls function 8 to save the mapping context and function 9 to restore the
context.

There are some limitations to functions 8 and 9. First, these functions only
save the mapping contexts of the 64Kb page frame whose address is returned
by function 2. If physical pages outside the page frame are changed, one of
the other save/restore functions must be used. Second, because the context
is saved within the EMM, the number of saved mapping contexts that can be
stored at one time is limited. When an application receives the save area
full error, it must either wait until another time or use one of the other
save/restore functions. Third, to keep track of multiple context saves,
functions 8 and 9 require that an application provide a handle. Finally, an
application can only save one mapping context at a time with function 8.
Before an application can use function 8 again, it must call function 9 to
restore the page map.

Function 15, Get/Set Page Map, provides a way to save the complete mapping
context of the EMM. This function avoids some of the pitfalls of the
function 8/9 combination by requiring that the application provide the
context save area. Since the application controls the save area, it has the
responsibility of keeping track of separate context saves. By requiring the
application to provide the save area, the EMM can be more lenient in the
rules regarding the use of function 15. An application does not need a
handle to use function 15 nor is an application restricted to one context
save at a time. As long as the application provides different save areas for
each call to function 15, it can continue to nest context saves.

Function 16, Get/Set Partial Page Map, provides all of the flexibility of
function 15 without the overhead that may be required by it. Since function
15 saves the entire mapping context of the EMM, the save area required may
be quite large and the function may take a relatively long time to execute.
Using function 16, an application can specify only the pages that are
necessary to save. Since an application usually requires only a limited
number of the physical pages, function 16 provides a way to customize each
context save for minimum overhead.

Warm Boot Functions

The functions Get/Set Handle Attribute (function 19) and Prepare Expanded
Memory Hardware for Warm Boot (function 29) provide support for expanded
memory hardware that will retain information across a warm boot (WB).
Imagine that there are unmapped pages below 640Kb. In that case, a warm boot
will cause the system to crash. The WB call, however, lets the EMM put the
board in a state that will survive a WB.

Move Functions

One of the more powerful functions in EMS 4.0 is Move/Exchange Memory Region
(function 24). The move memory function is useful for applications that use
expanded memory for storing blocks of data. The function simply requires an
application to specify the source block, the destination block, and the size
of the move. Function 24 does not alter the current mapping context.

The move memory function allows an application to move blocks of data from
expanded to expanded, expanded to conventional, conventional to expanded, or
conventional to conventional memory. If the source and destination regions
overlap, the move direction will be chosen to ensure that the destination
receives the source region intact. The exchange memory subfunction performs
an exchange of data regions. Unlike the move memory function, the exchange
memory subfunction does not allow the two data regions to overlap.

Operating Systems/Environment

Two functions, Alternate Map Register Set (function 28) and Enable/Disable
OS/E (function 30), allow an operating system to enhance the function of the
expanded memory hardware and manager. These functions, along with function
26, Get Expanded Memory Hardware Information, are designated operating
system/environment functions and should never be used by an application.

To ensure operating system control over these functions, each of them can be
password protected. By calling Disable OS/E Functions (function 30,
subfunction 0), an operating system or environment enables the password
protection of functions 26, 28, and 30. The first time this function is
called, the EMM returns a password to the operating system. Until the
password is returned to the EMM, any call to functions 26, 28, or 30 must
use that password to access those functions. The EMM is required by the EMS
to use a randomly generated password, since using a fixed value would defeat
the purpose of the password protection. The password protection is disabled
using the Enable OS/E function (function 30, subfunction 1).

The Alternate Map Register Set function (function 28) allows an operating
system to enhance the context-switching ability of the EMM. With nine
subfunctions, function 28 is one of the more complex functions defined by
EMS 4.0.

EMS-compatible memory boards can have multiple sets of mapping registers. By
changing the active map register set, an operating system or environment can
quickly change the context using hardware instead of slowly saving and
loading each map register. Function 28 provides direct control of which map
register set is active. In addition, it provides a method for simulating map
register sets in a way similar to the Get/Set Page Map function.

Function 28 also provides control over an additional set of mapping
registers devoted to DMA accesses to expanded memory. The DMA mapping
registers provide a way for an operating system to switch to another task
while waiting for a DMA process to be completed.

EMS Simulators

Because of the usefulness of memory management features of expanded memory,
many software products have appeared on the market that use PC/AT type
extended memory to emulate expanded memory. These simulators are handy since
they impose the EMS memory management features on extended memory that,
until recently, had no documented memory management guidelines. There are
two types of these EMS stimulators.

One EMS simulator is a pure software implementation that uses reserved areas
in conventional RAM as physical pages. To "map" a logical page, the
simulator copies the data from extended memory. Because the logical page
data is copied from extended RAM, this type of simulator is relatively slow.
Another problem is that this simulator cannot support data aliasing.

Data aliasing is a feature that is implemented by addressing the same block
of RAM in two different address spaces. It can be seen in expanded memory by
mapping the same logical page to two different physical pages. If aliasing
is supported, changing a byte in one physical page will automatically change
the identical byte in the other physical page. Since the "copy type" EMS
simulators copy data to each physical page when the page is mapped, there is
no way for the data to be simultaneously updated in both copies of a logical
page.

Using the paging registers in the Intel 386, an EMM driver can be written
that can implement all the EMS 4.0 functions. Because the
logical-to-physical page mapping is done in hardware instead of simply
copying the data, this type of simulator is fast and supports data aliasing.

EMS Routines

The library of routines in Figure 3 is designed to facilitate the use of
expanded memory. GetEMSMem, SaveEMSMem, RestoreEMSMem, and FreeEMSMem
provide all the basic functions needed for an application to use expanded
memory. CheckAlias and MapbyName are handy for advanced applications.
Finally, EMS32Upgrade implements functions 17, 22, and 23 of the EMS 4.0
using Version 3.2 calls.

The GetEMSMem routine takes care of the overhead involved in requesting
expanded memory. This routine determines if expanded memory is present,
finds the location and size of the page frame, requests memory, maps it into
the page frame, and names the handle.

Determining if the EMM is loaded is the first task that any program using
expanded memory must perform. The recommended technique for determining the
presence of the EMM is to interrogate the EMM device driver as you would any
other DOS device. The idea is to use the DOS file open and DOS I/O control
commands to determine if a device with the name EMMXXXX0 is available and
ready. Another technique for finding the EMM is to determine if the
interrupt 67h vector is pointing to the segment of a device driver with the
name EMMXXXX0.

The FindEMM routine uses both of these methods for finding the driver.
Whereas the Open Handle technique is the more formal of the two methods
because it uses only DOS commands, the Get Interrupt Vector technique is
actually simpler. One caution: if the application is a device driver or a
TSR interrupting a DOS command, the Get Interrupt Vector technique must be
used since DOS file commands are not available in these situations.

After the requested number of logical pages are reserved using the Allocate
Pages function (function 4), the size of the page frame is determined. If
the EMM is Version 3.2, the page frame segment is assumed to be four pages.
For Version 4.0, the Get Mappable Physical Address Array function (function
25) is called to determine the segments of all physical pages. The array is
searched to find physical page 0, which is the start of the page frame. The
routine then counts the number of consecutive page segments.

GetEMSMem maps the logical pages to the page frame. If there are more
logical pages than physical pages in the page frame, then only enough pages
are mapped to fill the page frame. Finally, if a name was passed to the
routine, the Get/Set Handle Name function (function 20) is called to set the
handle name.

The SaveEMSMem and RestoreEMSMem routines combine the simplicity of EMS
functions 8 and 9 with the flexibility of function 15. The advantage of
using the combination SaveEMSMem/RestoreEMSMem is that these routines keep
track of multiple saves and restores while allowing the application to
define the amount of memory for the save data.

The routines keep track of separate saves by returning an index value to the
caller after each call to SaveEMSMem. To restore the mapping context, call
RestoreEMSMem with the index value provided by the SaveEMSMem call that is
stored in BX.

The routines use the Get/Set Page Map function (function 15) to perform the
context save and restore. To keep track of the different save arrays, the
data buffer is organized as a linked list. Each item in the list contains
the context save area's length, index value, and the save array.

FreeEMSMem is a simple routine that deallocates an expanded memory handle
and all logical pages assigned to that handle. Although the routine does
nothing but call EMS Deallocate Pages (function 6), it is included for
completeness.

The CheckAlias routine implements a simple technique for determining if the
EMM supports data aliasing. The routine calls the EMM to allocate one
logical page, then maps the page to two different physical pages. One
physical page is modified and then compared to the other page. If the second
page is modified by the write, then data aliasing is supported.

MapbyName is a simple but handy routine that maps a logical page by
specifying the name of a handle instead of the handle itself. The routine is
implemented by two calls to the EMM. The first is a call to search for the
handle given the name pointed to by DS:SI. The second is a call to the map
page function using the handle returned by the first call.

Driver Routines

The added functionality of the 4.0 specification can be quite useful, so it
is a shame to write applications for the lowest common denominator. But
writing applications that require 4.0 drivers can limit the available
customer base. The solution is to write routines that implement some of the
features of EMS 4.0 using EMS 3.2 functions. In addition to the EMS
routines, Figure 3 has a collection of routines that help bridge the gap
between EMS  3.2 and EMS 4.0 drivers.

The routines in Figure 3 implement the Map Multiple, Map and Jump, and Map
and Call functions of the EMS 4.0 driver. Each routine duplicates the
application interface used by its respective EMS 4.0 functions. This way
applications can easily migrate away from these routines as the use of EMS
3.2 drivers diminishes.

A dispatch routine EMS32Upgrade is included to call each of the emulation
functions by their EMS 4.0 function numbers. The routine also calls
functions 1 though 15 of EMS 3.2. The status code is returned in AH as it is
when calling the EMM directly. If an EMS 4.0 routine is found, EMS32Upgrade
simply passes the function call to the EMS 4.0 driver.

The EMSMapMultiple routine provides the convenient function of mapping many
pages with one call. MapMultiple, like function 17 in EMS 4.0, is called
with the function and subfunction values in AX, a handle value in DX, the
number of pages to be mapped in CX, and DS:SI pointing to a
logical-to-physical map array. The array is composed of repeating structures
containing a logical page number followed by a physical page number or
physical page segment. Register AH should contain 50h to correspond to the
function value in the EMS 4.0 function. AL contains the subfunction
value--0 for structures containing the physical page numbers and 1 for
structures containing physical page segments.

The actual mapping is performed by calling function 5, Map/Unmap Handle
Page. MapMultiple must convert physical page segments into page numbers
since function 5 does not recognize page segments. To convert the physical
page segment address to a page number, the routine subtracts the page frame
segment address from the physical page segment, then divides the address by
the size of a segment. The result of the divide is checked to ensure that
there is no remainder and that the resulting quotient is between 0 and 3. If
either of these two requirements is  not met, the segment address is
considered to be invalid and the routine terminates.

The EMSMapandJump routine provides a way for an application to jump to a
routine with the physical page map set to a known value. The routine is
called with the AH containing the function value 55h, the handle in DX, and
DS:SI pointing to the map and jump structure. AL contains a selector
indicating whether the physical pages in the mapping array are selected by
page number or by segment address. As in the MapMultiple routine, a value of
0 in AL indicates page number and a value of 1 indicates page segment
address.

The map and jump structure contains a far pointer to the destination address
in segment offset format followed by a byte containing the number of pages
to map and a far pointer to the logical-to-physical map array. The map array
is in the same format as in the MapMultiple routine.

Since the mapping structure of the MapandJump function is the same as the
MapMultiple function, the MapMultiple routine is simply called to provide
the mapping function of the MapandJump routine. To exit the procedure
without disturbing the stack, the jump is performed by altering the return
address of the call to EMS32Upgrade. This way the MapandJump routine returns
to EMS32Upgrade, which performs a far return to the new address. One
caution, the technique of altering the stack does not allow this routine to
be called directly.

The EMSMapandCall routine provides a way for an application to jump to a
routine with the physical page map set to a known value. The routine is
called with the AH containing the function value 56h, the handle in DX, and
DS:SI pointing to the map and call structure. As before, AL contains the
physical page selector--0 for physical page number and 1 for segment
address.

The map and call structure is more complex than the map and jump structure
since the map and call function provides two separate mappings, one before
the call and one after the return. The structure contains a far pointer to
the subroutine, a byte containing the number of pages to map before the
call, a far pointer to the mapping structure used before the call, a byte
with the number of pages to map after the return, and a far pointer to the
mapping structure to use after the return. As before, the map array is in
the same format as in the MapMultiple routine.

EXAMPLE.ASM (see Figure 4) is a simple program written to show the utility
of the toolbox routines. The program, a simple TSR, captures an image of the
screen so it can be recalled later. The saved screen buffer is stored in
expanded memory to conserve conventional memory space, which is always
shorter than necessary. To assemble EXAMPLE.EXE, use the following three
statements:

MASM EXAMPLE
MASM EMSTOOLS
LINK EXAMPLE EMSTOOLS

The simplicity of EXAMPLE shows how easy it is to use expanded memory. Only
four calls are made in order to use all of the expanded memory functions
required by the TSR. The call to GetEMSMem checks for the driver and
reserves the memory. SaveEMSMem and RestoreEMSMem are used by the resident
routine to preserve the state of the page frame. Finally, one call is made
directly to the EMM to map the expanded memory page containing the stored
data. In a more complex program, the other toolbox routines can be used with
the same ease as the routines used by EXAMPLE.EXE.

As this article has demonstrated, the Expanded Memory Specification has
evolved into a powerful tool for the DOS applications developer. The
additional memory available, along with the functions to manage this memory,
enable developers to continue to create full-featured applications for the
DOS environment. The library of routines presented here provides some
enhancements to the the functionality of EMS 4.0. The most powerful expanded
memory tools, however, are the functions of EMS 4.0.

Figure 1

Information

 1.  Get Status

 2.  Get Page Frame Address

 3. Get Unallocated Page Count

 7. Get Version

 12.  Get Handle Count

 13. Get Handle Pages

 14. Get All Handle Pages

 25. Get Mappable Physical Address Array

 26. Get Expanded Memory Hardware Information (OpSys Function)

Allocation

 4. Allocate Pages

 6. Deallocate Pages

 18. Reallocate Pages

 27. Allocate Standard/Raw Pages

Mapping

 5. Map/Unmap Handle Page

 17. Map/Unmap Multiple Handle Pages

 22. Alter Page Map and Jump

 23. Alter Page Map and Call

Name Support

 20. Get/Set Handle Name

 21. Get Handle Directory

Save/Restore Mapping Context

 8. Save Page Map

 9. Restore Page Map

 15. Get/Set Page Map

 16. Get/Set Partial Page Map

Operating System/Environment

 28. Alternate Map Register Set

 30. Enable/Disable OS/E

Warm Boot

 19. Get/Set Handle Attribute

 29. Prepare Expanded Memory Hardware for Warm Boot

Move

 24. Move/Exchange Memory Region

Figure 3

;====================================================================
;            Assembly language EMS routines
;            (c) 1989 Microsoft Corporation
;====================================================================

        .MODEL    small

        .DATA

ems_header      db          "EMMXXXX0",0    ;ASCIIZ EMS driver name
fun25_array    dw    96 dup (0)    ;Array for physical page data
fun25_array_end    =          $
ems32up_version    db    0      ;Version number for dispatcher
ems32up_calltbl    dw    offset EMSMapMultiple
        dw    offset EMSMapandJump
        dw    offset EMSMapandCall

        .CODE

        PUBLIC    GetEMSMem
        PUBLIC    SaveEMSMem
        PUBLIC    RestoreEMSMem
        PUBLIC    FreeEMSMem
        PUBLIC    CheckAlias
        PUBLIC    MapbyName
        PUBLIC    EMS32Upgrade

;====================================================================
;GetEMSMem reserves the amount of expanded memory requested in BX
; Entry: BX - Amount of memory to reserve
;        SI - Pointer to name to assign to handle in ASCIIZ format
;             Set SI = -1 for no name
; Exit:  AL - EMM Version
;        BX - Segment of page frame
;        CX - Size of page frame
;        DX - EMS handle.
;====================================================================
GetEMSMem       proc
        push    bp
        mov    bp,sp
        push    ax
        push    bx
        push    cx
        push    dx
        push    di
        push    si
        push    es
;--------------------------------------------------------------------
;See if EMS driver installed.
;--------------------------------------------------------------------
        call    FindEMS    ;See if EMM is loaded
        or    ah,ah     ;Check for error
         jne     getp_error    ;RC <> 0, error
;--------------------------------------------------------------------
;Reserve pages.
;--------------------------------------------------------------------
        mov    bx,[bp-4]    ;Get number of pages to res
        mov    ah,43h    ;EMS Allocate pages function
        int    67h    ;Call EMS Driver
        or    ah,ah    ;Check for error
        jne     getp_error    ;If error, exit
        mov    [bp-8],dx    ;Save handle
;--------------------------------------------------------------------
;Get version.
;--------------------------------------------------------------------
        mov     ah,46h    ;Get expanded memory version
        int     67h
        or      ah,ah
        jne     getp_error
        mov    [bp-2],al    ;Save version
        mov    dl,al

;---------------------------------------------------------------------
;See how many physical pages and the number of pages in the page frame
;---------------------------------------------------------------------

        mov    cx,4    ;Assume EMM ver 3.2, 4 pages
        mov    bx,cx    ;Save number of pages in frame
        cmp    dl,40h    ;Check for version 4.0
        jb    getp_continue     ;If before 4.0, use 4 pages
        mov    ax,5801h    ;Get number of entries in the
        int    67h    ;map array
        or    ah,ah    ;Check for error
        jne    getp_error
        mov    ax,cx    ;Copy number of entries
        sal    ax,1    ;Mult number of entries by 4
        sal    ax,1
        cmp    ax,offset fun25_array_end - offset
            fun25_array
        jb    getp_2
        mov    ah,0f2h    ;Load array too large RC
        jmp     getp_exit
getp_2:
        mov    bx,cx    ;Copy number of physical pages
        mov    ax,ds    ;Set ES = DS
        mov    es,ax
        mov    di,offset fun25_array
        mov    ax,5800h    ;Get mappable page array
        int    67h    ;Call driver
        or    ah,ah
        jne    getp_error
        xor    ax,ax
getp_loop2:
        cmp    ax,[di+2]    ;Search list for physical page
        je    getp_3    ;zero. That is the     start of
        add    di,4    ;the page frame
        loop    getp_loop2
        mov    ah,0f3h    ;Page frame not found in list
getp_error:
        jmp    getp_exit
getp_3:
        xor    bx,bx    ;Zero consecutive page count
getp_loop3:
        inc    bx    ;Count the number of
        mov    dx,[di]    ;consecutive segments
        add    dx,1024    ;Add num of paragraphs per seg
        cmp    dx,[di+4]    ;Compare with next segment
        jne    getp_continue    ;If not consecutive, exit
        add    di,4    ;Point to next entry
        loop    getp_loop3
getp_continue:
        mov     [bp-6],bx    ;Put page frame size into
        mov    cx,[bp-4]    ;stack and get num of pages
;--------------------------------------------------------------------
;Map pages into page frame
;--------------------------------------------------------------------
        cmp    cx,bx    ;See if number of pages larger
        jb    getp_4    ;than the size of the frame
        mov    cx,bx    ;Use the page frame size
getp_4:
        xor    bx,bx    ;Start with logical page 0
        mov    dx,[bp-8]    ;Get back handle
getp_loop4:
        mov     al,bl    ;at physical page 0
        mov    ah,44h    ;EMS Map Page function
        int     67h    ;Call EMS Driver
        or    ah,ah    ;Check for error
        jne     getp_exit    ;If error, exit
        inc    bx    ;Point to next logical page
        loop    getp_loop4
;--------------------------------------------------------------------
;Get page frame segment
;--------------------------------------------------------------------
        mov    ah,41h    ;EMS Get page frame segment
        int    67h    ;Call EMS Driver
        or    ah,ah    ;Check for error
        jne     getp_exit    ;If error, exit
        mov    [bp-4],bx    ;Save page frame segment
;--------------------------------------------------------------------
;Set the handle name
;--------------------------------------------------------------------
        xor    ax,ax    ;Zero return code
        mov    si,[bp-12]    ;Get back name pointer
        cmp    si,-1     ;if -1, no name
        je    getp_exit
        mov    dx,[bp-8]    ;Get handle
        mov    ax,5301h    ;EMS Set Handle Name
        int    67h    ;Call EMS Driver
        or    ah,ah    ;Check for error
        jne     getp_exit    ;If error, exit
getp_exit:
        mov    [bp-1],ah    ;Save return code on stack
        pop    es
         pop    si
        pop    di
        pop    dx
        pop    cx
        pop    bx
        pop    ax
        pop    bp
        ret
GetEMSMem    endp

;====================================================================
;SaveEMSMem Saves the context of EMS driver
; Entry:    CX - Size of save area in bytes
;        ES:DI - Pointer to save area
; Exit:     AH - Return code
;           DX - Index, used to identify saved state
;====================================================================
SaveEMSMem    proc
        push    bx
        push    cx
        push    di
        mov    ax,4e03h    ;Get save area size
        int    67h
        xor    ah,ah    ;Clear top byte of save size
        add    ax,4
        mov    bx,di    ;Copy buffer pointer
        cmp    word ptr es:[di],4244h
                ;Check for initialized area
        je    save_1
        mov    es:[di],4244h    ;Load signature
        xor    dx,dx
        mov    es:[di+4],dx    ;Init first index
        dec    dx
        mov    es:[di+2],dx    ;Init queue end pointer
save_1:
;--------------------------------------------------------------------
;Find the first free area in the save buffer
;--------------------------------------------------------------------
        inc    di
        inc    di
        add    cx,bx    ;Add base address to size but
        sub    cx,ax    ;subtract entry size
        xor    dx,dx
save_2:
        cmp    dx,es:[di+2]    ;Find max value of index
        jae    save_3    ;in the queue
        mov    dx,es:[di+2]
save_3:
        cmp    word ptr es:[di],-1
                ;If = -1 then this is the
        je    save_4    ;last entry.
        add    di,es:[di]    ;Point to next entry
        jc    save_areafull    ;Check for end of segment and
        cmp    di,cx    ;end of buffer
        ja    save_areafull
        jmp    short save_2
save_4:
        add    di,4    ;Move past queue overhead
        inc    dx    ;Create new index
        push    ax
        mov    ax,4e00h    ;Save page map
        int    67h
        or    ah,ah
        pop    ax
        jne    save_exit
        mov    es:[di-4],ax    ;Update queue pointers
        mov    es:[di-2],dx
        add    di,ax
        sub    di,4
        mov    word ptr es:[di],-1
save_exit:
        pop    di
        pop    cx
        pop    bx
        ret
save_areafull:
        mov    ah,8ch    ;Save area full
        jmp    short save_exit
SaveEMSMem    endp

;====================================================================
;RestoreEMSMem Restore the context of EMS driver
; Entry:    DX - Index, value returned by SaveEMSMem when state saved
;        DS:SI - Pointer to save area
; Exit:     AH - Return code
;====================================================================
RestoreEMSMem    proc
        pushf
        push    cx
        push    di
        push    si
        mov    ax,[si]    ;Check for initialized area
        cmp    ax,4244h
        jne    restore_bad
        inc    si    ;Point to first entry
        inc    si
restore_1:
        cmp    [si+2],dx    ;Find index value
        je    restore_2
        cmp    word ptr [si],-1
                ;See if at end of queue
        je    restore_bad
        add    si,[si]
        jc    restore_bad    ;If overflow segment, error
        jmp    short restore_1
restore_2:
        mov    di,si    ;Copy entry
        add    si,4    ;Point to save array
        mov    ax,4e01h    ;Restore page map
        int    67h
        or    ah,ah
         jne    restore_exit
        sub    si,4
        add    si,[di]
restore_3:
        cmp    word ptr [si],-1
        je    restore_4
        mov    cx,[di]    ;Load size
        cld
        rep    movsb    ;Copy next entry
        jmp    short restore_3
restore_4:
        mov    word ptr [di],-1
restore_exit:
        pop    si
        pop    di
        pop    cx
        popf
        ret
restore_bad:
        mov    ah,0a3h
        jmp    short restore_exit
RestoreEMSMem    endp

;====================================================================
;FreeEMSMem Deallocates EMS memory
; Entry:   DX - handle to deallocate
;====================================================================
FreeEMSMem    proc
        mov    ah,45h    ;Deallocate pages
        int    67h
        ret
FreeEMSMem    endp

;====================================================================
;CheckAlias Checks for memory aliasing support by EMS driver
; Exit:   AH - return code
;         AL - 1 Aliasing not supported, 0 - Aliasing supported
;====================================================================
CheckAlias    proc
        pushf
        push    bx
        push    cx
        push    dx
        push    di
        mov    ah,41h    ;Get page frame segment
        int    67h
        or    ah,ah
        jne    chkalias_exit1
        mov    cx,bx    ;Copy page frame
;Allocate 1 logical page
        mov    ah,43h    ;EMS Allocate page
        mov    bx,1
        int    67h
        or    ah,ah
         jne    chkalias_exit1
;Map logical page to physical pages 0 and 1
        mov    ax,4400h    ;Map to page 0
        mov    bx,0    ;Logical page 0
        int    67h
        or    ah,ah
        jne    chkalias_exit
        mov    ax,4401h    ;Map to page 1
        mov    bx,0    ;Logical page 0
        int    67h
        or    ah,ah
        jne    chkalias_exit
;Write pattern to physical page 0, then compare to page 1
        xor     bl,bl     ;Clear alias flag
        push    es
        mov    es,cx
        add    cx,400h
        push    cx
        mov    di,100h    ;Point 256 bytes inside page
        cld
        mov    al,5ah    ;Write to page 0 then check
        mov    cx,8    ;for aliasing by scanning
        rep    stosb    ;for the same pattern on
        mov    cx,8    ;page 1
        pop    es
        mov    di,100h
        repe    scasb
        pop    es
        je     chkalias_exit
        inc    bl
chkalias_exit:
        mov    ah,45h    ;Deallocate pages
        int    67h
        mov    al,bl    ;Copy alias flag
chkalias_exit1:
        pop    di
        pop    dx
        pop    cx
        pop    bx
        popf
        ret
chkalias_error:
        mov    ax,8000h    ;Indicate SW error
        jmp    short chkalias_exit1
CheckAlias    endp

;====================================================================
;MapbyName Map expanded memory pages by name
; Entry:   AL - Physical page
;          BX - Logical page to map
;       DS:SI - Pointer to Handle name in ASCIIZ format
; Exit:    AH - Return code
;          DX - EMS Handle
;====================================================================
 MapbyName    proc
        push    cx
        mov    cx,ax    ;Save physical page number
        mov    ax,5401h    ;EMS find handle function
        int    67h    ;Call EMS driver
        or    ah,ah    ;Check for error
        jne    mbn_exit    ;If error, exit
        mov    al,cl    ;Get back physical page
        mov    ah,44h    ;EMS Map page function
        int    67h    ;Call EMS driver
        pop    cx
mbn_exit:
        ret
MapbyName    endp

;====================================================================
; FindEMS determines if the EMM is loaded
;====================================================================
FindEMS        proc
        call    check_ems_hdl    ;Check for driver using
        jnc    find_1    ;open handle technique
        rcl    ah,1    ;If not enough handles,
        jc     find_driver_error
                ;try interrupt technique
        call    check_ems_int
        jnc    find_1    ;If manager found, continue
find_driver_error:
        mov     ah,0f1h    ;Set error code
        jmp    short find_exit
find_1:
        mov    ah,40h    ;Check status
        int    67h
find_exit:
        ret
FindEMS        endp

;====================================================================
; Test for EMS driver (Open Handle Technique)
;====================================================================
check_ems_hdl    proc
        mov     dx,offset ems_header
        mov     ax,3d00h    ;Open device read only
        int    21h
        jnc    cemsh_device_open
        cmp    ah,4    ;If file or path not found
        jb    cemsh_not_found
                ;no EMS driver
        mov    ah,01h    ;Too many open handles or
        jmp    short cemsh_error
                ;access denied
cemsh_device_open:
        mov    bx,ax    ;Copy handle
        mov    ax,4400h    ;Get device information
        int    21h
        jc    cemsh_not_foundclose
        test    dx,80h    ;Test file/device bit
        je    cemsh_not_foundclose
                ;0 = file not device
        mov    ax,4407h    ;Get output status
        int    21h
        jc    cemsh_not_foundclose
                ;If error, no device
        cmp    al,0ffh    ;Check for ready status
        jne    cemsh_not_foundclose
                ;If not ready, no device
        mov    ah,3eh    ;Close handle
        int    21h    ;BX contains handle
        clc
cemsh_exit:
        ret
cemsh_not_foundclose:
        mov    ah,3eh    ;Close handle
        int    21h    ;BX contains handle
cemsh_not_found:
        mov    ah,81h    ;Not found error code
cemsh_error:
        stc
        jmp    short cemsh_exit
check_ems_hdl    endp

;====================================================================
; Test for EMS driver (Get Interrupt technique)
;====================================================================
check_ems_int    proc
        push    es    ;Test for ems driver
        push    di
        mov    ax,3567h    ;Get EMS vector
        int    21h
        mov    di,0ah    ;Using the segment from the
        mov    si,offset ems_header
                ;67h vector, look at offset
        mov    cx,8    ;0ah. Compare the next 8
        cld        ;bytes with the expected
        repe    cmpsb    ;EMS header. If they are
        pop    di    ;the same, EMS driver
        pop    es    ;found
        jne    cemsi_error
        clc
cemsi_exit:
        ret
cemsi_error:
        stc
        jmp    short cemsi_exit
check_ems_int    endp


;====================================================================
;EMS32Upgrade
; Entry: AX - EMS 4.0 compatible function number
;        Other registers configured as needed by the function
; Exit:  AH - Return code
;====================================================================
EMS32Upgrade    proc    far
        pushf
        push    bp
         mov    bp,sp
        push    ax    ;Save function number
        mov    al,EMS32up_version
                ;Get version
        or    al,al    ;See if initialized
        jne    ems32_2    ;Yes, version already found
        mov    ah,46h    ;EMS Get version
        int    67h    ;Call EMM
        or    ah,ah    ;Check for error
        jne    short ems32_exit
        mov    EMS32up_version,al
                ;Set version
ems32_2:
        cmp    al,40h    ;Check for version 4.0
        je    ems32_call_driver
        cmp    al,32h    ;Check for version 3.2
        je    ems32_3
        mov    ah,0ffh    ;Set bad version error code
        jmp    short ems32_exit
ems32_3:
        mov    ax,[bp-2]    ;Get function number
        cmp    ah,4eh    ;See if function 1 to 15
        jbe    ems32_call_driver
                ;If so, call EMM
        push    bx
        mov    bx,offset ems32_return
        push    bx    ;Put return address on stack
        mov    bx,ax    ;Convert function into
        mov    bl,bh    ;jump table index
        xor    bh,bh
        sub    bl,50h
        or    bl,bl
        je    ems32_4
        sub    bl,4
        cmp    bl,2
        jbe    ems32_4
        mov    ah,84h    ;Unsupported function code
        jmp    short ems32_exit
ems32_4:
        shl    bx,1
        add    bx,offset ems32up_calltbl
        push    [bx]    ;Push subroutine addr on stack
        mov    bx,[bp-4]    ;Get original BX
        retn        ;Call function
ems32_return:
        add    sp,2    ;Pull BX off stack
        jmp    short ems32_exit
ems32_call_driver:
        mov    ax,[bp-2]    ;Restore AX
        cmp    ah,55h    ;See if map and jump
        jne    ems32_5     ;If so, clean up stack since
        add    sp,2    ;EMM will not return to
        mov     [bp+4],ax    ;this routine
        mov    ax,[bp+2]    ;Use AX and flags values to
        mov    [bp+6],ax    ;overwrite return address
        pop    bp
        pop    ax    ;Clean off extra flags reg
        pop    ax    ;Get back AX
        popf        ;Restore Flags
ems32_5:
        int    67h    ;Call EMM
ems32_exit:
        add    sp,2    ;Clear off original AX
        pop    bp
        popf
        ret

;====================================================================
;EMSMapMultiple  Simulates the Map Multiple function of the EMS 4.0
;                driver
; Entry: AL - subfunction. 0 = Use physical page numbers
;                          1 = Use physical page segments
;        CX - Size of mapping array
;        DX - EMS Handle
;     DS:SI - Pointer to array of mapping structures (1 structure per ;
page)
; Exit:  AH - Return code
;
;  Page mapping structure:
;          log_to_phys_struc  STRUC
;            log_page_number  dw  ?
;            phy_page_number  dw  ?
;          log_to_phys_struc  ENDS
;====================================================================
mapm_pfp    equ    [bp-2]    ;Pointer to page frame
EMSMapMultiple    proc
        push    bp
        mov    bp,sp
        sub    sp,2    ;Create room for local var
        push    bx
        push    cx
        push    di
        push    si
        cmp    al,1    ;Check for legal subfunction
        jbe    mapm_1
        mov    ah,8fh    ;Subfunction code invalid
        jmp    short mapm_exit
mapm_1:
        xor    ah,ah    ;Clear return code.
        or      cx,cx    ;See if mapping 0 pages
        je    mapm_exit    ;If so, exit.
        xor    ah,ah
        mov    di,ax    ;Copy subfunction
        mov    ah,41h    ;EMS Get Page Frame segment
        int    67h    ;Call EMS driver
        or    ah,ah    ;Check for error
        jne    mapm_exit
        mov    mapm_pfp,bx    ;Save page frame pointer
mapm_loop1:
        mov    ax,[si+2]    ;Get physical page
        or     di,di     ;See if map by segment or num
        je    mapm_4
        sub    ax,mapm_pfp    ;Sub starting addr of frame
        jae    mapm_3    ;If positive, continue
mapm_2:
        mov    ah,8bh    ;Page segment illegal RC
        jmp    short mapm_exit
                ;Error, goto exit
mapm_3:
        push    dx    ;Save handle
        xor    dx,dx    ;Clear high word
        mov     bx,400h    ;Divide by segment size
        div    bx
        or    dx,dx    ;Check for remainder
                ;If found
        pop    dx    ;the segment was not on a
        jne     mapm_2    ;proper boundary
mapm_4:
        mov    bx,[si]    ;Get logical page
        mov    ah,44h    ;EMS Map page
        int    67h    ;Call EMS driver
        or    ah,ah    ;Check for error
        jne    mapm_exit
        add    si,4
        loop    mapm_loop1
mapm_exit:
        pop    si
        pop    di
        pop    cx
        pop    bx
        add    sp,2
        pop    bp
        ret
EMSMapMultiple    endp

;====================================================================
;EMSMapandJump  Simulates the Map and Jump function of the EMS 4.0
;               driver
; Entry: AL - Subfunction. 0 = Use physical page numbers
;                          1 = Use physical page segments
;        DX - EMS Handle
;     DS:SI - Pointer to map and jump structure
; Exit:  AH - Return code
;
;  Map and Jump structure:
;          map_and_jump_struc    STRUC
;            target_addr         dd  ?
;            log_phys_map_len    db  ?
;            log_phys_map_ptr    dd  ?
;          map_and_jump_struc    ENDS
;
;  Page mapping structure:
;          log_phys_map_struc  STRUC
;            log_page_number   dw  ?
;            phy_page_number   dw  ?
;          log_phys_map_struc  ENDS
;====================================================================
EMSMapandJump    proc
        push    bx
        push    cx
         push    si
        push    ds
        xor    cx,cx    ;Clear size
        mov    cl,ds:[si+4]    ;Get size of map array
        lds    si,ds:[si+5]    ;Get pointer to map array
        call    EMSMapMultiple
                ;Call MapMultiple to map pages
        or    ah,ah    ;Check for error
        pop    ds
        pop    si
        pop    cx
        pop    bx
        jne    mapandjump_exit
                ;If so, error code in AH. Exit
        mov    ax,ds:[si]    ;Copy jump address over return
        mov    [bp+4],ax    ;address on the stack
        mov    ax,ds:[si+2]
        mov    [bp+6],ax
        xor    ax,ax    ;Zero return code
mapandjump_exit:
        ret
EMSMapandJump     endp

;====================================================================
;EMSMapandCall  Simulates the Map and Call function of the EMS 4.0

;               driver
; Entry: AL - Subfunction. 0 = Use physical page numbers
;                          1 = Use physical page segments
;                          2 = Get Stack space size
;        DX - EMS Handle
;        DS:SI - Pointer to map and call structure
; Exit:  AH - Return code
;
;  Map and Call structure:
;          map_and_call_struc    STRUC
;            target_addr          dd  ?
;            new_page_map_len     db  ?
;            new_page_map_ptr     dd  ?
;            old_page_map_len     db  ?
;            old_page_map_ptr     dd  ?
;          map_and_call_struc    ENDS
;
;  Page mapping structure:
;          log_phys_map_struc  STRUC
;            log_page_number   dw  ?
;            phy_page_number   dw  ?
;          log_phys_map_struc  ENDS
;====================================================================
EMSMapandCall    proc
        push    ax
        mov    ax,4e03h    ;Get size of save array
        int     67h
        or    ah,ah
        pop    bx    ;Get back function
        jne    mac_0
        cmp    bl,2    ;Check for subfunction 2
        jne    mac_1
        add    al,24    ;Add additional room on stack
mac_0:
        jmp    mac_exit1      ;on the stack
mac_1:
        push    si    ;Save array pointer
        push    ds
        push    di    ;Save registers needed for
        push    es    ;save page map call
        sub    sp,ax    ;Create room on stack
        mov    di,ss    ;Point to save area on stack
        mov    es,di
        mov    di,sp
        push    ax    ;Save size of save area
        mov    ax,4e00h    ;Save page map
        int    67h    ;Call EMM
        or    ah,ah
        je    mac_2
mac_save_error:
        pop    di    ;Get save area size
        add    sp,di    ;Remove save area from stack
        jmp    short mac_exit
mac_2:
        mov    ax,[bp-2]    ;Get original AX for
        push    cx    ;subfunction
        xor    cx,cx    ;Clear size
        mov    cl,ds:[si+4]    ;Get size of new array
        lds    si,ds:[si+5]    ;Get pointer to new map array
        call    EMSMapMultiple
                ;Call MapMultiple to map pages
        pop    cx
        mov    ds,[bp-10]
        mov    si,[bp-8]     ;Restore registers
        or    ah,ah    ;Check for error
        jne    mac_save_error
                ;If so, error code in AH
        mov    es,[bp-14]
        mov    di,[bp-12]
        mov    ax,[bp+2]    ;Get flags off stack
        push    ax
        popf        ;Restore flags
        push    bp    ;Save my BP
        mov    bp,[bp]    ;Restore original BP
        mov    ah,0    ;Set return code
        call    dword ptr ds:[si]
                ;Call user routine
        pop    ax    ;Get back BP used by routine
        xchg    bp,ax      ;Put returned BP on stack in
        mov    [bp],ax    ;place of original BP
        pushf        ;Put all 16 bits of the flags
        pop    ax    ;register in AX
        mov    [bp+2],ax    ;Save new flag state on stack
        mov    [bp-12],di    ;Save new ES:DI
        mov    [bp-14],es
        mov    di,si    ;Save new DS:SI
        push    ds
        pop    es
        mov    si,ss    ;Point to save area on stack
        mov    ds,si
        mov    si,sp
        inc    si    ;Move past save area size
        inc    si
        mov    ax,4e01h    ;Restore page map
        int    67h
        pop    si    ;Get size of save area
        add    sp,si    ;Remove save area from stack
        push    es
        pop    ds
        mov    si,di
        or    ah,ah
        jne    mac_exit
        push    cx    ;Save working registers
        push    si
        push    ds
        mov    si,[bp-8]    ;Get pointer to structure off
        mov    ds,[bp-10]    ;the stack
        xor    cx,cx    ;Clear size
        mov    ax,[bp-2]    ;Restore subfunction
        mov    cl,ds:[si+9]    ;Get size of old array
        lds    si,ds:[si+10]    ;Get pointer to old map array
        call    EMSMapMultiple
                ;Call MapMultiple to map pages
        pop    ds
        pop    si
        pop    cx
mac_exit:
        pop    es
        pop    di
        add    sp,4    ;Clear off old SI and DS
mac_exit1:
        ret
EMSMapandCall     endp

EMS32Upgrade    endp

        end

Figure 4

;====================================================================
;    Sample program for EMS Toolbox Routines
;     (c) 1989 Microsoft Corporation
;====================================================================

    DOSSEG
    .MODEL    small
    .STACK    512

    .DATA
EMShandle    dw    0    ;EMS handle

pageframe_seg    dw    0

active    db    0    ;Flag to indicate int9 active
key_code    db    0    ;Code of key pressed
cursor_pos    dw    0    ;Saved position of cursor

save_index    dw    0
save_area    db    100 dup (0)
save_area_end    =    $

    .CODE
    EXTRN    GetEMSMem:PROC
    EXTRN    SaveEMSMem:PROC
    EXTRN    RestoreEMSMem:PROC

int09h    dd    0    ;Saved interrupt 9 vector

;--------------------------------------------------------------------
;Keyboard Interrupt.  This routine checks for the hotkey then saves
;  or restores the video page as necessary.
;--------------------------------------------------------------------
kbdint    proc    far
    assume    cs:_text,ds:nothing,es:nothing
    push    ax
    mov    ah,2    ;Get keyboard shift state
    int    16h
    cmp    al,08h    ;See if ALT key pressed
    jne    goto_kbdbios
    in    al,60h    ;Read keyboard port
    cmp    al,13h    ;See if 'R' key pressed
    je    kbd0
    cmp    al,2Eh    ;See if 'C' key pressed
    jne    goto_kbdbios
kbd0:
    push    ds
    push    ax
    mov    ax,@data    ;Point DS to data segment
    mov    ds,ax
    pop    ax
    assume    ds:_data
    mov    key_code,al    ;Save key value (C or R)
    cmp    active,0
    je    kbd1
    pop    ds
goto_kbdbios:
    pop    ax
    jmp    cs:int09h
kbd1:
    inc    active
    sti
    push    bx    ;Save registers
    push    cx
    push    dx
    push    di
    push    si
    push    es
    mov    di,@data
    mov    es,di    ;Point to save area
    mov    di,offset save_area
    mov    cx,offset save_area_end - offset
        save_area

    call    SaveEMSMem    ;Save state of EMS driver

    or    ah,ah    ;Check for error, if
    jne    kbd_exit    ;error, exit
    mov    save_index,dx
    mov    ax,4400h    ;EMS Map page 0
    xor    bx,bx    ;Logical page 0
    mov    dx,EMShandle
    int    67h
    or    ah,ah    ;Check for error, if
    jne    kbd_restore    ;error, exit
    mov    ah,0fh    ;Get display mode
    int    10h    ;Check for valid modes 2,3,7
    cmp    al,7
    je    kbd2
    cmp    al,2
    jb    kbd_restore
    cmp    al,3
    ja    kbd_restore
kbd2:
    push    ax
    mov    ah,3    ;Save cursor position
    int    10h
    mov    cursor_pos,dx
    pop    ax
    mov    es,pageframe_seg
    xor    di,di    ;Saved scr at offset 0
    cmp    key_code,2eh
            ;See if capture function
    je    kbd3
    mov    di,1000h    ;Save current scr at 1000h
kbd3:
    call    save_scr    ;Save current screen
    call    kb_reset    ;Reset keyboard controller

    cmp    key_code,2eh
            ;See if capture function
    je    kbd_restore    ;If so, were done
    push    ds
    mov    ds,pageframe_seg
    xor    si,si

    call    restore_scr    ;Display saved screen

    pop    ds
kb4:
    xor    ah,ah    ;Get key
    int    16h
    cmp    al,27    ;Check for ESC key. If
    jne    kb4    ;found exit. Else get
            ;another key.
    push    d    ;Restore original screen
    mov    ds,pageframe_seg
    mov    si,1000h

    call    restore_scr

    pop    ds

kbd_restore:
    mov    si,offset
        save_area    ;Restore EMS mapping
            ;context
    mov    dx,save_index

    call    RestoreEMSMem

    mov    ah,2    ;Set cursor position
    mov    dx,cursor_pos
    int    10h
kbd_exit:
    mov    active,0    ;Clear active flag
    pop    es
    pop    si
    pop    di
    pop    dx
    pop    cx
    pop    bx
kbd_exit1:
    pop    ds
    pop    ax
    iret
kbdint  endp

;--------------------------------------------------------------------
;Save screen
;--------------------------------------------------------------------
save_scr    proc    near
    stosw        ;Save video mode
    mov    ax,1234h    ;Buffer initialized flag
    stosw
    mov    cx,25    ;25 rows
    xor    dx,dx
saves1:
    push    cx
    mov    cx,80    ;80 columns
    xor    dl,dl
saves2:
    mov    ah,2    ;Set cursor position
    int    10h
    mov    ah,8    ;Read char and attribute
    int    10h
    stosw        ;Store in EMS buffer
    inc    dl    ;Point to next column
    loop    saves2
    pop    cx
    inc    dh    ;Point to next row
    loop    saves1
    ret
save_scr    endp

;--------------------------------------------------------------------
;Restore screen
;--------------------------------------------------------------------
restore_scr    proc    near
    lodsw        ;Get video mode
    xor    ah,ah    ;Set mode
    cmp    word ptr [si],1234h
            ;See if initialized
    jne    restore_exit
    int    10h
    inc    si
    inc    si
    mov    cx,25    ;25 rows
    xor    dx,dx    ;Start at row 0
restores1:
    push    cx
    mov    cx,80    ;80 columns
    xor    dl,dl    ;Start at column 0
restores2:
    push    cx
    mov    ah,2    ;Set cursor position
    int    10h
    lodsw        ;Get char and attr from buffer
    mov    bl,ah    ;Copy attribute
    mov    cx,1    ;Write 1 character
    mov    ah,9    ;Write char and attribute
    int    10h
    inc    dl    ;Point to next column
    pop    cx
    loop    restores2
    pop    cx
    inc    dh    ;Point to next row
    loop    restores1
restore_exit:
    ret
restore_scr    endp

;--------------------------------------------------------------------
;KB_RESET resets keyboard and signals end-of-interrupt to the 8259
;--------------------------------------------------------------------
kb_reset    proc near
    assume    cs:_text
    in    al,61h    ;get control port value
    mov    ah,al    ;save it in AH
    or    al,80h    ;set bit 7
    out    61h,al    ;send reset value
    mov    al,ah    ;get original value
    out    61h,al    ;send to enable keyboard
    cli        ;suspend interrupts
    mov    al,20h    ;get EOI value
    out    20h,al    ;send EOI to 8259
    sti        ;enable interrupts
    ret
kb_reset    endp


;--------------------------------------------------------------------
;Initialization. Reserve EMS memory and hook into keyboard interrupt
;--------------------------------------------------------------------
main:    mov    ax,@data    ;Set DS to data seg
    mov    ds,ax

    mov    bx,1    ;Get 1 page
    mov    si,-1

    call    GetEMSMem    ;Allocate EMS memory


    or    ah,ah

    jne    ems_error
    mov    EMShandle,dx ;Save EMS handle
    mov    pageframe_seg,bx
            ;Save EMS page frame
    push    es
    mov    ax,3509h    ;Get and Set int 9 vector
    int    21h
    mov    word ptr int09h,bx
    mov    word ptr int09h[2],es
    mov    ax,2509h
    push    ds
    push    cs
    pop    ds
    mov    dx,offset kbdint
    int    21h
    pop    ds
    pop    es
    mov    dx,ds
    add    dx,10h
    mov    bx,es
    sub    dx,bx
    mov    ax,3100h    ;TSR
    int    21h
ems_error:
    mov    al,ah    ;Copy EMS return code
    mov    ah,4ch    ;terminate with return code
    int    21h

    END    main





A Complete Guide to OS/2 Interprocess Communications and Device Monitors

Richard Hale Shaw

The protected mode operating environment in the OS/2 operating system allows
each program to run in a different session independently of other programs.
Yet this same protection mechanism prevents a program from readily sharing
data and communicating with other processes. Fortunately, OS/21 offers
several interprocess communications facilities (IPC) that allow two
processes to signal, synchronize, serialize resources, and transfer data.
This article explores OS/2 IPC and introduces OS/2 character device
monitors.

OS/2 has come a long way. When I wrote what would become the first article
in this series, most OS/2 developers were still using OS/2 Version 1.0 or
beta copies of Version 1.1 from the Microsoft(R) OS/2 Software Development
Kit. With the addition of Microsoft Word Version 5 for OS/2, I have written,
developed, and edited this article and its sample programs entirely under
OS/2--something I have not been able to do in the past. I can run each
tool--word processor, program editor, compiler, test programs--in
different Presentation Manager windows, switching between them with a mouse
click. This capability is another  example of how productive an environment
OS/2 can be.

In previous articles, I discussed OS/2's protected mode operating
environment and how it is organized in screen groups which are composed of
one or more sessions. A session and a screen group are usually synonymous.
The exception is the Presentation Manager's screen group, which can run more
than one session simultaneously. Each session consists of one or more
processes (roughly synonymous with programs), each of which has one or more
threads of execution. Since each session is independent of the others, a
process can run without concern that it will be unnecessarily interrupted by
others or that another process will wantonly hang the system.

However, this same protection and independence makes it difficult for one
process to communicate directly with another one. OS/2 isolates the data of
one program from the data of another program. You can't just cast a pointer
to a common data area as you would under a real-mode environment like the
MS-DOS(R) operating system, therefore sharing data is more complex under
OS/2.

Fortunately, OS/2 offers a rich set of interprocess communications (IPC)
facilities that allow for flexible communications between well-behaved,
cooperating programs. In this article, I'll discuss the forms of IPC, then
each of the IPC facilities provided by OS/2. I'll also present a sample
program for each IPC that will show you how to use them in an application
program. And I'll introduce you to OS/2 character device monitors, which
allow an application to intercept data from a device driver long before it
reaches another application. Under OS/2, you can create pop-up programs with
keyboard monitors, a subject that I will also cover.

An OS/2 IPC Overview

Clearly, there are times when it is advantageous for threads in different
processes to communicate. For instance, a process may need to signal another
that an event has occurred, or it may need to synchronize its activities
with those of another process. At other times, several processes may need to
share a resource that only one process at a time may use, or a process may
need to transfer data to another process.

The most primitive form of IPC is signaling, which is used when a thread in
one process needs to notify a thread in another process that an event has
occurred. Although you should use semaphores for signaling most of the time,
you can also use the OS/2 signaling facility to pass signals between
processes. OS/2 signals are crude, but they do allow a process to notify
another of an event. Signals are also used to notify an application that the
user is trying to terminate the process by typing Control-C or
Control-Break.

Synchronization, which is required when a thread needs to coordinate its
activities with those of another thread, is another form of IPC. Semaphores
are the simplest and easiest OS/2 tools to use for this purpose, but you can
also use queues and shared memory.

Yet another form of IPC is serialization, which allows one thread to ensure
that it has exclusive use of, or owns, a resource that only one process can
access or use at a time. Semaphores are usually the perfect solution to
providing exclusivity as long as every process that wants to use the
resource must first gain control of the semaphore.

RAM semaphores are used to signal, serialize resources, and synchronize
threads in the same process. For interprocess signaling, serialization, and
synchronization, you'll want to use OS/2 system semaphores most of the time.
(OS/2 also offers Fast-Safe RAM semaphores, which are usually used with
dynamic-link libraries.)

Interprocess data transfer is another form of IPC. It is used when a thread
in one process needs to transfer data to a thread in another process. Of
course, data transfer isn't a problem between threads in the same process,
since they can access the same global data. OS/2's shared memory, pipes, and
queues are available to facilitate interprocess data transfer. Pipes are the
easiest to implement and use if the data transfer is going to take place
between two related processes (that is, where one process is the parent of
the other). Shared memory is slightly more involved, since the application
must perform a modest amount of memory management; but it allows you to
transfer data between two programs as if each had the same variables in its
global data area. Queues are a  powerful form of IPC under OS/2; they
require more work from your program, but they allow several sender processes
to transfer data to a single receiver process. Named pipes, the most
powerful form of OS/2 IPC, are almost limitless in their use--even
across a network. I've given the discussion of named pipes special treatment
in the sidebar "Named Pipes: A Welcome Latecomer."

System Semaphores

A RAM semaphore is a 4-byte long variable. You can define a semaphore
external to any function in the application and make it globally available
to all threads in the process. There are no limits to the number of RAM
semaphores you can use in an application. Unfortunately, only the threads in
the process that defines a RAM semaphore can access it. While you can place
a RAM semaphore in shared memory and allow threads in different  processes
to access it there, you needn't bother: use system semaphores instead.

System semaphores, used for signaling, synchronization, or serialization,
are the most versatile IPC mechanism that OS/2 offers. Like their RAM
semaphore cousins, system semaphores have two distinct states,  set and
clear. You can use the same application program interface (API) services on
system semaphores that you use to manipulate RAM semaphores. Using these
services ensures that the action of setting or clearing a system semaphore
is a single atomic OS/2 instruction that the OS/2 system scheduler cannot
preempt.

A thread must always reference a semaphore by means of a semaphore handle.
The address of a RAM semaphore is its semaphore handle, so all you have to
do is define it and initialize it to 0L to use it. System semaphores must be
created with the DosCreateSem function or opened with the DosOpenSem
function before they can be used. Then OS/2 will return a handle for the
semaphore. Only one process can create a particular system semaphore, but
any other process can open the semaphore once it has been created. Thus
every process can obtain the semaphore's handle.

The naming convention that OS/2 uses for system semaphores is essentially
the same one used for shared memory and queues. It requires a
pseudo-filename which, in the case of system semaphores, is preceded by a
pseudo-directory name, \SEM. For instance, a thread can create a system
semaphore called \SEM\SEM0. The \SEM directory doesn't really exist; it's a
means of identifying SEM0 as a system semaphore. To create a system
semaphore, a thread must pass the semaphore's name to the DosCreateSem
function, which returns the semaphore handle. The creating thread must
specify whether other processes will be able to change or only monitor the
semaphore. Then other processes can call DosOpenSem, which will open the
semaphore and return a handle for them to use. Note that OS/2 Version 1.1
allows a maximum of 256 system semaphores at any time, so be judicious in
their use.

When a thread is finished with a system semaphore, it should call
DosCloseSem to close it. If the thread owns the system semaphore at the
time, it should always clear the semaphore before closing it. Closing the
semaphore tells OS/2 that the thread is no longer using  it. OS/2 will
return an error if the thread attempts to use a system semaphore after
closing it, even if the thread was the one that created the semaphore. Once
the last thread closes it, OS/2 will discard the semaphore.

As already mentioned, a thread can use a system semaphore handle with any of
the API functions used for manipulating RAM semaphores. Of those functions,
DosSemClear and DosSemSet are best for signaling, synchronization, and
mutual exclusion. DosSemRequest will cause the calling thread to wait, or
block, while another thread owns the semaphore. Once its owner clears the
semaphore with DosSemClear, DosSemRequest will set the semaphore and the
blocked thread will unblock and return.

There are three additional functions that you can use to make a thread wait
for a change in a semaphore's state. These functions allow a thread to
synchronize its actions with the actions of another thread. DosSemWait will
wait for a thread to call DosSemClear and clear a semaphore (like
DosSemRequest does except   DosSemWait doesn't set the semaphore).
DosSemSetWait does the same thing but sets the semaphore before it begins to
block. DosMuxSemWait lets you construct a list of semaphores and returns
when any semaphore in the list has been cleared.

Sample Semaphore

SSEM.C (see Figure 1) demonstrates the use of system semaphores. It shows
how to create or open a system semaphore and how to use it with DosSemWait,
DosSemRequest, DosSemSet, and DosSemClear. It also illustrates a simple
example of synchronization between processes.

What's unique about SSEM is that to use it, you should run two or more
instances of it in separate sessions. The first instance will always create
the system semaphore, set it, and enter a program loop. Each time the
program goes through the loop, it prints a message and clears the semaphore.
Then it looks to see if the operator has pressed a key and, if so, closes
the semaphore and terminates itself. If the operator has not pressed a key,
the process sleeps for a while and requests ownership of the semaphore. When
the semaphore becomes available, SSEM begins the loop again.

A subsequent instance of SSEM, unable to create the semaphore because it
already exists, will open the semaphore instead. Then it will wait until
some other instance of SSEM clears the semaphore and enters the loop
described above. Thus multiple instances of the same program will take turns
owning the semaphore.

The program doesn't really use the semaphore to protect a resource, but
using the semaphore does force the program to indirectly serialize its use
of the OS/2 video resources. Only one instance of the program can write its
message at a time--when the semaphore is available. This sample should
give you a better idea of how system semaphores work.

Anonymous Pipes

UNIX system developers originally introduced pipes as a form of operating
system IPC. Pipes get their name from their resemblance to a water pipe, in
which water transfers in one direction from one location to another. In an
operating system context, a pipe allows a process to send data to another
process, as if the first is writing to a file that the second is reading.
You can also use pipes for redirection, since, in many cases, STDIN and
STDOUT are indistinguishable from file handles. OS/2 anonymous pipes are a
very efficient, versatile means of interprocess data transfer and  demand
little from the programmer or the application.

While anonymous pipes are flexible to use, their application is limited: you
can only use them between related processes, where one process is the parent
(or an ancestor) of another. Thus one process can create an anonymous pipe
and start or spawn another process that can read from it or write to it. I
will discuss how to use anonymous pipes, but move on rather quickly to more
powerful forms for transferring data in IPC.

A pipe is a first in, first out (FIFO) stream in which one process inserts
data at one end and another takes the data out at the other end. It
resembles a file to the processes that use it but it is actually a memory
buffer that OS/2 manages. A process can write data to a pipe and another
process can read data from the pipe as if either were manipulating a disk
file. Indeed, several processes can write data to the pipe, but only one
process can read from it. OS/2 manages the pipe and handles the scheduling
and synchronization of the data.

Under OS/2, pipe handles, like file handles, can be inherited by a child
process. Thus, a parent process can pass a pipe handle to a child process as
a command line parameter or via shared memory and the child can use the
handle.

The parent process can also use a pipe to read the child's standard output
or write to its standard output. This is done by closing either the standard
input or output handle and substituting a pipe handle in its place. When the
child uses either standard input or standard output, it will actually be
using the pipe handle. For instance, a parent process can call DosClose to
close STDOUT. By calling DosDupHandle, the parent process creates a
duplicate of the pipe's write handle and forces it to be recognized as
STDOUT. When the child process begins, all output that it writes to STDOUT
will actually go to the pipe where the parent process can read it. This will
become clearer to you in the PIPE sample program.

To create an anonymous pipe, a process must call DosMakePipe. This API
function returns two handles to the pipe, one for reading and one for
writing. The receiver can pass one handle to a child process as a command
line parameter or by means of shared memory (see the section on shared
memory below). Then one process can use DosRead to read from the pipe and
the other can use DosWrite to write to it. DosWrite will not return until
the data passed to it has been written to the pipe, so it may have to wait
until a DosRead by the receiver has made enough room in the pipe for it to
complete its task and return.

There are a few additional differences between pipes and files other than
that pipes do not involve disk I/O: for one, a process cannot seek in a
pipe, but it doesn't have to maintain a file pointer. For another, an
application needn't worry about whether the data in the pipe will be
overwritten, since OS/2 manages the pipe. Also, a pipe is inherently faster
than a disk file since OS/2 stores the entire pipe in memory. On the other
hand, although a file is limited only by the available disk space, a pipe
can never be larger than 64Kb; the creating process can suggest a pipe size
to OS/2 when it calls DosMakePipe, but OS/2 may dynamically change the size
of the pipe as it is used.

An application doesn't have to worry about filling up the pipe unless data
is being read from the pipe too slowly or written to the pipe too quickly.
Alternatively, a thread can use DosWriteAsync and DosReadAsync so that it
won't be blocked while the pipe is full or being written to. But these
functions create their own threads and require a huge stack to accommodate
any combination of API calls. It's more reasonable to create your own thread
that performs these operations asynchronously using calls to DosWrite or
DosRead rather than using DosWriteAsync and  DosReadAsync.

Since data is written to a pipe in FIFO order, it is up to the processes
involved to synchronize themselves. If the data written to the pipe varies
in length, your applications will have to work out some sort of protocol, or
procedure for knowing how to interpret the data. Once an application
finishes using the pipe, it should close it with DosClose. When all the
processes using the pipe have closed their pipe handles, OS/2 will delete
the resources used to manage the pipe.

Sample Pipe Program

PIPE.C (see Figure 2) illustrates a simple use of an anonymous pipe. The
program also demonstrates a technique that an application might use when it
wants to start another process in the same session and give the operator the
ability to monitor the new process's output. When you run PIPE, it divides
the screen into two halves (upper and lower) and displays the child
program's output in the upper half while interacting with the operator in
the lower half. It does this by closing the STDOUT handle and substituting
it with a pipe handle. Thus, the child process thinks it is writing to
STDOUT, when its output is actually going to an anonymous pipe. The parent
process uses a separate thread to read the pipe and display the output in
the lower half of the screen.

To run PIPE, enter

PIPE <programname> [args...]

and follow PIPE with the name of the program (and any arguments) whose
output will be displayed in the upper half of the screen. PIPE begins by
creating an anonymous pipe and closing STDOUT. Then it calls DosDupHandle to
duplicate the pipe's write handle as the new STDOUT. Using the pipe handle
as the new STDOUT does not hinder PIPE itself, since it uses VIO for its own
video output. But if the child program uses VIO or STDERR, they will not be
transferred to PIPE for printing on the screen.

Next, PIPE clears the screen and divides it in two (by using VioWrtNCell to
draw a line horizontally across the center of the screen). Then it creates a
buffer that includes both the program to run and its arguments and calls
DosExecPgm to run the child process asynchronously, allowing the child and
PIPE to run at the same time in the same session.

After this, PIPE creates a new thread of execution to read the pipe input
and print it on the screen. Contained in the function readchild, this thread
enters a program loop where it uses a call to DosRead to read from the pipe.
When DosRead returns, the thread strips the pipe input of carriage returns
and line feeds and scrolls the upper portion of the screen up as it prints
each line. If it finds any incomplete lines, it moves them to the beginning
of the buffer, sleeps briefly, and returns to the DosRead at the top of the
loop.

Simultaneously, the main thread also enters a loop, where it calls
KbdStringIn to read a line from the user. As the thread gets each line, it
scrolls the lower half of the screen up, prints the line, and returns to the
top of the loop. If the line typed was QUIT, the thread breaks out of the
loop. Then it calls DosCwait to see if the child process has completed. If
not, it calls DosKillProcess to terminate the child process and terminates
itself.

You can use PIPE with any program that writes its output to STDOUT. I've
written  a simple test program, TEST.C, for testing the PIPE program.
TEST.C, which can be downloaded from any of the MSJ bulletin boards, prints
a list of messages to STDOUT in an infinite loop.

Shared Memory

Shared memory is an OS/2 IPC mechanism that allows more than one process to
use the same memory segment at the same time. It's useful for interprocess
data sharing and data transfer, since one process can transfer data to
another by copying it to a shared memory segment. Then another process can
read it and modify it, and the original process can read it back. An
unlimited number of processes can access shared memory, all of whom can read
from or write to the shared memory segment.

Other than a small amount of code required to set up shared memory among
processes, a program can access a shared memory segment by means of a far
pointer, making shared memory access relatively transparent to an
application. This flexibility usually demands some method for establishing
mutual exclusion; otherwise one process may overwrite the data of another.
Because of this you should usually plan on using a system semaphore to
manage each shared memory segment.

There are three ways of establishing shared memory. In each one, a
process--the owner of the shared memory--allocates a shared memory
segment that a second process will be able to access. Using the first
method, the owner process gives the second process access to the shared
memory by creating a memory selector that the second process can use. This
is known as givable memory; it requires that the owner know the process ID
of the second process in order to create the new selector. Under the second
approach, known as getable memory, the owner passes its own memory selector
to the second process, which can then get its own selector and access the
memory segment.

An alternative technique involves named shared memory. Under this scheme, a
process can create a shared memory segment that is associated with a
specific name. Then any process that knows the name of the shared memory can
access it directly.

Of the three approaches to shared memory, I think named shared memory is the
most convenient and flexible to use. However, the first two methods are
terrific for limiting shared memory access to a few specific processes and
work particularly well with queues.

To use unnamed shared memory, a process must first call DosAllocSeg and
allocate the shared segment, specifying the segment size and whether the
segment is shareable. DosAllocSeg will return a selector to the segment.
OS/2 relates selectors and processes, so that one process cannot use
another's memory selector. Thus if the process knows the process ID of the
second process, it can create givable shared memory by passing the selector
and ID to DosGiveSeg, which will return a new selector the other process can
use. Finally, the process must pass this selector to the other process, as a
command line parameter or using some other mechanism. Queues lend themselves
perfectly to this scheme, since you can create a pointer from the shared
memory selector and pass the pointer through the queue. Or the first process
can pass the original selector to the other process, which can use DosGetSeg
to access the unnamed shared memory.

If your application doesn't know the ID of the other process, then you will
want to use named shared memory. With named shared memory, your application
uses a name for the shared memory segment which resembles the
pseudo-filename used  by system semaphores. An example would be
\SHAREMEM\MEM; the SHAREMEM pseudo-directory name indicates that a shared
memory segment is being created. The application passes this shared memory
name to DosAllocShrSeg, which returns a selector to the shared segment. As
with system semaphores, any other process can open a shared memory segment
once it has been created. The other processes must call DosGetShrSeg, which
will return a selector to the named shared segment.

Under either scheme, a process should call DosFreeSeg to free the shared
memory once it has finished with it. OS/2 will not discard the segment until
the last process using it has called DosFreeSeg. As with system semaphores,
a process can continue to access a shared segment even after the segment is
free and the process that created the segment has terminated.

Once a process has obtained the selector to a shared memory segment, it can
convert the selector into a far pointer using the MAKEP macro and access the
shared memory directly.

Shared Memory Program

SHMEM.C (see Figure 3), a sample shared memory program, uses named shared
memory. There is no need for a program for unnamed shared memory, since the
sample program for queues serves adequately for both.

As with the sample system semaphore program, you can run multiple copies of
the shared memory program. Each instance of the program competes for access
to a piece of named shared memory by using a system semaphore. Once an
instance of the program gains access to this memory, it looks to see if
another instance of the program has placed a message there and, if so,
prints it. Then it writes its own message to the shared memory segment,
releases the semaphore (and ownership of the shared memory), and blocks
until it can gain access to the semaphore again. As with SSEM, an instance
of SHMEM will terminate if you press a key while its session is in the
foreground.

You can run more than one instance of SHMEM by running them in different
sessions. Each instance prints messages left by other instances, but only
one will create or own the shared memory. The messages identify the owner
and other users of the shared memory, as well as their process IDs.

Queues

Queues are one of OS/2's most powerful and complex IPC mechanisms for data
transfer. They are more flexible than anonymous pipes and potentially more
powerful than shared memory. Queues are usually used in conjunction with
shared memory and provide a framework that allows you to easily implement a
shared memory manager. Used properly, they eliminate a lot of the work
associated with shared memory data transfers.

Traditionally, queues are FIFO mechanisms in which one process inserts data
at one end and another process takes it out at the other. In such cases, a
process reading the queue must accept elements in the order in which they
are written to it. OS/2 queues, on the other hand, can be ordered on a FIFO,
last in, first out (LIFO), or priority basis. With priority order, the
sender can assign a priority number to each element. The element with the
highest priority is always first in the queue. If two elements have the same
priority, they are placed in the queue in FIFO order. In addition, multiple
processes can send data to the queue, whereas only one process--the
queue owner--can read from it.

Unlike anonymous pipes, queues can be accessed by unrelated processes,
although a process must know the name of the queue to access it. A process
writes data to a queue as a distinct set of data elements. The sender, not
the receiver, controls the size of the data it receives. You will typically
use shared memory to transfer data via a queue: the queue transfers elements
that contain information about the data being transferred. A sender will
pass a pointer to the shared memory as part of a queue element. The receiver
can then use this pointer to access the shared memory data.

A receiver or a queue owner has far more control over the queue elements
than does the receiving process of a pipe. It can remove the queue elements
in almost any order and can choose to leave some elements in the queue while
retrieving others. In addition, the queue owner can decide to free the
shared memory when it wishes or keep it around for a while. Unlike anonymous
pipes, queues require their own set of API functions for sending and
receiving data: you cannot use DosRead and DosWrite with queues. OS/2 limits
queues to 409, with a total of 3268 data elements at a time.

Queues are relatively easy to use, but there are a few steps involved in
accessing them. First, the receiving process (the queue owner) must create
the queue with DosCreateQueue, specifying the name of the queue and the
queue order. DosCreateQueue will then return a queue handle. The queue name
is a pseudo-filename like those used for system semaphores and named shared
memory. An example is \QUEUES\QNAME. Once the queue owner creates the queue,
other processes can open it via DosOpenQueue, which will return a queue
handle. This function also returns the process ID of the queue owner, so
that a sender can create a shared memory selector for the queue owner.

When a process is finished with the queue, it should close it using
DosCloseQueue. OS/2 will maintain the queue as a resource until the owner
closes the queue, at which time it disposes of the queue and any elements
remaining in it. Attempts by a process to write to the queue after that will
fail. A sender can reopen a queue after closing it as long as the queue
owner has not closed the queue.

As I mentioned earlier, queue data elements are typically stored in shared
memory segments. There are, however, several ways that an application can
pass data through a queue. The most common approach is to use unnamed shared
memory, with the sender allocating a segment every time it places an element
in the queue (these are segments the size of a queue element, not 64Kb
segments). The sender writes data to the segment, creates a selector to the
shared memory that can be used by the queue owner, and converts the selector
into a pointer. Then it calls DosWriteQueue, passing it a priority value and
the pointer to the shared memory. The sender can then free the shared memory
segment if it no longer needs it, since OS/2 will not discard the segment
until the queue owner frees it.

Alternatively, the application can allocate one shared memory segment and
divide it into pieces that the sender and queue owner will reuse. With this
approach, the sender passes a pointer to the portion of the shared memory
segment where the data resides, rather than a pointer to the segment itself.
Although it is more work for the programmer to create a scheme to manage
portions of the shared segment, the resulting program should run faster,
since the constant allocation and reallocation of memory found in the first
scheme is absent.

There is, however, a way to pass data via a queue without using shared
memory. If the sender is only transferring small pieces of data that are no
more than 2 bytes in size (such as a series of tokens or even selectors),
the application can forgo the use of shared memory entirely. The
DosWriteQueue function includes a 2-byte request code that OS/2 reserves for
use by the application. An application can use this code to pass 2-byte data
elements through the queue to the receiving process without implementing
either of the two shared memory schemes outlined above.

Regardless of the method you use, the receiver must use DosReadQueue to read
the elements from the queue. This function will return the process ID of the
sender, the request code described in the previous paragraph, the length of
the data element, a pointer to the data element, and its priority.
DosReadQueue can wait until an element is in the queue or return immediately
if no data elements are available. While the receiver should typically
devote a thread to reading the queue, it can avoid this by providing a
semaphore handle to DosReadQueue that OS/2 will clear when data elements are
in the queue. The receiver can check this semaphore from time to time to see
if a queue element has arrived while it was tending to other cases.

The receiver can use DosPeekQueue to examine elements in the queue without
removing them, and both sender and receiver can call DosQueryQueue to
determine the number of elements in the queue. In addition, the receiver can
flush the queue with DosPurgeQueue. Of course, it's the receiver's
responsibility to free any shared memory segments passed with each element.

Queue Programs

The sample queue takes the shared memory example a step farther, moving
these applications into the world of client-server relationships. This is
why I have written two programs, one for the client and one for the server.
The server's job is simple: if it gets a message from a client process, it
must print that message. The client, not much more complex than the server,
periodically sends messages to the server. The processes communicate the
messages using queues and shared memory.

If you study the listing for QSERVER.C (see Figure 4), you will see that the
server begins by creating the queue. Then, it enters a program loop and
blocks on a call to DosReadQueue. Once a queue element is available, the
server returns from DosReadQueue and calls DosQueryQueue to get the count of
the number of elements in the queue. Next it prints the message contained in
shared memory, the element priority, the process ID of the sender, and the
element count. Then the server frees the shared memory that the message
occupies. If the operator has pressed a key in the server's session, it
breaks out of the program loop, closes the queue and exits the program.
Otherwise, it returns to the top of the loop and the DosReadQueue call.

QCLIENT.C contains the source code for the client process. The client begins
by opening the queue. If the server hasn't yet created the queue, the client
will sleep for a second and try again. As you can see, the order in which
you run the programs doesn't matter.

Once the client has opened the queue, it enters a program loop where it will
send messages to the server. It begins by allocating a shared memory
segment. Then it uses DosGiveSeg and the MAKEP macros to create a pointer to
the shared memory that the queue owner--the server process--can
use. The client calls DosWriteQueue to place the pointer, the length of the
message, and the element priority in the queue. The client immediately frees
the shared memory segment, which won't be discarded until the server frees
it. Then the client prints a message to show that it successfully placed a
message in the queue to the server. If the operator has pressed a key, it
closes the queue and exits the program. Otherwise, it increments the
priority (or resets it to 0) and sleeps for a while before continuing at the
top of the program loop.

Although you can run only one instance of the server program, you can run a
client program in every other session. If you are willing to forgo the
acknowledgment from a client process that it is sending messages, you can
run DETACHed instances of clients that can also communicate with the server.
In fact, you can DETACH the server, which will invisibly accept messages
from several clients.

One note about priorities: try pressing Control-S in the server's session
while the client processes are generating messages. Then, after a few
moments, press another key to get the server restarted. The server will
continue with the highest-priority elements first, gradually working down to
lower-priority elements until it catches up.

Signals

OS/2 signals are IPC mechanisms that notify a process that an external event
has occurred. Signals are routinely used by OS/2 to communicate with a
process. For instance, if a user presses Control-C or Control-Break to
terminate a process, OS/2 will generate the signal SIG_CTRLBREAK or the
signal  SIG_CTRLC. If the process has installed a signal handler, it can
field these signals in any manner it wishes. Otherwise, the command
processor parent of the process will receive the signal, translate it into
the SIGKILLPROCESS termination signal, and pass it to the process. OS/2 may
also terminate a process by sending SIGKILLPROCESS to it directly (if you
select Close in the System Icon pulldown menu on a Presentation Manager
window, the window's command processor will send SIGKILLPROCESS to the
application). OS/2 also sends a SIGKILLPROCESS signal to a child process
when the parent process uses DosKillProcess to terminate the child.

Signals can also be used in OS/2 for simple communications between two
applications.  OS/2 provides three signal flags, SIG_PFLG_A, SIG_PFLG_B, and
SIG_PFLG_C, that an application can use to signal another.

Signal handling under OS/2 is similar to interrupt handling under DOS2.
Under DOS, an external event generates an interrupt to the CPU. The CPU then
executes a predefined interrupt handling routine, which DOS or the machine
BIOS provides. A DOS application can substitute its own interrupt handler to
process interrupts while the application is running. Then it will (we hope)
reset the predefined interrupt handler when it is through or about to
terminate.

Under OS/2, external events generate signals. OS/2 provides a predefined
signal handler for each type of signal, but an application can substitute or
register its own signal handler instead. Then the application can reset the
original signal handler when it is done. However, since a signal handler is
specific to an application, it doesn't matter if the application doesn't
replace the signal handler before terminating. An application can use
DosSetSigHandler to set up its own signal-handling routine by specifying the
address of the signal handler function and the type of signal to be trapped.

An application must register a signal handler with OS/2 for each type of
signal. If the process has not registered a signal handler by the time the
signal is generated, OS/2 will pass the signal handler to the default signal
handler--usually the command processor for the session, either CMD.EXE
or the Presentation Manager shell (depending how the application was
started).

When OS/2 passes a signal to an application that has registered its own
signal handler for that signal, it switches the application's main thread to
the code of the signal-handling routine. Since signals are time-critical
events that require an immediate response, OS/2 will interrupt the main
thread even if it is executing a critical section of code (that is, one
defined by a call to DosEnterCritSec and a subsequent call to
DosExitCritSec).

If the main thread is waiting for the return of an API function when it is
switched to the signal handler, OS/2 will force the thread to abort the call
rather than wait for the function to complete. Once the main thread has
processed the signal, OS/2 will switch it back to its own code. If an API
function is aborted, the function call will return an error code. The
exceptions are API functions that perform file I/O: OS/2 places calls to
these functions in a blocked state when they are called. Thus OS/2 can
switch the calling thread back to the middle of an API file I/O function
without aborting the function call.

The lesson to be learned from these applications is that if you are writing
an application that uses a signal handler, do not use the main thread for
any work that would be corrupted if a signal interrupts it. To be safe, have
the main thread start one thread of execution that executes the remaining
code and starts any other threads. Then the main thread should install the
signal handler and indefinitely block on a call to DosSleep. You can use
DosHoldSignal to have the main thread temporarily ignore a signal while it
is performing some uninterruptible processing, but you should not do this as
a general practice.

An application should never install a signal handler that  will ignore
SIG_CTRLC, SIG_CTRLBREAK, or SIGKILLPROCESS, even if it ignores the signal
flags. For instance, if the signal handler ignores SIGKILLPROCESS,  you will
not be able to terminate it without rebooting your machine. Upon receiving
any of these three signals, the process should free its resources, notify
any child processes to terminate, and terminate itself. You can use
DosExitList to ensure that this occurs properly.

A well-written OS/2 application can, however, use the three signal flags for
communicating with other processes. These are not flags that communicate a
state (as semaphores do), nor are they flags that a process can manipulate.
Instead, these flags signal a process that something has happened and allow
it to act accordingly.

If you are writing an application that will handle signals, you can install
a signal handler with DosSetSigHandler. This function can specify five
different ways of reacting to a specific signal. The process can:

■    replace the current signal handler with another

■    acknowledge the signal

■    return an error to the sender (indicating that the receiver has refused
to accept the signal)

■    ignore the signal

■    remove the signal handler

To call DosSetSigHandler, your program must specify the signal, the action
to take, and the address of the signal handler function being installed. If
you are supplying the latter, DosSetSigHandler will return the address of
the current signal handler and its action, so that your application can
restore these later.

The signaling process should use DosFlagProcess to signal the receiver
process, but to use this function it must know the process ID of the
receiver. In addition, DosFlagProcess allows the sender to send a 2-byte
unsigned value to the receiver. The content of this argument is left to the
discretion of the application; this is not unlike the 2-byte value that a
queue sender can pass to a queue owner.

Once the signal has been generated, OS/2 will preempt the receiver's primary
thread and direct it to execute the code of the new signal handler. The
signal handler should immediately issue a call to DosSetSignalHandler to
acknowledge the signal. This action resets the signal handler and allows the
process to accept the signal again (the receiver can't receive that signal
again until it has cleared the signal handler). If the signal handler does
not acknowledge the signal, DosFlagProcess will return an error code to the
sender indicating that the receiver refused to accept the signal. Once the
receiver process no longer needs  to handle the new signal,
DosSetSignalHandler should be used to restore the previous signal handler.

The signal handler routine always has the same form:

void APIENTRY sighandler
(USHORT arg, USHORT num);

(This line was broken due to space limitations, but should run on one
line--Ed.) You can use any name you wish for the function and
arguments, but you must declare the function this way. OS/2 will supply the
contents of the two arguments. You never actually call the signal handler
function in your code--when your program receives the appropriate
signal, OS/2 will set up the stack as if the function had been called and
jump to the first line of code inside the function. The first argument
specifies the type of signal that was sent. If it is a flag signal, the
second argument will be the discretionary value supplied to DosFlagSignal by
the sender. Otherwise, OS/2 will set this argument to 0.

When the receiver's signal handler is invoked, OS/2 will load the CS, IP,
SS, SP, and flag registers with new values to represent the code and stack
for the signal-handling routine. The signal handler should terminate with a
far return so that OS/2 can resume execution of the interrupted thread.
Fortunately, if you are writing your application in C, this process is
transparent to the application and your code, since the function was
declared with the APIENTRY type modifier. OS/2 will execute the far return
for you when it reaches the end of the function.

What happens if your program receives a signal while the signal handler is
active? If the receiver's signal handler hasn't acknowledged or cleared the
signal yet, DosFlagProcess will return an error to the signal-sending
process. If the receiver's signal handler has acknowledged the signal but is
still active (that is, execution has not returned to the primary thread),
the OS/2 error handler will trap the second occurrence of the signal. In
other words, the receiver will not know that the signal was sent, and
DosFlagProcess will not return an error to the sender (not yet, at least).
However, if the sender generates the signal a third time while the
receiver's error handler is still active after acknowledging the first
signal, DosFlagProcess will return ERROR_SIGNAL_PENDING to the receiver.
Consequently, OS/2 can only handle two signals per process at a time.

Sample Signal Program

The sample SIG.C program (see Figure 5) shows you how to create a process
that can signal another process. In keeping with the sample semaphore and
shared memory programs, I have used one body of source code to create a
program that you can run simultaneously in different sessions.

The first instance of SIG will always be the receiver. When you run it, SIG
will display its process ID and attempt to create a system semaphore. If
successful, it will start a keyboard thread. The keyboard thread blocks on a
call to KbdCharIn and terminates the process immediately upon its return.
The user can then terminate the receiver instance of SIG at any time by
pressing a key. Since the KbdCharIn call is in a separate thread, the main
thread can install the signal handler and then sleep indefinitely. This
eliminates the possibility of the keyboard call being interrupted by a jump
to the signal handler code.

After the main thread has started the keyboard thread, it installs the
signal handler. The signal-handling routine will process all signals except
the one used to detect a broken pipe (DosSetSigHandler will return an error
if you attempt to install a signal handler for SIG_BROKENPIPE without
opening the pipe first). Once it has installed the signal handler, the main
thread enters a loop in which it sleeps for 10 seconds , wakes up, and
sleeps again.

To generate some signals, you must run at least one other instance of the
program in a different session. When you run a signaling instance of SIG,
you must pass it the process ID of the receiver instance as a command line
parameter. Therefore, if the receiver prints a process ID of 50 when you
start it, run each subsequent instance with:

SIG 50

After printing its own process ID, each signaler attempts to create the
semaphore. When the signaler fails (since the receiver has already created
the semaphore) it begins to execute the sender code in the program. It reads
the receiver's process ID from the command line and enters the program loop,
where it  calls DosFlagProcess repeatedly to signal the receiver, switching
to a different signaling flag each time, and prints a message to the screen
with each pass. As it goes through the loop, it checks for any keyboard
input and terminates if the user has pressed a key. Then it sleeps briefly
before returning to the top of the loop.

The receiver process remains idle until it receives a signal. When it
receives a signal, OS/2 switches the main thread to the code of the signal
handler function, sighandler. This function immediately acknowledges the
signal with a call to DosSetSigHandler, prints a message identifying the
signal, and prints a subsequent message showing whether it successfully
acknowledged the signal. Then the function returns and OS/2 switches the
thread back to its own code.

Note that you can't terminate the receiver with a Control-C or
Control-Break. The receiver will acknowledge these signals but refuse to
terminate. You can, however, end the program by selecting Close from the
OS/2 Presentation Manager system icon or by selecting Task/Close from the
Presentation Manager Task Manager window. If you do this, the program will
print a message acknowledging the signal before terminating. You can also
end the program by pressing a key when the receiver's session is in the
foreground. You might want to experiment with running several instances of
SIG, each signaling the first instance. This experiment should give you a
good idea of how signaling works and how to use it.

Finally, I should mention that although signaling is fast, the system
semaphore is the preferred means of setting a flag that can be used by two
different processes. If speed is critical and the other caveats associated
with signals are not a problem, by all means go ahead and use them. But for
most signaling, system semaphores are a more elegant solution.

Device Monitors

Character device monitors are not a form of OS/2 IPC. They do, however,
allow an application to monitor and manipulate the data sent to and from a
character device. In this way you can create a process that can monitor or
modify the data stream between a device driver and its associated subsystem,
whether or not the monitor process is in the foreground session. While
monitors are not a part of OS/2 IPC, they do allow one application to
control and manipulate the data that another application receives from a
device.

The prototypical example of a device monitor is a keyboard monitor. You can
use an OS/2 keyboard monitor to create a program that activates when the
user presses a hot key, just as with many of the popular DOS
terminate-and-stay-resident programs. I'll limit the discussion of monitors
here to what you need to know to create programs that do just that.

A device monitor only monitors a device's interaction with the screen group
in which you install it. You can, however, install a monitor in more than
one screen group at a time. If you install a keyboard monitor in one of the
Presentation Manager's sessions, it will monitor the keyboard device stream
for the entire Presentation Manager screen group. You can use monitors on
any character device, including the keyboard, the mouse, and the printer,
but you cannot monitor a block device (like a disk drive) or the
asynchronous communications device driver. Usually, you can  control only
the data stream flowing between the device driver and the API subsystem for
that driver. However, a printer monitor will let you control the data
flowing in either direction.

When you install a device monitor, OS/2 places it in the data stream between
the device driver and the buffers used by the associated API subsystem. A
keyboard monitor, for example, will receive data from the KBD$ device driver
and pass data to the keyboard (KBD) subsystem. If you install more than one
monitor, OS/2 will pass the data in the stream from one monitor to the next.
This collection of monitors is known as the monitor chain. Together a set of
monitors can filter the data placed in a session's API buffer for the device
being monitored.

There are several steps that a program must take to create and install a
character device monitor. The monitor process should  issue a call to
DosMonOpen first, which will return a device handle for the data stream to
be monitored. Then the program should call DosMonReg, which will register
the process as a device monitor for the data stream. A call to DosMonReg
causes the device driver to create a monitor dispatcher. The monitor
dispatcher will divert the data stream from the device driver to the monitor
and from the monitor to the API buffer (or  the next monitor in the chain);
DosMonReg will also register the monitor's input and output buffers with the
monitor dispatcher.

Once you've registered the monitor, the device driver's monitor dispatcher
will automatically place records from the data stream into the monitor's
input buffers. The monitor  examines the data in its buffers, and can either
modifiy it or leave it alone. Then it must efficiently transfer the data
from its input buffer to its output buffer by means of calls to DosMonRead
and calls to DosMonWrite. The monitor dispatcher will automatically transfer
the data to the API buffers or the next monitor in the chain. Note that a
monitor can optionally consume a data record if it does not want it passed
on to the application. When that happens, the application never receives the
data.

Because a monitor can affect the device information received by
applications, you should take care in designing and writing one. You can use
DosMonReg to set the monitor's position in the monitor chain (first, last,
or next available). OS/2 will always place the first monitor to register for
the first position at the front of the monitor chain. If another monitor
subsequently registers for first position, OS/2 will place it after any
other monitor that has already registered for that position. Likewise, OS/2
will place the first monitor that registers for last position last, and
subsequent monitors vying for that position will be next to last, and so on.
If a monitor registers for the next available position, it will be placed
after any monitors that requested first position and before any that
requested last position. You can usually place a keystroke monitor almost
anywhere in the monitor chain so long as other monitors do not remove or
modify the data for which it's looking.

A monitor should be efficient; it should read from, modify, and write to the
data stream as quickly as possible. In addition, a monitor should never take
any action that might disrupt the data stream. As a rule, you should
dedicate one thread of the monitor process to read to and write from the
data stream and another thread to react to the data content.

DosMonOpen should be called  only once in a program. Therefore if your
application is going to use multiple monitors, place the monitor code from
the call to DosMonReg through the DosMonRead/DosMonWrite loop in a new
thread. The portion of the thread that performs the DosMonRead and
DosMonWrite calls should not have to wait for I/O services or perform
operations that may block it. If the monitor thread is blocked, the monitor
will stop the entire data stream, halting the flow of data from the device
driver to every process in the screen group until the monitor thread becomes
unblocked. If the monitor hangs, it can hang the entire screen group.

A monitor thread's priority should be set to help ensure that the OS/2 task
scheduler dispatches the monitor thread before it dispatches any threads
accessing the API buffer. The monitor should run at the lower levels of the
time-critical (the highest) priority category. If the monitor's priority is
too low, it will waste CPU cycles and not deliver data from the device
driver fast enough. Then the threads reading the API buffer will block
longer than necessary, since they will be waiting for the arrival of the
data in the buffer.

Note that I said to place the monitor at the lower level of the
time-critical priority category. Threads that service the hardware device
drivers usually use the top of the time-critical category. The monitor must
wait for these threads to deliver data from the device to the monitor chain.
If you set the monitor at the same level, it will use too many CPU cycles,
since the dispatcher will schedule it on an equal basis with the device
driver threads. Furthermore, if the time-critical priority category is
overloaded with monitors, the device drivers will not be able to meet the
demands of the devices.

Keyboard Monitor

The sample MONITOR.C program (see Figure 6) shows how you can install a
simple keyboard monitor. This monitor examines the data stream from the
keyboard device driver and displays a pop-up screen whenever the user
presses the Alt-F10 hot key. The pop-up screen waits for another key press,
then ends the pop up. If the user presses the Ctrl-F10 termination key, the
monitor removes itself from the keyboard monitor chain and terminates
itself.

MONITOR consists of two threads. The main thread begins by setting a RAM
semaphore and launching the monitor thread. The monitor thread uses the RAM
semaphore to signal the main thread to create the pop up. After the main
thread starts the monitor thread, it enters a program loop and blocks on a
call to DosSemRequest. When the monitor thread clears the semaphore, the
main thread will become active. It will call VioPopUp to create a pop-up
screen, print a message, and enter another program loop.

In the next program loop, the main thread repeatedly checks for a key press
from the operator and breaks out of the loop if a key is available. If not,
the thread sleeps for one second. Each time it goes through the loop, the
thread increments a counter; when 20 seconds have passed (or rather, twenty
1-second sleep intervals), the thread will break out of the loop. Once out
of the loop, the thread will end the pop-up screen and block on the RAM
semaphore again. Using the inner loop allows this thread to wait until a
user presses a key without waiting indefinitely while a pop-up screen is
active. Although OS/2 will turn over the pop-up session to a pending
VioPopUp call after 30 seconds, it makes no sense for the thread to own this
resource for more than 20 seconds.

If the monitor thread clears the program_active variable and the RAM
semaphore, the main thread will break out of the main program loop and then
terminate the program.

The monitor thread itself is a fairly bland example of a keyboard monitor.
First, it calls DosMonOpen to get a device handle for the keyboard device
driver. Then the thread calls  DosGetInfoSeg to get the program's screen
group number, which is needed by DosMonReg. It registers its input and
output buffers with DosMonReg and raises the thread's priority with
DosSetPrty. Then the thread enters the main monitor loop, reading keyboard
data from the KBD$ device driver with DosMonRead and writing the data to the
KBD API buffer with DosMonWrite (assuming no other monitors are installed).

Between each read and write, the monitor thread checks to see if the scan
key for a keystroke is the hot key required to generate the pop-up screen in
the main thread. If so, the monitor thread sets a variable, popup_active,
clears the RAM semaphore, and returns to the top of the loop.

By clearing the RAM semaphore, the monitor thread activates the main thread
and uses the popup_active variable to determine whether the pop-up screen is
active. If it is active, the monitor thread will pass the key to the API
buffers, even  if it is the hot key.

If the monitor thread discovers the termination key in the keyboard data
stream, it clears the program_active variable, breaks out of the monitor
loop, and closes the monitor with a call to DosMonClose. Just before the
thread terminates itself, it clears the RAM semaphore. Clearing the RAM
semaphore causes the main thread to unblock and shut down the process.

You can run MONITOR from any session or screen group, but you'll get the
best results if you DETACH it so that it runs as a background process. You
can also DETACH it from a windowed command prompt in the Presentation
Manager's screen group. Doing that allows you to create a pop-up program
that will appear even if you are currently using a session in the PM's
screen group.

OS/2 provides enough forms of IPC--ranging from primitive signaling to
sophisticated named pipes--to meet almost any programming requirement.
This allows for a great deal of flexibility when developing applications
that will need to share data, such as servers and communications programs.
The sample programs provided with this article were designed specifically to
demonstrate IPC mechanisms. The challenge is to take the code, build on it,
and adapt it to fit the needs of your development project.

Figure 1

SSEM.MAK

# make file for ssem.c
#
COPT=/Lp /W3 /Zpiel /G2s /I$(INCLUDE) /Alfw

ssem.exe: ssem.c ssem.mak
    cl $(COPT) ssem.c /link /co /noi llibcmt
    markexe    windowcompat ssem.exe

SSEM.C

/* ssem.c RHS 5/1/89
 This program demonstrates the use of system semaphores. To use it,
 run more than one copy of the program in different sessions with:

 SSEM
*/
#define INCL_DOS
#define INCL_KBD
#define INCL_ERRORS

#include<os2.h>
#include<stdio.h>

#define    SEMNAME    "\\sem\\ssem.sem"

void error_exit(USHORT err, char *msg);
PID Os2GetPid(void);
void main(void);

void main(void)
    {
    HSYSSEM semhandle;
    USHORT retval;
    int creator = FALSE;
    KBDKEYINFO kbdkeyinfo;
    PID pid;
    unsigned count = 0;

    pid = Os2GetPid();

    /* first process will successfully execute this code and create the
    semaphore */
    if(retval = DosCreateSem(CSEM_PUBLIC,&semhandle,SEMNAME))
        {
        if(retval != ERROR_ALREADY_EXISTS)
            error_exit(retval,"DosCreateSem");

    /* second process will open the semaphore previously created */
        else if (retval = DosOpenSem(&semhandle,SEMNAME))
            error_exit(retval,"DosOpenSem");

        DosSemWait(semhandle,SEM_INDEFINITE_WAIT);
        DosSemRequest(semhandle,SEM_INDEFINITE_WAIT);
        }
    else
        {
/* then first process will wait here until second process executes the code
in the loop below */
        DosSemSet(semhandle);
        creator = TRUE;
        }

    while(TRUE)    /* both processes will continue to execute in
        this loop */
        {
        printf("%s (%u) %u\n",(creator ? "Creating Thread" :\
"User Thread"), pid,count++);

        DosSemClear(semhandle);

        KbdCharIn(&kbdkeyinfo,IO_NOWAIT,0);                    /* check for
key press */
        if(kbdkeyinfo.fbStatus & FINAL_CHAR_IN)                     /* if
pressed, break out */
            break;

        DosSleep(150L);
        DosSemRequest(semhandle,SEM_INDEFINITE_WAIT);
        }

    DosCloseSem(semhandle);    /* close the semaphore */
    DosExit(EXIT_PROCESS,0);    /* and exit */
    }

PID Os2GetPid(void)        /* returns process id */
    {
    PIDINFO pidinfo;

    DosGetPID(&pidinfo);    /* get process id */
    return pidinfo.pid;    /* return it */
    }

void error_exit(USHORT err, char *msg)
    {
    printf("Error %u returned from %s\n",err,msg);
    DosExit(EXIT_PROCESS,0);
    }

Figure 2

PIPE.MAK

# make file for pipe.c
#
COPT=/Lp /W3 /Zpiel /G2s /I$(INCLUDE) /Alfw

pipe.exe: pipe.c pipe.mak
    cl $(COPT) pipe.c /link /co /noi llibcmt
    markexe windowcompat pipe.exe

PIPE.C

/* pipe.c RHS 5/2/89
   pipe example:
   this program will reroute the stdout of a child process to the
   pipe, then it will print the child's output to the upper portion
   of the screen while you can type into the lower portion of the
   screen

   To run:

     PIPE <child program name> [child program arguments]
 */
#define INCL_DOS
#define INCL_KBD
#define INCL_VIO
#define INCL_ERRORS

#include<os2.h>
#include<mt\stdio.h>
#include<mt\process.h>
#include<mt\string.h>

#define    PIPESIZE    80
#define    STDIN    0
#define    STDOUT    1
#define    FAILBUFLEN    128
#define    ENV    0L
#define    THREADSTACK    800

char readbuf[PIPESIZE+5];
HFILE readhandle, writehandle, newSTDOUT = STDOUT;
USHORT cell = 0x1f20;
char readchild_stack[THREADSTACK];

void error_exit(USHORT err, char *msg);
PID Os2GetPid(void);
void main(int argc, char **argv);
void readchild(void);

void main(int argc, char **argv)
    {
    USHORT retval, cell2 = 0x1fc4, i;
    STRINGINBUF stringinbuf;
    char failbuf[FAILBUFLEN];
    RESULTCODES resultcodes;
    char keybuf[PIPESIZE], *p;
    PID   pid,pidasync;

    if(argc < 2)        /* must have one argument */
    error_exit(0,"main");

                                    /* create pipe */
            if(retval = DosMakePipe(&readhandle,&writehandle, PIPESIZE))
        error_exit(retval,"DosMakePipe");

    DosClose(STDOUT);    /* close stdout */
                                        /* pipe write handle now stdout */
    if(retval = DosDupHandle(writehandle,&newSTDOUT))
        error_exit(retval,"DosDupHandle");
                                        /* clear the screen */
    VioScrollUp(0,0,0xffff,0xffff,0xffff,(PBYTE)&cell,0);
    VioWrtNCell((PBYTE)&cell2,80,11,0,0);
                /* draw line to split screen */

    memset(readbuf,0,sizeof(readbuf));    /* clear buffer */
    strcpy(readbuf,argv[1]); /* add program name */
    if(!(strstr(strupr(readbuf),".EXE"))) /* tack on .EXE */
        strcat(readbuf,".EXE");

    p = (&readbuf[strlen(readbuf)]+1);
    for( i = 2; i < argc; i++)    /* add program arguments */
        {
        strcat(p,argv[i]);
        strcat(p," ");
        }
                                            /* run the child program */
    if(retval = DosExecPgm(failbuf,FAILBUFLEN,EXEC_ASYNC,
    readbuf,ENV,&resultcodes, readbuf))
        error_exit(retval,"DosExecPgm");
    pidasync = resultcodes.codeTerminate;

    /* start pipe read thread */
    if(_beginthread(readchild,readchild_stack,
    THREADSTACK,NULL) == -1)
        error_exit(-1,"_beginthread");

    while(TRUE)
        {
        stringinbuf.cb = sizeof(keybuf)-1;
                                                /* await operator input */
        if(retval = KbdStringIn(keybuf,&stringinbuf,IO_WAIT,0))
            error_exit(retval,"KbdStringIn");

        keybuf[stringinbuf.cchIn] = '\0';
                                                /* scroll screen up */
        VioScrollUp(12,0,23,0xffff,1,(PBYTE)&cell,0);
                                                /* write command */
        VioWrtCharStr(keybuf,strlen(keybuf),23,0,0);
                                                /* clear prompt line */
        VioWrtNCell((PBYTE)&cell,80,24,0,0);
        if(!strcmp(strupr(keybuf),"QUIT"))                        /* if QUIT
command, get out */
            break;
        }
                                            /* see if child finished */
    if(retval = DosCwait(DCWA_PROCESSTREE,DCWW_NOWAIT,
    &resultcodes,&pid,resultcodes.codeTerminate))
        {
        if(retval == ERROR_CHILD_NOT_COMPLETE) /*if not, kill it*/
            {
            if(retval = DosKillProcess(DKP_PROCESSTREE,
            pidasync))
                error_exit(retval,"DosKillProcess");
            }
        else if(retval != ERROR_WAIT_NO_CHILDREN)
            error_exit(retval,"DosCwait");
        }

    DosExit(EXIT_PROCESS,0);    /* get out */
    }


void readchild(void)    /* this thread reads the pipe, and
            prints the lines */
    {
    USHORT retval,bytesread,i, lines;
    char *p = readbuf;

    while(TRUE)
        {                                    /* read the pipe */
        if(retval = DosRead(readhandle,p,
        PIPESIZE-(p-readbuf),&bytesread))
            error_exit(retval,"DosRead");
        bytesread += (p-readbuf);

        for(i = lines =0; i < bytesread; i++)
                /* remove CRs and LFs */
            {
            if(readbuf[i] == '\n')
                lines++;
            if(readbuf[i] == '\r' || readbuf[i] == '\n')
                readbuf[i] = '\0';
            }
                                            /* print each line found */
        for(p = readbuf, i = 0; i < bytesread && lines;
        lines--)
            {
            VioScrollUp(0,0,10,0xffff,1,(PBYTE)&cell,0);
            VioWrtCharStr(p,strlen(p),10,0,0);
            i += strlen(p);
            p += strlen(p);
            for( ; !(*p) && i < bytesread; i++, p++);
            DosSleep(1L);
            }

        if(*p && !lines && i < bytesread)                            /* if
anything is leftover */
            {
            memmove(readbuf,p,bytesread-i);    /*move it up*/
            p = &readbuf[bytesread-i];
            }
        else
            p = readbuf;
        }
    }

PID Os2GetPid(void)        /* returns process id */
    {
    PIDINFO pidinfo;

    DosGetPID(&pidinfo);    /* get process id */
    return pidinfo.pid;    /* return it */
    }

void error_exit(USHORT err, char *msg)
    {
    char buf[100];

    sprintf(buf,"Error %u returned from %s\r\n",err,msg);
    VioWrtTTY(buf,strlen(buf),0);
    DosExit(EXIT_PROCESS,0);
    }

Figure 3

SHMEM.MAK

# make file for shmem.c
#
COPT=/Lp /W3 /Zpiel /G2s /I$(INCLUDE) /Alfw

shmem.exe: shmem.c shmem.mak
    cl $(COPT) shmem.c /link /co /noi llibcmt
    markexe windowcompat shmem.exe


SHMEM.C

/* shmem.c RHS 5/2/89
 This program demonstrates the use of shared memory. To use it, run
 more than one copy of the program in different sessions with:

 SHMEM
*/
#define INCL_DOS
#define INCL_KBD
#define INCL_ERRORS

#include<os2.h>
#include<stdio.h>
#include<memory.h>

#define    SHAREDMEM    "\\sharemem\\shmem.mem"
#define    SEMNAME        "\\sem\\ssem.sem"
#define    SHAREDMEMSIZE    200

void error_exit(USHORT err, char *msg);
PID Os2GetPid(void);
void main(void);

void main(void)
    {
    HSYSSEM semhandle;
    SEL selector;
    USHORT retval;
    int creator = FALSE;
    KBDKEYINFO kbdkeyinfo;
    PCH p;
    PID pid;

    pid = Os2GetPid();

    /* first process will successfully execute this code and create the
    semaphore */
    if(retval = DosCreateSem(CSEM_PUBLIC,&semhandle,SEMNAME))
        {
        if(retval != ERROR_ALREADY_EXISTS)
            error_exit(retval,"DosCreateSem");

    /* second process will open the semaphore previously created */
        else if (retval = DosOpenSem(&semhandle,SEMNAME))
            error_exit(retval,"DosOpenSem");

        if(retval = DosGetShrSeg(SHAREDMEM,&selector))
            error_exit(retval,"DosGetShrSeg");

        DosSemWait(semhandle,SEM_INDEFINITE_WAIT);
        DosSemRequest(semhandle,SEM_INDEFINITE_WAIT);
        }
    else
        {
    /* then first process will wait here until second process executes
    the code in the loop below */
        if(retval =  DosAllocShrSeg(SHAREDMEMSIZE,
        SHAREDMEM,&selector))
            error_exit(retval,"DosAllocShrSeg");

        DosSemSet(semhandle);
        creator = TRUE;
        }

    p = MAKEP(selector,0);    /* create pointer to shmem */
    if(!creator)
        memset(p,'\0',SHAREDMEMSIZE);

    while(TRUE)
    /* both processes will continue to execute in this loop*/
        {
        if(*p)
            printf("%s (%u) received: %s\n",\
            (creator ? "Owner" : "User"),pid,p);

        sprintf(p,(creator ?
        "from %u, the owner of this shared  \
        memory...Hello!" :"from %u,         \
        a user of this shared memory...Hello!"),pid);

        DosSemClear(semhandle);

        KbdCharIn(&kbdkeyinfo,IO_NOWAIT,0);
        /* check for key press */
        if(kbdkeyinfo.fbStatus & FINAL_CHAR_IN)
        /* if pressed, break out */
            break;

        DosSleep(150L);
        DosSemRequest(semhandle,SEM_INDEFINITE_WAIT);
        }

    DosCloseSem(semhandle);    /* close the semaphore */
    DosFreeSeg(selector);
    DosExit(EXIT_PROCESS,0);  /* and exit */
    }

PID Os2GetPid(void)        /* returns process id */
    {
    PIDINFO pidinfo;

    DosGetPID(&pidinfo);    /* get process id */
    return pidinfo.pid;    /* return it */
    }

void error_exit(USHORT err, char *msg)
    {
    printf("Error %u returned from %s\n",err,msg);
    DosExit(EXIT_PROCESS,0);
    }

Figure 4

Q.H

/* q.h RHS 5/1/89
   queue server/client common header file
*/

#define QUEUENAME    "\\queues\\qserver.que"

QSERVER.MAK

# make file for qserver.c
#
COPT=/Lp /W3 /Zpiel /G2s /I$(INCLUDE) /Alfw

qserver.exe: qserver.c qserver.mak q.h
    cl $(COPT) qserver.c /link /co /noi llibcmt
    markexe windowcompat qserver.exe

QSERVER.C

/* qserver.c RHS 5/1/89
 This program demonstrates the use of queues and creates a server
 process that client processes can pass messages to. To use it, start
 one instance of this program with:

 QSERVER

 Then start multiple instances of QCLIENT.C with:

 QCLIENT
 */

#define INCL_DOS
#define INCL_DOSQUEUES
#define INCL_KBD
#define INCL_ERRORS

#include<os2.h>
#include<stdio.h>

#include"q.h"

void error_exit(USHORT err, char *msg);
void main(void);

void main(void)
    {
    USHORT retval, qcount = 0;
    KBDKEYINFO kbdkeyinfo;
    PCH p;
    HQUEUE qhandle;
    QUEUERESULT qresult;
    USHORT ellength;
    BYTE priority;

    if(retval = DosCreateQueue(&qhandle,QUE_PRIORITY,QUEUENAME))
        error_exit(retval,"DosCreateQueue");

    printf("Server has opened queue, awaiting messages...\n");

    while(TRUE)
        {
        if(retval = DosReadQueue(qhandle,&qresult,&ellength,
        (PVOID FAR *)&p,0x0000,DCWW_WAIT,&priority,0L))
            {
            if(retval != ERROR_QUE_EMPTY)
            /* if error was not from empty queue */
                error_exit(retval,"DosReadQueue");
            continue;
            }
        DosQueryQueue(qhandle,&qcount);
        printf(
        "Server: (%u pending) message received from %u,         priority
%02u: %s\n",qcount,
        qresult.pidProcess, priority,p);
        DosFreeSeg(SELECTOROF(p));

        KbdCharIn(&kbdkeyinfo,IO_NOWAIT,0);                        /* check
for key press*/
        if(kbdkeyinfo.fbStatus & FINAL_CHAR_IN)
            /* if pressed, break out */
            break;
        }
    DosCloseQueue(qhandle);
    DosExit(EXIT_PROCESS,0);    /* and exit */
    }

void error_exit(USHORT err, char *msg)
    {
    printf("Error %u returned from %s\n",err,msg);
    DosExit(EXIT_PROCESS,0);
    }

QCLIENT.MAK

# make file for qclient.c
#

COPT=/Lp /W3 /Zpiel /G2s /I$(INCLUDE) /Alfw

qclient.exe: qclient.c qclient.mak q.h
    cl $(COPT) qclient.c /link /co /noi llibcmt
    markexe windowcompat qclient.exe

QCLIENT.C

/* qclient.c RHS 5/1/89
This program demonstrates the use of queues. To use it, you should first
start QSERVER.C with:

 QSERVER

Then you can run multiple copies of this program with:

 QCLIENT
*/

#define INCL_DOS
#define INCL_KBD
#define INCL_ERRORS

#include<os2.h>
#include<string.h>
#include<stdio.h>
#include"q.h"

char *p = "Hello!";
#define MAX_PRIORITY    15

void error_exit(USHORT err, char *msg);
PID Os2GetPid(void);
void main(void);

void main(void)
    {
    USHORT retval;
    KBDKEYINFO kbdkeyinfo;
    HQUEUE qhandle;
    BYTE priority = 0;
    SEL selector,qownersel;
    PID qowner;
    PCH ptr;
    int len;
    PID pid;

    pid = Os2GetPid();

    while(TRUE)
        {
        if(!(retval = DosOpenQueue(&qowner,&qhandle,QUEUENAME)))
            break;


        else
            {
            DosSleep(1000L);
            continue;
            }
        }
    printf("Client (%u) has opened queue, now messaging...\n",pid);

    while(TRUE)
        {
        if(retval = DosAllocSeg(len = (strlen(p)+1),&selector,
        SEG_GIVEABLE))
                        error_exit(retval,"DosAllocSeg");

        ptr = MAKEP(selector,0);
        strcpy(ptr,p);
        if(retval = DosGiveSeg(selector,qowner,&qownersel))
            error_exit(retval,"DosGiveSeg");

        ptr = MAKEP(qownersel,0);

        if(retval = DosWriteQueue(qhandle,0,len,
        (PBYTE)ptr,priority))
            error_exit(retval,"DosWriteQueue");

        DosFreeSeg(selector);
        printf("Client (%u) sent message, priority %u\n",
pid,priority);

        KbdCharIn(&kbdkeyinfo,IO_NOWAIT,0);
                /* check for key press */
        if(kbdkeyinfo.fbStatus & FINAL_CHAR_IN)
                /* if pressed, break out */
            break;
        if(++priority > MAX_PRIORITY)
            priority = 0;
        DosSleep(50L);
        }

    DosCloseQueue(qhandle);
    DosExit(EXIT_PROCESS,0); /* and exit */
    }

void error_exit(USHORT err, char *msg)
    {
    printf("Error %u returned from %s\n",err,msg);
    DosExit(EXIT_PROCESS,0);
    }

PID Os2GetPid(void)         /* returns process id */
    {
    PIDINFO pidinfo;

    DosGetPID(&pidinfo);    /* get process id */
    return pidinfo.pid;    /* return it */
    }

Figure 5

SIG.MAK

# make file for sig.c
#
COPT=/Lp /W3 /Zpiel /G2s /I$(INCLUDE) /Alfw

sig.exe: sig.c sig
    cl $(COPT) sig.c /link /co /noi llibcmt
    markexe windowcompat sig.exe


SIG.C

/* sig.c RHS 5/1/89 signal handler example
   This program lets you observe how one process can use the OS2
   signaling mechanism to pass signals between two processes.
   To use it, first run the program in one session with:

    SIG

   Note the process ID it prints. This will be the receiver of
   the signals. Then you can run subsequent instances of SIG in
   other sessions by passingthem the process ID of the first
   instance as a command line argument:

    SIG n

    where 'n' is the process ID printed by the first instance. */

#define INCL_DOS
#define INCL_VIO
#define INCL_KBD
#define INCL_ERRORS

#include<os2.h>
#include<mt\stdio.h>
#include<mt\memory.h>
#include<mt\string.h>
#include<mt\stdlib.h>
#include<mt\process.h>

#define    SEMNAME    "\\sem\\ssem.sem"
#define    THREADSTACK    400
char keyboard_thread_stack[THREADSTACK];

unsigned pid = 0;
unsigned signaler = FALSE;
USHORT prevaction0, prevaction1, prevaction2, prevaction3, prevaction4,
       prevaction5;
PFNSIGHANDLER prevhandler0, prevhandler1, prevhandler2, prevhandler3,
        prevhandler4, prevhandler5;

#define MAXSIGS 3

char *signames[MAXSIGS] =    {"Flag A", "Flag B",    "Flag C"};

USHORT signum[MAXSIGS] =    {PFLG_A, PFLG_B, PFLG_C };

void main(int argc, char **argv);
PID OS2GetPid(void);
void APIENTRY sighandler(USHORT arg, USHORT num);
void error_exit(USHORT err, char *msg);
void keyboard_thread(void);


void APIENTRY sighandler(USHORT arg, USHORT num)
    {
    USHORT retval;
    PFNSIGHANDLER prevhandler = NULL;
    USHORT prevaction = 0;

    /* acknowledge immediately */
    retval = DosSetSigHandler((PFNSIGHANDLER)sighandler,
    &prevhandler,&prevaction,SIGA_ACKNOWLEDGE,num);

    switch(num)
        {
        case SIG_CTRLC:
            printf("%u received ^C...",pid);
            break;
        case SIG_KILLPROCESS:
            printf("%u notified of pending termination,
            terminating...",pid);
            DosExit(EXIT_PROCESS,0);
            break;
        case SIG_CTRLBREAK:
            printf("%u received ^Break...",pid);
            break;
        case SIG_PFLG_A:        /* FLAG_A received */
            printf("%u received signal, Flag A,
            arg=%u...",pid,arg);
            break;
        case SIG_PFLG_B:
            printf("%u received signal, Flag B,
            arg=%u...",pid,arg);
            break;
        case SIG_PFLG_C:
            printf("%u received signal, Flag C,
            arg=%u...",pid,arg);
            break;
        default:
            printf("%u received unknown signal (%u),
            arg=%u", pid, num, arg);
            break;
        }

    if(retval)
        printf("unable to acknowledge signal,
        retval=%u.\n",retval);
    else
        printf("acknowledged.\n");
    return;
    }

void main(int argc, char **argv)
    {
    unsigned retval,receiver_pid, signal = 0;
    HSYSSEM semhandle;
    KBDKEYINFO kbdkeyinfo;

    pid = OS2GetPid();
    printf("%s loaded, pid=%u\n",argv[0],pid);

    if(retval = DosCreateSem(CSEM_PUBLIC,&semhandle,SEMNAME))
        {
        if(retval != ERROR_ALREADY_EXISTS)
            error_exit(retval,"DosCreateSem");

        signaler = TRUE;        /* set flag */
        if(argc != 2)
            error_exit(0,"main");
        receiver_pid = atoi(argv[1]);
        }
    else
        {
               if(_beginthread(keyboard_thread,keyboard_thread_stack,
                  THREADSTACK,NULL) == -1)
    error_exit(-1,"_beginthread");

/* set up signal handler to be sighandler function, pass address of
prevhandler and prevaction even though there weren't any previous. */
        if(retval = DosSetSigHandler(sighandler,&prevhandler0,
        &prevaction0,SIGA_ACCEPT,SIG_CTRLC))
            error_exit(retval,"DosSetSigHandler(0)");
        if(retval = DosSetSigHandler(sighandler,&prevhandler1,
        &prevaction1, SIGA_ACCEPT,SIG_KILLPROCESS))
            error_exit(retval,"DosSetSigHandler(1)");
        if(retval = DosSetSigHandler(sighandler,&prevhandler2,
        &prevaction2,SIGA_ACCEPT,SIG_CTRLBREAK))
            error_exit(retval,"DosSetSigHandler(2)");
        if(retval = DosSetSigHandler(sighandler,&prevhandler3,
        &prevaction3,SIGA_ACCEPT,SIG_PFLG_A))
            error_exit(retval,"DosSetSigHandler(3)");

        if(retval = DosSetSigHandler(sighandler,&prevhandler4,
        &prevaction4, SIGA_ACCEPT,SIG_PFLG_B))
            error_exit(retval,"DosSetSigHandler(4)");
        if(retval = DosSetSigHandler(sighandler,&prevhandler5,
        &prevaction5,SIGA_ACCEPT,SIG_PFLG_C))
            error_exit(retval,"DosSetSigHandler(5)");
        }

    while(TRUE)
        {
        if(signaler)    /* if another copy is out there */
            {
        /* signal it (not its children) with FLAG_A  */
            if(retval = DosFlagProcess(receiver_pid,
            FLGP_PID,signum[signal], pid))
                error_exit(retval,"DosFlagProcess");
            else
            printf("Sender (%u): sent signal to %u:%s\n",
    `        pid receiver_pid, signames[signal]);
            if(++signal >= MAXSIGS)
                signal = 0;
                                                /* check for key press */
            retval = KbdCharIn(&kbdkeyinfo,IO_NOWAIT,0);
            if(!retval || (retval != ERROR_SIGNAL_PENDING))
                                                    /* if pressed, break out
*/
                if(kbdkeyinfo.fbStatus & FINAL_CHAR_IN)
                    break;
            DosSleep(100L);     /* sleep again */
            }
        else
            DosSleep(10000L);   /* don't wake too often */
        }
    }

PID OS2GetPid(void)
    {
    PLINFOSEG ldt;
    SEL gdt_descriptor, ldt_descriptor; /* infoseg descriptors */

    if(DosGetInfoSeg(&gdt_descriptor, &ldt_descriptor))
        return 0;

    ldt = MAKEPLINFOSEG(ldt_descriptor);
    return ldt->pidCurrent;
    }

void error_exit(USHORT err, char *msg)
    {
    printf("Error %u returned from %s\n",err,msg);
    DosExit(EXIT_PROCESS,0);
    }

void keyboard_thread(void)
    {
    KBDKEYINFO kbdkeyinfo;

    while(TRUE)
        {
        if(!KbdCharIn(&kbdkeyinfo,IO_WAIT,0))
            DosExit(EXIT_PROCESS,0);
        }
    }

Figure 6

MONITOR.MAK

# make file for monitor.c
#
COPT=/Lp /W3 /Zpiel /G2s /I$(INCLUDE) /Alfw

monitor.exe: monitor.c monitor.mak
    cl $(COPT) monitor.c /link /co /noi llibcmt
    markexe    windowcompat monitor.exe

MONITOR.C

/* monitor.c RHS 5/1/89
This program demonstrates the use of character device monitors on the
keyboard. It creates a keyboard monitor that you can run from any session or
the DETACHed session. Once you run the monitor with:

 MONITOR

you can activate the monitor with Alt-F10. This will create a pop-up screen
which will wait for any key press to end.

You can terminate the monitor with Ctrl-F10. */

#define INCL_DOS
#define INCL_VIO
#define INCL_KBD
#define INCL_ERRORS

#include<os2.h>
#include<mt\stdio.h>
#include<mt\conio.h>
#include<mt\process.h>

unsigned long semhandle = 0L;
unsigned char HotKey = 0x71;        /* HotKey = Alt-F10 */
unsigned char TerminateKey = 0x67;    /* TerminateKey = Ctrl-F10 */
unsigned program_active = TRUE;
unsigned popup_active = FALSE;

MONIN monInbuf;
MONOUT monOutbuf;

#define    THREADSTACK    400
char monitor_stack[THREADSTACK];

#define MONINBUFSIZE    (sizeof(monInbuf)-sizeof(monInbuf.cb))
#define MONOUTBUFSIZE    (sizeof(monOutbuf)-sizeof(monOutbuf.cb))
#define RELEASE     0x40         /* bit mask to distinguish */
#define MAXMILLISECONDS    20000L
#define SLEEPTIME    1000L

typedef struct _keypacket        /* KBD monitor data record */
    {
    unsigned    mnflags;
    KBDKEYINFO cp;
    unsigned    ddflags;
    } KEYBUFF;


void error_exit(USHORT err, char *msg);
void main(void);
void keyboard_monitor(void);

void main(void)
    {
    USHORT popupwait = (VP_WAIT | VP_OPAQUE);
    KBDKEYINFO kbdkeyinfo;
    long milliseconds;

    DosSemSet(&semhandle);

    if(_beginthread(keyboard_monitor,monitor_stack,THREADSTACK,
    NULL) == -1)
        error_exit(-1,"_beginthread");
    while(TRUE)
        {

        DosSemRequest(&semhandle,SEM_INDEFINITE_WAIT);
            /* wait for semaphore */

        if(!program_active)    /* time to terminate? */
            break;        /* then get out */
        VioPopUp(&popupwait,0);
        printf("You made it...press a key");

        /* wait 20 seconds for keystroke, then break out */
        for( milliseconds = 0L; milliseconds <
MAXMILLISECONDS; milliseconds += SLEEPTIME)
            {
            KbdCharIn(&kbdkeyinfo,IO_NOWAIT,0);                    /* check
for key press */
            if(kbdkeyinfo.fbStatus & FINAL_CHAR_IN)
            /* if pressed, break out */
                break;
            DosSleep(SLEEPTIME);    /* sleep for a while */
            }

        VioEndPopUp(0);        /* end the popup */
        popup_active = FALSE;
        }
    DosExit(EXIT_PROCESS,0);         /* kill the process */
    }

void keyboard_monitor(void)
    {
    USHORT retval;
    SEL gdt_descriptor, ldt_descriptor; /* infoseg descriptors */
    KEYBUFF keybuff;
    unsigned count;
    HKBD KBDHandle;
    PGINFOSEG gdt;
    PLINFOSEG ldt;

    monInbuf.cb = MONINBUFSIZE;
    monOutbuf.cb = MONINBUFSIZE;

    /* obtain a handle for registering buffers */
    if(retval = DosMonOpen ( "KBD$", &KBDHandle ))
        error_exit(retval,"DosMonOpen");

    if(DosGetInfoSeg(&gdt_descriptor, &ldt_descriptor))
        error_exit(retval,"DosGetInfoSeg");

    gdt = MAKEPGINFOSEG(gdt_descriptor);
    ldt = MAKEPLINFOSEG(ldt_descriptor);

    /* register the buffers to be used for monitoring */
    if(retval = DosMonReg ( KBDHandle, (PBYTE)&monInbuf,
    (PBYTE)&monOutbuf, MONITOR_BEGIN, gdt->sgCurrent))
        error_exit(retval,"DosMonReg");
    /* raise thread priority */
    DosSetPrty(PRTYS_THREAD, PRTYC_TIMECRITICAL, 0, 0);

    for(keybuff.cp.chChar = 0; ; ) /* loop thru all keyboard in */
        {
        count = sizeof(keybuff);
                                            /* read keyboard */
        if(retval = DosMonRead ((PBYTE)&monInbuf, IO_WAIT,
(PBYTE)&keybu (PUSHORT)&count ))
            error_exit(retval,"DosMonRead");
                                            /* if make on our key */
        if(keybuff.cp.chChar == 0)
            if(!popup_active && (keybuff.cp.chScan == \            HotKey)
&& !(keybuff.ddflags & RELEASE))
                {
                popup_active = TRUE;
                DosSemClear(&semhandle);
                continue;
                }

            else if((keybuff.cp.chScan == TerminateKey) \                &&
!(keybuff.ddflags & RELEASE))
                {
                program_active = FALSE;
                break;
                }

        if(retval = DosMonWrite((PBYTE)&monOutbuf,
        (PBYTE)&keybuff,count))
            error_exit(retval,"DosMonWrite");
        }
    DosMonClose(KBDHandle);        /* close the monitor */
    DosSemClear(&semhandle);
    DosExit(EXIT_THREAD,0);    /* kill the thread */
    }

void error_exit(USHORT err, char *msg)
    {
    printf("Error %u returned from %s\n",err,msg);
    DosExit(EXIT_PROCESS,0);
    }

Figure A

NPSERVER.MAK

# make file for npserver.c
#
#COPT=/Lp /W3 /Zp /Zl /G2s /Ox /I$(INCLUDE)    /Alfw
COPT=/Lp /W3 /Zpiel /G2s /I$(INCLUDE) /Alfw

npserver.exe:    npserver.c npserver.mak
    cl $(COPT) npserver.c /link /co /noi llibcmt
    markexe    windowcompat npserver.exe

NPSERVER.C

/* npserver.c RHS 5/1/89
 This program demonstrates the use of named pipes by creating a
 server process which will create more than instance of a named pipe.
 To see how this works, run this program with:

 NPSERVER

 Then run multiple instances of the NPCLIENT program in other
 sessions. You can also run the NPRMODE program in the compatibility
 box to talk to the server. */

#define INCL_DOS

#include<os2.h>
#include<mt\string.h>
#include<mt\stdio.h>
#include<mt\process.h>
#include"nmpipe.h"

#define MAXSERVERS 5
#define THREADSTACKSIZE 1200

unsigned long semhandle = 0L;
typedef struct _servers
    {
    char    stack[THREADSTACKSIZE];
    int    threadID;
    } SERVER;

SERVER servers[MAXSERVERS];
USHORT pipeopenmode = PIPE_ACCESS_DUPLEX;
USHORT pipemode = (PIPE_WAIT | PIPE_READMODE_BYTE | PIPE_TYPE_BYTE |\
MAXSERVERS);

void main(void);
void error_exit(USHORT err, char *msg);
void server_thread(int servernum);

void main(void)
    {
    int i;

    DosSemSet(&semhandle);

    for( i = 0; i < MAXSERVERS; i++)    /* create server threads */
        {
        if(servers[i].threadID = _beginthread(server_thread,
    servers[i].stack,THREADSTACKSIZE,(void *)i)  == -1)
            error_exit(-1,"_beginthread");
        DosSleep(100L);
        }

    DosSemRequest(&semhandle,SEM_INDEFINITE_WAIT);
        /* wait for semaphore */
    DosExit(EXIT_PROCESS,0);     /* kill the process */
    }

void server_thread(int servernum)
    {
    char readbuf[PIPESIZE];
    HPIPE pipehandle;
    USHORT retval;
    USHORT bytesread;

    printf("Server instance %d creating Named pipe...\n",servernum);
        /* create pipe instance */

    if(retval = DosMakeNmPipe(NAMEDPIPE,&pipehandle,pipeopenmode,
    pipemode, PIPESIZE,PIPESIZE,1000L))
        error_exit(retval, "DosMakeNmPipe");

    printf("Server instance %d waiting for Client to open pipe...\n",
    servernum);

    if(retval = DosConnectNmPipe(pipehandle))
        /* wait until connected */
        error_exit(retval,"DosConnectNmPipe");

    printf("Server instance %d: Client has opened pipe,  \
     waiting for message...\n",servernum);

    while(TRUE)
        {
        /* read a message */
        if(retval = DosRead(pipehandle,readbuf,PIPESIZE,&bytesread))
        error_exit(retval,"DosRead");

        printf("Server instance %d received:
        \'%s\'...acknowledging\n",servernum,readbuf);
        /* acknowledge it */
        if(retval = DosWrite(pipehandle,ACKNOWLEDGE,
        strlen(ACKNOWLEDGE)+1, &bytesread))
    error_exit(retval,"DosWrite");
        DosSleep(250L);
        }
    DosSemClear(&semhandle);
    DosClose(pipehandle);
    }

void error_exit(USHORT err, char *msg)
    {
    printf("Error %u returned from %s\n",err,msg);
    DosExit(EXIT_PROCESS,0);
    }

Figure B

NPCLIENT.MAK

# make file for npclient.c
#

#COPT=/Lp /W3 /Zp /Zl /G2s /Ox /I$(INCLUDE) /Alfw
COPT=/Lp /W3 /Zpiel /G2s /I$(INCLUDE) /Alfw

npclient.exe:    npclient.c npclient.mak
    cl $(COPT) npclient.c /link /co /noi llibcmt
    markexe    windowcompat npclient.exe

NPCLIENT.C

/* npclient.c RHS 5/1/89
 This program creates a client process which will communicate with an
 instance of the named pipe created by NPSERVER. After running
 NPSERVER, you can run several instances of this program with:

 NPCLIENT
*/
#define INCL_DOS
#define INCL_KBD
#define INCL_ERRORS

#include<os2.h>
#include<string.h>
#include<stdio.h>
#include"nmpipe.h"

char writebuf[PIPESIZE];

void main(void);
void error_exit(USHORT err, char *msg);
PID Os2GetPid(void);

void main(void)
      {
      HPIPE pipehandle;
     USHORT retval;
     USHORT bytesread;
     USHORT action;
      PID pid;

     pid = Os2GetPid();
     printf("Client %u trying to open named pipe...\n",pid);
     while(TRUE)        /* open the pipe */
    {
         if(retval = DosOpen(NAMEDPIPE,&pipehandle,&action,
           0L,FILE_NORMAL,FILE_OPEN, OPEN_SHARE_DENYNONE |
              OPEN_ACCESS_READWRITE, 0L))
    {
    printf("%u: Error %u trying to open %s,
    sleeping...\n",pid,retval,NAMEDPIPE);
                 if(retval == ERROR_PIPE_BUSY)
        while(TRUE)
        {
        if(!(retval = DosWaitNmPipe(NAMEDPIPE,1000L)))
        break;
        if(retval != ERROR_SEM_TIMEOUT)
            error_exit(retval,"DosWaitNmPipe");
        }
    printf("Trying again...\n");
    }
              else
    break;
               }

      printf("Client %u: server has connected pipe,
            sending message...\n", pid);

      while(TRUE)
    {
        strcpy(writebuf,"Anything I want?"); /* write a message */
        if(retval = DosWrite(pipehandle,writebuf,
           strlen(writebuf)+1,&bytesread))
              error_exit(retval,"DosWrite");

    /* read acknowledgment */
    if(retval = DosRead(pipehandle,writebuf,PIPESIZE,&bytesread))
    error_exit(retval,"DosRead");
    printf("%u: Server has acknowledged: \'%s\'\n",pid,writebuf);
    DosSleep(200L);
    }
      DosClose(pipehandle);
      DosExit(EXIT_PROCESS,0);
       }

PID Os2GetPid(void)    /* returns process id */
       {
      PIDINFO pidinfo;

      DosGetPID(&pidinfo);   /* get process id */
      return pidinfo.pid;    /* return it */
       }

void error_exit(USHORT err, char *msg)
       {
      printf("Error %u returned from %s\n",err,msg);
      DosExit(EXIT_PROCESS,0);
       }

Figure C

NPRMODE

# make file for nprmode.c
#

#COPT=/W3 /Zp /Zl /G2s /Ox /I$(INCLUDE)
COPT=/W3 /Zpiel /G2 /I$(INCLUDE)

nprmode.exe:    nprmode.c nprmode.mak
    cl $(COPT) nprmode.c /link /co

NPRMODE.C

/* nprmode.c RHS 5/1/89
 This program opens the named pipe created by NPSERVER from the DOS
 compatibility environment. After starting NPSERVER in a protected
 mode session, you can bring the DOS box session into the foreground
 and run this program with:

 NPRMODE
*/

#include<string.h>
#include<stdio.h>
#include<stdlib.h>
#include<fcntl.h>
#include<io.h>
#include<share.h>
#include<sys\types.h>
#include<sys\stat.h>

#include"nmpipe.h"

#if !defined(TRUE)
#define TRUE 1
#endif

char message[80];

void main(void);

void main(void)
      {
      char *mess = "Message from the DOS box!";
      int pipehandle, err, count = strlen(mess);

      if((pipehandle = sopen(NAMEDPIPE,(O_BINARY | O_RDWR),
           SH_DENYNO)) == -1)
    {
    printf("sopen failed, errno = %d\n",errno);
    exit(0);
    }

      while(TRUE)
    {
    strcpy(message,mess);

    if((err = write(pipehandle,message,count)) == -1)
    {
    printf("write failed, errno = %d\n",errno);
    exit(0);
    }

         count = 79;
         if((err = read(pipehandle,message,count)) == -1)
    {
    printf("read failed, errno = %d\n",errno);
    exit(0);
    }
         if(err)
    {
    message[err] = NULL;
    printf("Message received from the protected world:
        \"%s\"\n",message);
    }
         else
    printf("No message from protected world\n");
         }
      close(pipehandle);
      exit(0);
       }

Figure D

/* nmpipe.h RHS 5/1/89
shared header file for named pipe source */

#define    NAMEDPIPE       "\\PIPE\\NMDPIPE"
#define    QUIT_COMMAND     "QUIT"
#define    ACKNOWLEDGE      "You got it!"
#define    PIPESIZE         100



Find FIles Under Presentation Manager and Windows with a Handy Utility

Kevin P. Welch

Increased disk capacity has made searching for lost or misplaced files a
difficult task. Locating a small file on a system that uses several large
hard disks and a network file server can be time-consuming, since the file
could be tucked away in some long-forgotten subdirectory or on some
seldom-used volume. I solved this problem when I was working in the
MS-DOS(R) operating system by using several commercial and public domain
utilities designed to assist in managing disks and their constantly
expanding contents. When my primary computing environment switched from
MS-DOS1 to the Windows environment and then to OS/2 Presentation Manager,
few of those long-familiar utilities survived the transition. Faced with the
continued prospect of missing files, I decided to create a file management
utility to help solve this problem.

The File Finder program described here is a result of this work. It lets you
quickly search your hard disk for misplaced files in either Windows2 or OS/2
Presentation Manager. When you run FINDER.EXE, the dialog box in Figure 1
will be displayed.

The first field of interest in this dialog box is the search specification,
or pattern field. When you press the Search button, the program will search
all your fixed disk drives for files that satisfy the criteria (including
wild-card characters) you specified. The resulting list of files will then
be displayed with their respective subdirectory names, file size, and
creation date and times in the list box immediately below the search
specification field.

You can limit the search to a particular set of drives by selecting those of
interest from the multiple-selection list box in the lower right-hand corner
of the dialog box. Selecting fewer drives will speed up the search process
(especially when excluding network drives) and potentially reduce the number
of files found.

As an additional benefit, the File Finder allows you to select listed files
and load them, much as you would using the Windows MS-DOS Executive or the
OS/2 File Manager. Each time you select a file, the File Finder
automatically determines if the file is a directly executable program or, in
the case of the Windows version, if it contains data supported by some other
application.

In the Windows version, the actual classification of each file is determined
by parsing the contents of the Windows initialization file WIN.INI. If the
file extension is listed as a Windows program (usually EXE, COM, and BAT
files), it will be considered directly executable:

[windows]
programs=exe com bat

If the file is known to the system as data, the corresponding Windows
application will be automatically associated with the file using the full
pathname as its first parameter:

[extensions]
wri=write.exe ^.wri
art=scrap.exe ^.art
crd=cardfile.exe ^.crd

If there is insufficient memory, both the Windows and OS/2 versions of File
Finder will generate a warning message informing you that insufficient
memory is available to complete the requested search operation. You can
correct this situation by providing a more detailed search specification or
selecting fewer disk drives.

Creating the File Finder

To build the File Finder, use a Microsoft C Compiler Version 5.1 and the
Microsoft Windows Version 2.10 Software Development Kit or the Microsoft
OS/2 Version 1.10 Software Development Kit (SDK) to create the following
source files (see Figures 2a and 2b  for code fragments from the Windows and
Presentation Manager versions, respectively). The complete source files can
be downloaded from any of the MSJ bulletin boards.

FINDER make file
FINDER.H header file
FINDER.C source file
FINDER.RC resource file
FINDER.ICO icon file
FINDER.LNK link file
FINDER.DEF module file

Note that although the Windows and OS/2 versions of File Finder use a
similar-looking set of files, their actual contents are quite different.When
you have defined these files, using either version, you can build FINDER.EXE
by entering the MAKE FINDER command.

In most cases the compilation should proceed without any fanfare and you
will be well on your way to using the program. If you do have problems
compiling or linking the program, you might want to check your environment
variables, the search path, and the location of the required run-time
libraries used by the C compiler and the appropriate SDK.

Windows File Finder

The Windows version of the File Finder is a unique application in that it
doesn't have a main window function. In its place, the WinMain function (the
entry point for the application) brings up the File Finder dialog box. The
FinderDlgFn becomes in effect the main window function, assisted in part by
the Windows dialog box manager. If you read carefully through the
FinderDlgFn source code, you will observe that most of the work involves
initializing the dialog box, searching for files, and verifying whether a
selected file is a program or data.

When the dialog box is first displayed, the FinderDlgFn function will
receive a WM_INITDIALOG message. This message causes the dialog box to be
centered inside the screen and all system disk drives to be displayed in the
lower right-hand corner list box. As a convenience to the user, all disk
drives labeled C or above are automatically selected. Finally, the program
defines an icon for the File Finder dialog box,  using the SetClassWord
function.

This last operation might require a little further explanation. In Windows,
dialog boxes are based on a predefined window class, #32770. Each time you
create a dialog box, Windows creates another instance of this window using
the resource template you specified as a model. Each of the child windows
(known as controls) are automatically created and positioned inside this
window, according to the coordinates specified in the dialog box template.

Using dialog boxes saves you considerable development time, but the
overwhelming benefit comes from their highly refined keyboard user
interface. At times this user interface is so appealing that you might want
to use a dialog box as a top-level window. That is the technique used by the
File Finder.

Unfortunately, there are several drawbacks to this approach. The first and
perhaps most significant is that Windows automatically locks your
application data segment whenever a dialog box is displayed. Although there
are some good reasons for this, it does mean that an application based on a
top-level dialog box will always have a fixed data segment--something
that you normally try to avoid.

The second drawback involves the dialog box class icon. Since dialog boxes
cannot be made iconic in normal operation, Windows does not define an icon
for the #32770 window class. The File Finder application gets around this
limitation by manually defining an icon using the SetWindowWord function.
Although satisfactory in most situations, this approach creates another
unfortunate side effect: if any other application uses the same technique,
its dialog box icons will automatically change to reflect the most recently
defined one.

Unfortunately, the real solution is more complicated and requires a choice.
You could either hide the dialog box temporarily and create an iconic
window, or you could write your own dialog box subsystem that is not subject
to these limitations.

Following initialization of the dialog box, the next event of significance
to the File Finder is the receipt of the ID_SEARCH command. This command
resets the contents of the file list box and starts a recursive directory
search covering all the selected drives. Note that the redraw flag of the
file list box is temporarily set to FALSE. This speeds up the search
operation greatly by eliminating any unnecessary list box update operations.

The actual search operation is performed by the Directory function. This
function retrieves a directory entry for each normal file in the specified
subdirectory using the _dos_findfirst and _dos_findnext functions in the
Microsoft C run-time library. After all the files that satisfy the specified
criteria are found, the search is recursively repeated in each subdirectory.
This entire process is repeated until the whole drive has been searched or
the list box becomes full.

The last event of significance to the File Finder is the selection of a file
from the file list box. Whenever a file is selected, it is automatically
evaluated to determine if it is a program or data, as previously mentioned.
If as a result of this evaluation the Run button is enabled, the file can be
successfully loaded.

Double-clicking on the file or pressing the Run button will load either the
selected program or the application responsible for the information.
Although there are several mechanisms for starting other applications inside
Windows, the C run-time function spawnl is used in this particular case.

OS/2 File Finder

The OS/2 version of the File Finder (written by Jerry Weldon) is similar to
the Windows version, consisting of a single dialog box centered inside the
desktop. When  the dialog box is initially displayed, all fixed and network
disk drives are automatically selected.

The actual search operation is performed almost identically to the search in
Windows. Like the Windows version, the Directory function is called
recursively, successively searching each subdirectory for files that match
the search criteria until the entire drive has been traversed. This process
is repeated for each of the selected disk drives.

When a disk file is selected following a search operation, the File Finder
automatically retrieves the name and checks the extension. If the file has
an EXE extension, the Run button is enabled. OS/2 1.1 does not have a
facility to associate file extensions with applications, unlike Windows, but
this limitation will be removed by the High Performance File System to be
provided with the next release of OS/2.

The only other differences in the OS/2 version are the association of an
icon with the main dialog box and the use of a monospace font in the file
list box.

As with Windows, Presentation Manager dialog boxes are not normally
associated with icons. However,  a more reliable mechanism exists in
Presentation Manager to accomplish this task than in Windows. In the main
function, the dialog box is manually loaded, and an icon is explicitly
associated with it using a WM_SETICON message.

The use of a fixed pitch or monospace font in the file list box is
necessitated by the lack of tab stops to align the various columns. The list
box is declared in the resource file with the LS_OWNERDRAW style. Specifying
this style enables the list box owner to take control of some of the text
display and formatting operations normally handled by the control.

In this case the dialog box  (being the owner of the list box) receives a
WM_MEASUREITEM message when the list box is created. The dialog box responds
to this message by defining a monospace Courier font and returning a value
specifying the height of each text line. Each time an item is displayed in
the list box, the dialog box receives a WM_DRAWITEM message. The dialog box
message processing function intercepts this message, selects the Courier
font into the presentation space of the list box, and returns FALSE,
enabling the list box to display the text using the newly selected font.

Enhancements

After using the File Finder for a while and reviewing the source code, you
will undoubtedly come up with many ideas for enhancement. One that I like is
the inclusion of something akin to the File Finder in  every Open-File
dialog box. When using more than one application, I invariably need to open
other files located elsewhere on my computer. Rather than using the
Open-File dialog box to search manually for the desired file, I often bring
up the File Finder and start a quick search. Once the errant file is
located, I load it into the application using the Open-File dialog box.

Although there are many styles of Open-File dialog boxes in use today, most
could be easily adapted to incorporate file search capabilities. For
example, I incorporated the File Finder into one of my own dialog boxes in
the manner shown in Figure 3.

Ideas for enhancements usually occur as the need arises. You could extend
the program to support multiple search expressions, specific file
attributes, or hidden subdirectories. More complicated operations are
certainly possible and could be easily incorporated into either the Windows
or Presentation Manager version of the File Finder program.

Figure 2a

■
 ■
 ■
/* FinderDlgFn( hWnd, wMsg, wParam, lParam ) : BOOL
This function is responsible for processing all the messages relating to the
file finder dialog box. This mainly involves the definition and retrieval of
the various events generated by the user. */

 ■
 ■
 ■
      case ID_FILES : /* file selected */
         {
            int      nItem;
            PSTR     pszExt;
            BOOL     bEnable;
            char     szExt[16];
            char     szDir[64];
            char     szFile[64];
            char     szProg[64];

   /* process listbox notification code */
            switch( HIWORD(lParam) )
               {
            case LBN_SELCHANGE : /* selection is changing */

   /* initialization */
               bEnable = FALSE;

   /* reteve index to selected item */
               nItem = SendMessage( LOWORD(lParam), LB_GETCURSEL, 0,
                                    0L );
               if ( nItem >= 0 ) {

   /* retrieve text of selected item */
                  SendMessage(
                     LOWORD(lParam),
                     LB_GETTEXT,
                     nItem,
                     (LONG)(LPSTR)szFile
                  );

   /* continue if file name selected */
                  if ( szFile[0] == ' ' ) {

   /* retrieve current directory */
                      do {
                        SendMessage(
                           GetDlgItem(hDlg,ID_FILES),
                           LB_GETTEXT,
                           --nItem,
                           (LONG)(LPSTR)szDir
                        );
                     } while( szDir[0] == ' ' );

   /* define full path name */
                     sprintf(
                        szPath,
                        "%s\\%s",
                        szDir,
                        strtok(&szFile[2]," ")
                     );

   /* check if extension defined */
                     if ( strrchr(szPath,'.') ) {

   /* retrieve extension */
                        strcpy( szExt, strrchr(szPath,'.')+1 );

   /* retrieve list of program extensions */
                        GetProfileString(
                           "windows",
                           "programs",
                           "",
                           szProg,
                           sizeof(szProg)
                        );

    /* continue verification if not a program */
                        if ( !Present(szProg,szExt) ) {

   /* retrieve extension profile string */
                           GetProfileString(
                              "extensions",
                              szExt,
                              "",
                              szApp,
                              sizeof(szApp)
                           );

   /* check if extension listed */
                           if ( szApp[0] ) {
                              bEnable = TRUE;
                              *strchr(szApp,' ') = 0;
                           }

                         } else {
                           bEnable = TRUE;
                           strcpy( szApp, szPath );
                           szPath[0] = 0;
                        }

                     }

                  }

               }

    /* enable run button */
               EnableWindow( GetDlgItem(hDlg,ID_RUN), bEnable );

               break;
            case LBN_DBLCLK : /* double-click */

    /* perform run if button enabled */
               if ( IsWindowEnabled(GetDlgItem(hDlg,ID_RUN)) )
                 if ( spawnl(P_NOWAIT,szApp,szApp,szPath,NULL) >= 0 )
                     ShowWindow( hDlg, SW_MINIMIZE );

               break;
            default :  /* something else */
               break;
            }

         }
         break;
      case ID_SEARCH :  /* search using path */
      case IDOK :
         {
            WORD     wItem;
            WORD     wNumber;
            HWND     hListBox;
            BOOL     bContinue;
            HCURSOR  hOldCursor;
            char     szDrive[8];
            char     szPattern[64];
            char     szFileSpec[64];

   /* disable run button & change cursor to hourglass */
            EnableWindow( GetDlgItem(hDlg,ID_RUN), FALSE );
            hOldCursor = SetCursor( LoadCursor(NULL,IDC_WAIT) );

   /* retrieve search pattern & erase current file list */
            GetDlgItemText( hDlg, ID_PATTERN, szPattern,
                            sizeof(szPattern) );

            SendMessage( GetDlgItem(hDlg,ID_FILES), WM_SETREDRAW,
                         FALSE, 0L );
            SendMessage( GetDlgItem(hDlg,ID_FILES), LB_RESETCONTENT,
                         0, 0L );

   /* perform search on each selected drive */
            bContinue = TRUE;
            hListBox = GetDlgItem( hDlg, ID_DRIVES );
            wNumber = (WORD)SendMessage( hListBox, LB_GETCOUNT, 0,
                       0L );

            for ( wItem=0; (bContinue)&&(wItem<wNumber); wItem++ )
               if ( SendMessage(hListBox,LB_GETSEL,wItem,0L) )
   {

    /* define file specification */
                  SendMessage( hListBox, LB_GETTEXT, wItem,
                               (LONG)(LPSTR)szDrive );
                  sprintf( szFileSpec, "%c:\\%s", szDrive[2],
                           szPattern );

   /* create directory listing using pattern */
                  bContinue = Directory( szFileSpec, _A_NORMAL,
                                    GetDlgItem(hDlg,ID_FILES) );

               }

    /* activate redrawing */
            SendMessage( GetDlgItem(hDlg,ID_FILES), WM_SETREDRAW,
                         TRUE, 0L );

    /* check for null search operation */
            if ( SendMessage( GetDlgItem(hDlg,ID_FILES), LB_GETCOUNT,
                 0, 0L ) == 0L )
               MessageBox( hDlg, "No files found matching criteria!",
                           "File Finder", MB_OK|MB_ICONHAND );

    /* restore cursor & repaint display */
            SetCursor( hOldCursor );
            InvalidateRect( GetDlgItem(hDlg,ID_FILES), NULL, TRUE );

         }
         break;
      case ID_RUN :  /* run selection */

    /* start application & make finder iconic */
         if ( spawnl(P_NOWAIT,szApp,szApp,szPath,NULL) >= 0 )
            ShowWindow( hDlg, SW_MINIMIZE );

         break;
      case ID_QUIT :  /* close dialog */
      case IDCANCEL :
         EndDialog( hDlg, TRUE );
         break;
      default :  /* something else */
         break;
      }

      break;
   default :
      bResult = FALSE;
      break;
   }

   /* return result */
   return( bResult );

}

Figure 2b

■
 ■
 ■
/* FinderDlgProc This function is responsible for processing all the
messages which relate to the file finder dialog box. This mainly involves
the definition and retrieval of the various events generated by the user. */
 ■
 ■
 ■
  /* define search pattern */
        WinSetDlgItemText( hwnd, IDD_PATTERN, "*.*" );
  /* initialize drive list */
        DosQCurDisk( &usCurDrive, &flDrives );
        szDrive[0] = '[';
        szDrive[1] = '-';
        szDrive[3] = '-';
        szDrive[4] = ']';
        szDrive[5] = '\0';
        for ( szDrive[2] = 'A'; szDrive[2] <= 'Z'; szDrive[2]++ )
         {
          if ( flDrives & 1 )
            WinSendDlgItemMsg(
              hwnd,
              IDD_DRIVES,
              LM_INSERTITEM,
              MPFROMSHORT( LIT_END ),
              MPFROMP( szDrive )
            );
          flDrives >>= 1;
        }
   /* select all fixed drives */
        hwndListBox = WinWindowFromID( hwnd, IDD_DRIVES );
        cszList = SHORT1FROMMR( WinSendMsg(
          hwndListBox,
          LM_QUERYITEMCOUNT,
          0L,
          0L
        ) );
        for ( iItem = 0; iItem < cszList; iItem++ )
         {
          WinSendMsg(
            hwndListBox,
            LM_QUERYITEMTEXT,
            MPFROM2SHORT( iItem, sizeof(szDrive) ),
            MPFROMP( szDrive )
          );
          if ( szDrive[2] >= 'C' )
            WinSendMsg(
              hwndListBox,
              LM_SELECTITEM,
              MPFROMSHORT( iItem ),
              MPFROMSHORT( TRUE )
            );
        }
      }
      break;

    case WM_CONTROL:
      switch ( SHORT1FROMMP( mp1 ) ) {

        case IDD_PATTERN:
          if ( SHORT2FROMMP( mp1 ) == EN_CHANGE )
  /* enable/disable search button */
            WinEnableWindow(
              WinWindowFromID( hwnd, IDD_SEARCH ),
              WinQueryWindowTextLength(
                WinWindowFromID( hwnd, IDD_PATTERN )
              )
            );
          else
            fPassToDef = TRUE;
          break;

        case IDD_FILES:
          switch ( SHORT2FROMMP( mp1 ) ) {

            case LN_SELECT:   /* an item was selected */
              {
                USHORT    iSelection;   /* selected file index */
                CHAR      szFile[64];   /* name of selected file */
                USHORT    cchFile;      /* file name length */
                BOOL      fExecable;    /* is file executable? */

  /* get name of selected file */
                fExecable = FALSE;
                iSelection = SHORT1FROMMR( WinSendDlgItemMsg(
                  hwnd,
                  IDD_FILES,
                  LM_QUERYSELECTION,
                  MPFROMSHORT( LIT_FIRST ),
                  0L
                ) );
                if ( iSelection != LIT_NONE ) {
                  WinSendDlgItemMsg(
                    hwnd,
                    IDD_FILES,
                    LM_QUERYITEMTEXT,
                    MPFROM2SHORT( iSelection, sizeof(szFile) ),
                    MPFROMP( szFile )
                  );
   /* is file executable? */
                  if ( szFile[0] == ' ' )
                   {
                    cchFile = strlen( strtok( &szFile[2], " " ) );
                    if ( cchFile > 4
                     && strcmp( &szFile[2+cchFile-4], ".EXE" ) == 0 )
                      fExecable = TRUE;
                  }
                }
  /* if file is executable, enable run button */
                WinEnableWindow(
                  WinWindowFromID( hwnd, IDD_RUN ),
                  fExecable
                );
              }
              break;

            case LN_ENTER: /* equivalent to run button clicked */
              WinSendDlgItemMsg( hwnd, IDD_RUN, BM_CLICK, 0L, 0L );
              break;

            default:
              fPassToDef = TRUE;
              break;
          }
          break;

        default:
          fPassToDef = TRUE;
          break;
      }
      break;

    case WM_COMMAND:
      switch ( COMMANDMSG( &usMsg )->cmd )
       {

        case IDD_SEARCH: /* search using path */
        case DID_OK:
          {
            HPOINTER  hptrOld;        /* handle to old cursor */
            USHORT    iItem;          /* temporary loop variable */
            HWND      hwndListBox;    /* handle to list box */
            BOOL      fContinue;      /* boolean search flag */
            CHAR      szDrive[8];     /* drive specification string */
            CHAR      szPattern[64];  /* search pattern string */
            CHAR      szFileSpec[64]; /* file specification string */

  /* disable run button & change cursor to hourglass */
            WinEnableWindow(WinWindowFromID( hwnd, IDD_RUN), FALSE );
            hptrOld = WinQueryPointer( HWND_DESKTOP );
            WinSetPointer(
              HWND_DESKTOP,
              WinQuerySysPointer( HWND_DESKTOP, SPTR_WAIT, FALSE )
            );
  /* retrieve search pattern & erase current
   file list */
            WinQueryDlgItemText(
              hwnd,
              IDD_PATTERN,
              sizeof(szPattern),
              szPattern
            );
            WinSendDlgItemMsg(hwnd, IDD_FILES, LM_DELETEALL, 0L, 0L);

  /* perform search on each selected drive */
            fContinue = TRUE;
            hwndListBox = WinWindowFromID( hwnd, IDD_DRIVES );
            iItem = LIT_FIRST;
            while (
              (iItem = SHORT1FROMMR( WinSendMsg(
                hwndListBox,
                LM_QUERYSELECTION,
                MPFROMSHORT( iItem ),
                0L ) )) != LIT_NONE
              && fContinue
            )
            {
  /* define file specification */
              WinSendMsg(
                hwndListBox,
                LM_QUERYITEMTEXT,
                MPFROM2SHORT( iItem, sizeof(szDrive) ),
                MPFROMP( szDrive )
              );
              sprintf(szFileSpec, "%c:\\%s", szDrive[2], szPattern );

  /* create directory listing using pattern */
              fContinue = Directory(
                szFileSpec,
                FILE_NORMAL,
                WinWindowFromID( hwnd, IDD_FILES )
              );
            }


 ■
 ■
 ■
        case IDD_RUN:
          {
            USHORT    iSelection;      /* selected file index */
            CHAR      szFile[64];      /* name of executable file  */
            CHAR      szDir[64];       /* name of directory */
            CHAR      szPath[64];      /* full path name of file  */
            CHAR      szFail[64];      /* if DosExecPgm fails */
            RESULTCODES resc;          /* result of child process  */
            USHORT    usErr;           /* error code  */

  /* get name of selected file */
            iSelection = SHORT1FROMMR( WinSendDlgItemMsg(
              hwnd,
              IDD_FILES,
              LM_QUERYSELECTION,
              MPFROMSHORT( LIT_FIRST ),
              0L
            ) );
            if ( iSelection != LIT_NONE ) {
              WinSendDlgItemMsg(
                hwnd,
                IDD_FILES,
                LM_QUERYITEMTEXT,
                MPFROM2SHORT( iSelection, sizeof(szFile) ),
                MPFROMP( szFile )
              );
  /* define full path name */
              do {
                WinSendDlgItemMsg(
                  hwnd,
                  IDD_FILES,
                  LM_QUERYITEMTEXT,
                  MPFROM2SHORT( --iSelection, sizeof(szDir) ),
                  MPFROMP( szDir )
                );
              } while ( szDir[0] == ' ' );
              sprintf(
                szPath,
                "%s\\%s",
                szDir,
                strtok( &szFile[2], " " )
              );

 ■
 ■
 ■



Writing Faster Graphics Programs by Using Offscreen Bitmap Manipulation

Kevin Ruddell

Working in an interactive environment, such as the OS/2 Presentation
Manager, it is not enough to write a program that produces correct output.
The length of the response time must also be considered. If it is too long,
users are likely to become frustrated. The ideal program, then, is one that
is snappy, responsive, and accurate.

This article considers an assortment of techniques you can use to make your
Graphics Programming Interface (GPI) Presentation Manager program run more
quickly. I will discuss them as they apply to a particular program,
JIGSAW.C, which implements a jigsaw puzzle. JIGSAW has gone through many
versions, starting with a rather slow feasibility test, but it is now a lot
of fun to play.

A recent version of JIGSAW was shipped with the Microsoft(R) OS/2 Software
Development Kit, Version 1.1, and with the Microsoft OS/2 Presentation
Manager Toolkit, Version 1.1.

To play JIGSAW, you load a bitmap from a file. JIGSAW uses BitBlt (bit block
transfer) with clip paths to create a collection of picture fragments that
are the puzzle pieces.

Each piece is associated with a single retained segment and an auxiliary
data structure, used for drawing and for selection with the mouse. To speed
the program's responsiveness to user requests, the real work is done in a
second thread, with work requests transmitted from the main thread in the
form of messages. This arrangement makes it possible for the user to
override a lengthy drawing operation with a higher-priority request (for
instance, program termination or magnification change).

To scroll and zoom in on or back from the puzzle image, JIGSAW changes the
default viewing transform. An individual piece is moved by changing its
segment transform. To minimize flicker (and increase speed), drawing
operations are generally done offscreen and the result copied to the screen.

Some of the data structures used in JIGSAW are shown in Figure 1. (Full
source code listings are available for downloading from any of the MSJ
bulletin boards--Ed.)Note the double-linked list used to record
piece-related data. Using the double-linked list, the program traverses each
piece in the puzzle starting at pslTail, going from the top to the bottom of
the scene (see Figures 2-5).

Clipping

The use of clipping in Presentation Manager limits the effect of Gpi or Win
calls to part of a picture. The clipping boundary can be specified in world
space, model space, presentation page space, and device space. The only form
of clipping that will be discussed here is the one in world space--the
clip path. A path is a closed boundary defined by Gpi drawing calls. You
define a clip path by defining a path, then calling GpiSetClipPath. The path
is specified in world space, and any subsequent drawing calls only change
the pels that lie within or on the clip path after translation to device
space (see Figures 6-8).

BitBlt

BitBlt is a high-performance method of copying bitmaps. In Presentation
Manager, there are two forms of BitBlt: GpiBitBlt and GpiWCBitBlt. Each
employs source and target rectangles, along with options for specifying the
mixing function and the compression mode. Translation and scaling factors
are applied to the bitmap so as to map the source rectangle to the
destination rectangle.

GpiBitBlt works entirely in device coordinates, whereas GpiWCBitBlt works in
device coordinates in the source and in world coordinates in the target. If
you are placing drawing orders in a retained segment, you must use
GpiWCBitBlt; but if you are not, you may use either form. The advantage of
using GpiWCBitBlt is that it automatically takes account of coordinate space
transforms; the advantage of GpiBitBlt is that it gives you device-level
control of where your BitBlt goes. Note that the source argument of
GpiWCBitBlt is a handle to a bitmap that must not be currently selected into
a presentation space, whereas the source argument to GpiBitBlt is a handle
to a presentation space that must have a bitmap selected into it (See Figure
9).

One further note on GpiWCBitBlt is that for OS/21 Versions 1.1 and 1.2, a
rotation in the transforms does not cause the data to be rotated. Instead,
the target rectangle is transformed to device space and is then replaced by
the smallest upright rectangle containing the transformed rectangle (see
Figures 10, 11, and 12).

Rectangles

With clipping in effect, increasing the boundaries of the BitBlt rectangles
beyond the dimensions of the clip path has no effect on the drawing except
to slow it down. An early version of a given program might ignore this fact
and use BitBlt to transfer the whole bitmap through the clip path. Once the
program is debugged, you can speed it up by trimming the source and target
rectangles. In the case of JIGSAW, such trimming caused a sixfold speedup in
an early version. Determining the bounding rectangles of a piece in model
space and in device space is illustrated in Figures 13 and 14.

Whenever the dimensions of the BitBlt source and target rectangles in device
coordinates differ, BitBlt must spend extra time stretching or compressing
the bitmap. You can eliminate this extra time by seeing to it that the
source and target rectangles are the same size. This is easy to do with
GpiBitBlt, since both rectangles are expressed in device coordinates. When
using GpiWCBitBlt, you can use coordinate conversion to see how a rectangle
in world space maps to a rectangle in device space.

Because of  rounding effects, it is not always the case in OS/2 Versions 1.1
and 1.2 that the device space rendering of a rect-angle in a segment retains
precisely the same dimensions when the translation part of the segment
transform applied to it (lM31 and lM32) is changed. Changing the size by
even one pel causes stretching or compression to occur. In the case of
JIGSAW, therefore, if you move a piece around the screen by changing its
segment transform and then use GpiWCBitBlt, sometimes the piece will be
drawn quickly and sometimes slowly.

Using Memory Alignment

You can determine the horizontal and vertical alignment factors for the
display driver by calling WinQuerySysValue with SV_CXBYTEALIGN and
SV_CYBYTEALIGN, respectively. BitBlt copies data between memory locations,
and you can speed up the transfer by aligning your data on the appropriate
memory boundaries. If you are doing a BitBlt to the screen and the sides of
your tightest-possible rectangle are not memory-aligned, you can expand them
until they are aligned. Note that, in the case of the screen, the alignment
should be performed using desktop window-relative coordinates (see Figure
15).

On most currently available hardware, main memory accesses are significantly
faster than display memory accesses. For that reason, and also to minimize
flicker, it is often desirable to perform drawing operations off the screen
and use BitBlt to copy the result to the screen.

In JIGSAW, when a piece is selected, the offscreen image is erased in a
tight box around the selected piece. Then, all nonselected pieces
overlapping the erased area are redrawn. This intermediate state is saved
for later use, and the selected piece is drawn on top. If this erasing and
drawing is done on the screen, the user sees the intermediate states of
erasure and healing (redrawing of the screen) in stages. This is rather ugly
(see Figure 16). However, if the offscreen intermediary is used, the user
just sees the selected piece appear on the surface, with no other
disturbance to the image.

When you scroll using WinScrollWindow, you should advance by multiples of
the display driver alignment factor whenever possible. This allows the
Presentation Manager to move the bitmap data in memory-aligned chunks.

Clip Paths and Masks

If performing a BitBlt through a clip path gives you what you want and, as
in the case of JIGSAW, you will reuse the result many times, you should
consider using masks. A mask is a bitmap that is AND'ed or OR'ed to another
bitmap in order to change part of the other bitmap and leave the rest
undisturbed. The idea is that you do the work of performing BitBlt through a
clip path twice to prepare masks. These masks can then be used for BitBlts
without clipping, stretching, or compression, achieving the same result as
previously but with much greater speed.

The first mask is created by filling the mask with 1s, using BitBlt with the
raster operation set to ROP_ONE and no clip path. Then, a clip path is
defined and filled with 0s by means of BitBlt with the raster operation set
to ROP_ZERO. When BitBlt is used to copy this bitmap to the screen with a
raster operation of ROP_SRCAND (logical AND), the effect is to punch a hole
in the screen bitmap in the shape of the clip path.

The second mask is created by filling the mask with 0s, using BitBlt with
the raster operation set to ROP_ZERO and no clip path. A clip path is
defined and filled with data, using BitBlt with the raster operation set to
ROP_SRCCOPY. BitBlt will copy this bitmap to the screen with a raster
operation of ROP_SRCPAINT (logical OR), so it fills the hole in the screen
bitmap left by the first mask (see Figure 17).

Segments

Gpi-retained segments are a high-level programming construct; they allow you
to store selected Gpi calls in numbered bundles for later use. Capable of
being created, edited, and deleted, the segments have multiple uses. In
JIGSAW, they are used for correlation; in earlier versions they were used in
painting, discussed below.

The pieces in JIGSAW are drawn to fit together in world space, with one
piece assigned to each retained segment. Initially, the segment transforms
are set to identity (no scaling, rotation, or translation). When a piece is
moved, the translation part of its segment transform (lM31 and lM32) is
changed to reflect the desired displacement.

Correlation

In JIGSAW, the user moves a piece by moving the mouse pointer on top of it,
depressing the left mouse button, dragging the mouse pointer to the new
location, and releasing the left mouse button. The process of converting the
mouse coordinates when the mouse button is clicked to the identifier of a
particular segment is an example of correlation. To correlate, you supply a
correlation point to OS/2 Presentation Manager (PM). PM creates an imaginary
rectangle (the pick aperture) centered on the correltion point. PM then
simulates drawing segments and reports when it draws inside the pick
aperture. In JIGSAW, the pick aperture is limited to the correlation
point--which is a one-by-one rectangle set by GpiSetPickApertureSize.
OS/2 runs through the drawing orders in the indicated segments and registers
a hit for those segments that draw within the pick aperture when a call is
being made to GpiCorrelateChain,   GpiCorrelateFrom, or GpiCorrelateSegment.

In the case of JIGSAW, there are typically no more than one or two pieces
under any given point on the screen. The most straightforward approach is to
make each segment part of the segment chain and then call GpiCorrelateChain.
This approach is simple, and you may want to use it in an early version of
your program to get it working, but it wastes time, since it involves
running through all of the drawing orders in all of the segments to see if
one of them intersects the pick aperture. A cleverer approach is to keep
track of the bounding box for each segment and only do a correlation check
on the segments whose bounding boxes intersect the pick aperture. For this
purpose, the double-linked list in priority order is useful. JIGSAW runs
through the list from high to low priority, applying the bounding box test
to each segment and, if necessary, calling GpiCorrelateSegment (see Figure
18).

A refinement of this method of correlation does away with the call to
GpiCorrelateSegment by using the knowledge of the shape of each piece stored
in the masks. The hole-punching mask contains 1s in the background and 0s in
the foreground, so it is easy to tell whether  a given pel is on a piece
(see Figure 19).

As with correlation, there is no point in executing drawing orders that are
clipped to produce no visible output. If part of the picture is being
healed, say, as part of selecting a piece, then a simple visibility test
using the bounding box of each piece will tell you if there is a need to
draw it (see Figure 20).

Transforms

In OS/2 Versions 1.1 and 1.2, the linear translation components of
transforms are stored internally to a higher precision than is available to
an application. This extra internal precision minimizes rounding errors when
combining transforms with the TRANSFORM_ADD or TRANSFORM_PREEMPT options.
Another effect of higher internal precision is that if you call
GpiQueryDefaultViewMatrix  to obtain the current default viewing
transformation matrix and immediately call GpiSetDefaultViewMatrix  with the
same matrix and the TRANSFORM_REPLACE option, you may actually change the
default viewing transformation. In the case of JIGSAW, this change produced
a one-pel difference in alignment between some parts of the image drawn
before and after the change. The remedy was to create the default viewing
transformation by means of incremental updates and immediately call
GpiQueryDefaultViewMatrix and GpiSetDefaultViewMatrix, discarding the extra
internal precision, prior to any drawing (see Figure 21).

Second Message Queue

In an interactive application, it is important that the system stay
responsive to user requests. In the early versions of JIGSAW, some of the
drawing operations took many seconds to complete. To solve this problem, a
second thread should be used to do the actual drawing, with the primary
thread's message queue receiving the original requests--and that in
fact was done in later versions. The primary thread can then interrupt
drawing or other lengthy operations as needed and recovery can proceed in an
orderly fashion.

Messages provide a convenient and simple means of transferring requests
between threads. In JIGSAW, the second thread has a message queue that
receives messages posted by the primary thread.

Synchronization

Care must be taken in accessing the memory shared by the primary and
secondary threads. When possible, variables should be accessed by one thread
or the other, but not by both. Semaphores should be used when access to
shared memory is required. RAM semaphores themselves can serve as flags.
These flags can be freely accessed by both threads by means of OS/2
semaphore calls (see Figure 20).

Apart from the critical section issues of accessing shared variables, you
also need to preserve the serial nature of a user's drawing requests. If
some operations that happen to be fast are handled by the primary thread,
and others are put in the secondary thread, the user may see out-of-sequence
updates to the screen, producing a result that is messy or incorrect, or
both.

Merging Messages

The most straightforward scheme for handling messages is to retrieve each
message from the queue with WinGetMsg and process it. In some cases,
however, there will be several consecutive messages all of the same type
waiting in the queue, and their combined effect can be quickly determined
(for example, throw away all but the last mouse move message). You can use
WinPeekMsg with the PM_NOREMOVE option to examine the queue without
disturbing it, make a decision, and then remove selected entries from the
queue using WinPeekMsg with the PM_REMOVE option.

Using JIGSAW as a guide and implementing the techniques I've presented, you
should be able to make your GPI programs run more quickly. Because JIGSAW
now uses small, equal-sized source and target rectangles with GpiBitBlt,
scrolls in memory-aligned steps, draws with masks,  uses bounding box tests
for painting and correlation, and merges messages in its second message
queue, it is much faster than before. For example, the first version of
JIGSAW took 32 seconds to correlate a mouse click, erase the selected piece,
and heal the erased area. Later versions took only a fraction of a second to
complete the same operation. The moral of the story is that when wisely
used, PM can yield excellent performance, not to mention happy users.

Figure 1

/*---------- inter-thread messages -------------------*/

#define UM_DRAW    WM_USER+2    /* draw the current picture */
#define UM_SIZING    WM_USER+5    /* perform sizing by
        recalculating the default
        viewing transform */
#define UM_ZOOM    WM_USER+6    /* zoom the picture by
        recalculating the default
        viewing transform */
#define UM_REDRAW    WM_USER+8    /* redraw the entire client
        window */
#define UM_JUMBLE    WM_USER+9    /* scatter the pieces on the
        window */
#define UM_LEFTDOWN    WM_USER+11    /* correlate and prepare to
        drag */
#define UM_MOUSEMOVE    WM_USER+12    /* remove, reposition, and
        redraw */

/*-------------------- segment list ------------------------*/

typedef struct _SEGLIST {    /* sl */
  LONG    lSegId;    /* segment identifier */
  struct _SEGLIST FAR * pslPrev;    /* points to lower-priority
        neighbor */
  struct _SEGLIST FAR * pslNext;    /* points to higher-priority
        neighbor */
  POINTL    ptlLocation;    /* piece location, world
        coordinates */
  RECTL    rclCurrent;    /* segment bounding box, model
        coords */
  RECTL    rclBitBlt;    /* segment bounding box, world
        coords */
  POINTL    aptlBitBlt[4];    /* BitBlt parameters */
  LONG    lAdjacent[8];    /* adjacent segments in original
        pict */
  POINTL    aptlSides[12];    /* control points for piece
        outline */
  HDC    hdcHole;    /* punch hole in target */
  HPS    hpsHole;
  HBITMAP    hbmHole;
  HDC    hdcFill;    /* used to fill hole */
  HPS    hpsFill;
  HBITMAP    hbmFill;
} SEGLIST ;
typedef  SEGLIST FAR  *PSEGLIST;    /* psl */
typedef PSEGLIST FAR *PPSEGLIST;    /* ppsl */

PSEGLIST pslHead   = NULL;    /* head of the list */
PSEGLIST pslTail   = NULL;    /* tail of the list */
PSEGLIST pslPicked = NULL;    /* picked segment's list
        member */

/*------------------- Miscellaneous ----------------------------*/

SEL       selStack;    /* async thread stack selector */

POINTL    ptlOffset;    /* ptlBotLeft conv to device
    coords */
POINTL    ptlBotLeft  = { 0, 0};    /* puzzle boundaries, world
    space */
POINTL    ptlTopRight = { 3000, 3000};

HPS           hpsBitmapSize=NULL;    /* bitmap sized to the current
    size */
HDC           hdcBitmapSize=NULL;
HBITMAP       hbmBitmapSize=NULL;
BITMAPINFOHEADER   bmpBitmapSize={12L, 0, 0, 0, 0};

HPS           hpsBitmapBuff=NULL;    /* image composed here, copied to
    scrn */
HDC           hdcBitmapBuff=NULL;
HBITMAP       hbmBitmapBuff=NULL;
BITMAPINFOHEADER   bmpBitmapBuff={12L, 0, 0, 0, 0};
LONG    lByteAlignX, lByteAlignY;    /* memory alignment constants */

Figure 8

PSEGLIST  psl;
POINTL    aptlSides[12];

    ■
    ■
    ■

GpiBeginPath( psl->hpsHole, 1L);    /* define a path */
GpiMove( psl->hpsHole, &aptlSides[11]);
GpiPolySpline( psl->hpsHole, 12L, aptlSides);
GpiEndPath( psl->hpsHole);
GpiSetClipPath( psl->hpsHole, 1L, SCP_AND);    /* make it the clip
        path */
GpiBitBlt( psl->hpsHole    /* fill it with zeros */
     , NULL
     , 2L
     , &psl->aptlBitBlt[2]
     , ROP_ZERO
     , BBO_IGNORE);

Figure 9

/* create a shadow bitmap of the original, set to current output size */

  ptlOffset = ptlBotLeft;    /* hottom left corner in device
    coord */
  GpiConvert( hpsClient, CVTC_WORLD, CVTC_DEVICE, 1L, &ptlOffset);

  aptl[0] = ptlBotLeft;    /* current output rect, device
    coord */
  aptl[1] = ptlTopRight;
  GpiConvert( hpsClient, CVTC_WORLD, CVTC_DEVICE, 2L, aptl);

  aptl[0].x -= ptlOffset.x;    /* put bottom left at (0,0) */
  aptl[0].y -= ptlOffset.y;
  aptl[1].x -= ptlOffset.x - 1;    /* correct for coordinate
    conversion */
  aptl[1].y -= ptlOffset.y - 1;
  aptl[2].x = 0L;
  aptl[2].y = 0L;
  aptl[3].x = bmpBitmapFile.cx;    /* bitmap dimensions */
  aptl[3].y = bmpBitmapFile.cy;
  GpiSetBitmap( hpsBitmapSize, hbmBitmapSize);
  GpiBitBlt( hpsBitmapSize    /* copy the bitmap */
       , hpsBitmapFile
       , 4L
       , aptl
       , ROP_SRCCOPY
       , BBO_IGNORE);

Figure 13

/* Determine the bounding rect of a segment in model space. */

  VOID SetRect ( PSEGLIST psl)
  {
  MATRIXLF  matlf;

  GpiResetBoundaryData( hpsClient);
  GpiSetDrawControl( hpsClient, DCTL_DISPLAY, DCTL_OFF);
  GpiSetDrawControl( hpsClient, DCTL_BOUNDARY, DCTL_ON);
  GpiDrawSegment( hpsClient, psl->lSegId );
  GpiSetDrawControl( hpsClient, DCTL_BOUNDARY, DCTL_OFF);
  GpiSetDrawControl( hpsClient, DCTL_DISPLAY, DCTL_ON);
  GpiQueryBoundaryData( hpsClient, &(psl->rclCurrent));
  }
/* Determine the bounding rect of a segment in model space. Use special
knowledge that the only non-identity part of the segment transform is the
translation part. */

  VOID SetRect ( PSEGLIST psl)
  {
  MATRIXLF  matlf;

  GpiQuerySegmentTransformMatrix( hpsClient    /* get model xform of
                , psl->lSegId    segment */
                , 9L
                , &matlf );
  psl->rclCurrent = psl->rclBitBlt;    /* world space bounding
    rect  */
  psl->rclCurrent.xLeft   += matlf.lM31;    /* apply translation */
  psl->rclCurrent.yBottom += matlf.lM32;
  psl->rclCurrent.xRight  += matlf.lM31;
  psl->rclCurrent.yTop    += matlf.lM32;
  }

Figure 14

RECTL    rcl;
PSEGLIST psl;

SetRect( psl);
rcl = psl->rclCurrent;   /* get model space bounding box of piece  */
GpiConvert( hpsClient, CVTC_MODEL, CVTC_DEVICE, 2L, (PPOINTL)&rcl);
rcl.xRight++;        /* adjust rectangle for conversion to device
            space */
rcl.yTop++;

Figure 15

#define ROUND_DOWN_MOD( arg, mod)    \
    {    \
          if( arg < 0)    \
          {                         \
     arg -= mod - 1;    \
           arg += arg % mod;    \
          } else    \
              arg -= arg % mod;    \
    }

#define ROUND_UP_MOD( arg, mod)    \
    {    \
          if( arg < 0)    \
          {    \
              arg -= arg % mod;    \
          } else    \
          {    \
              arg += mod - 1;    \
              arg -= arg % mod;    \
          }    \
    }

  lByteAlignX = WinQuerySysValue( HWND_DESKTOP, SV_CXBYTEALIGN);
  lByteAlignY = WinQuerySysValue( HWND_DESKTOP, SV_CYBYTEALIGN);

  WinMapWindowPoints( hwndClient, HWND_DESKTOP, aptl, 2);
        /* client to scrn */
  ROUND_DOWN_MOD( aptl[0].x, lByteAlignX);    /* round down */
  ROUND_DOWN_MOD( aptl[0].y, lByteAlignY);    /* round down */
  ROUND_UP_MOD(   aptl[1].x, lByteAlignX);    /* round up */
  ROUND_UP_MOD(   aptl[1].y, lByteAlignY);    /* round up */
  WinMapWindowPoints( HWND_DESKTOP, hwndClient, aptl, 2);
        /* scrn to client */
  aptl[2] = aptl[0];        /* no shift */
  GpiBitBlt( hpsClient        /* memory to screen */
       , hpsBitmapBuff
       , 3L
       , aptl
       , ROP_SRCCOPY
       , BBO_IGNORE );

Figure 17

VOID DrawPiece( hps, psl)

HPS      hps;
PSEGLIST  psl;
{
    POINTL    aptl[4];
    MATRIXLF  matlf;

    if( GpiQuerySegmentAttrs( hpsClient,
        psl->lSegId, ATTR_VISIBLE) == ATTR_ON)
    {
      GpiQuerySegmentTransformMatrix( hpsClient      /* get segment
        , psl->lSegId        transform */
                    , 9L
                    , &matlf );
      aptl[2].x = aptl[2].y = 0L;
      aptl[3] = psl->aptlBitBlt[3];
      aptl[0].x = psl->rclBitBlt.xLeft     + matlf.lM31;
      aptl[0].y = psl->rclBitBlt.yBottom + matlf.lM32;
      GpiConvert( hpsClient, CVTC_MODEL, CVTC_DEVICE, 1L, aptl);
      aptl[1].x = aptl[0].x + aptl[3].x;
      aptl[1].y = aptl[0].y + aptl[3].y;
      GpiBitBlt( hps                /* punch a hole */
           , psl->hpsHole
           , 4L
           , aptl
           , ROP_SRCAND
           , BBO_IGNORE );
      GpiBitBlt( hps                /* fill the hole */
           , psl->hpsFill
           , 4L
           , aptl
           , ROP_SRCPAINT
           , BBO_IGNORE );
    }
}

Figure 18

LONG      alSegTag[HITS][DEPTH][2];
  RECTL     rcl;
  POINTL    ptl;

  ptl.x = (LONG)(SHORT)SHORT1FROMMP( mp);
  ptl.y = (LONG)(SHORT)SHORT2FROMMP( mp);

/* scan segment list in order of decreasing priority, looking for a hit */

  for( pslPicked = pslTail; pslPicked != NULL;
  pslPicked = pslPicked->pslPrev)
  {
    rcl = pslPicked->rclCurrent;
    GpiConvert( hpsClient, CVTC_MODEL, CVTC_DEVICE, 2L, (PPOINTL)&rcl);
    rcl.xRight++;
    rcl.yTop++;
    if( WinPtInRect( habAsync, &rcl, &ptl))    /* is point in
                        bounding box? */
    if( 0L < GpiCorrelateSegment( hpsClient
                     , pslPicked->lSegId
                     , PICKSEL_VISIBLE
                     , &ptl
                     , HITS
                     , DEPTH
                     , (PLONG)alSegTag ))
        break;                   /* got a hit */
  }
  if( pslPicked)
    lPickedSeg     = pslPicked->lSegId;

Figure 19

POINTL    aptl[2];
  LONG      lColor;
  POINTL    ptl;
  RECTL     rcl;

  ptl.x = (LONG)(SHORT)SHORT1FROMMP( mp);
  ptl.y = (LONG)(SHORT)SHORT2FROMMP( mp);

/* scan segment list in order of decreasing priority, looking for a hit */

  aptl[0] = aptl[1] = ptl;
  aptl[1].x++;
  aptl[1].y++;
  GpiBitBlt( hpsBitmapSave, NULL, 2L, aptl, ROP_ONE, BBO_IGNORE);
  lColor = GpiQueryPel( hpsBitmapSave, &ptl);
  for( pslPicked = pslTail; pslPicked != NULL;
  pslPicked = pslPicked->pslPrev)
  {
    rcl = pslPicked->rclCurrent;
    GpiConvert( hpsClient, CVTC_MODEL, CVTC_DEVICE, 2L, (PPOINTL)&rcl);
    rcl.xRight++;
    rcl.yTop++;
    if( WinPtInRect( habAsync, &rcl, &ptl))    /* is point in
                        bounding box? */
    {
    DrawPiece( hpsBitmapSave, pslPicked, FALSE);
    if( GpiQueryPel( hpsBitmapSave, &ptl) != lColor)
        break;                   /* got a hit */
    }
  }
  if( pslPicked)
    lPickedSeg     = pslPicked->lSegId;

Figure 20

BOOL DoDraw( hps, hrgn, fPaint)

HPS    hps;
HRGN   hrgn;
BOOL   fPaint;
{
  HRGN      hrgnOld;
  RECTL     rcl, rclRegion, rclDst, rclClient;
  PSEGLIST  psl;

  if( fPaint)
  {
    GpiSetColor( hps, CLR_BACKGROUND);
    GpiPaintRegion( hps, hrgn);         /* erase region */
  }
  GpiQueryRegionBox( hps, hrgn, &rclRegion);
  WinQueryWindowRect( hwndClient, &rclClient);
  if( !WinIntersectRect( habAsync, &rclRegion, &rclRegion,
      &rclClient))
    return( FALSE);                /* not in client window */
  GpiSetClipRegion( hps, hrgn, &hrgnOld);  /* make the clip region */
  for( psl = pslHead; psl != NULL; psl = psl->pslNext)  /* scan all
                            segments */
  {

/* get the piece bounding box in device coordinates */

    rcl = psl->rclCurrent;
    GpiConvert( hpsClient, CVTC_MODEL, CVTC_DEVICE, 2L, (PPOINTL)&rcl);
    rcl.xRight++;
    rcl.yTop++;

/* if the piece might be visible, and drawing allowed, draw the piece */

    if( WinIntersectRect( habAsync, &rclDst, &rcl, &rclRegion))
    if( DosSemWait( hsemDrawOn, SEM_IMMEDIATE_RETURN))
      DrawPiece( hps, psl);

    else
      break;
  }
  GpiSetClipRegion( hps, NULL, &hrgnOld); /* clear the clip region */

  return( TRUE);
}

Figure 21

/* set the default viewing transform  */
VOID SetDVTransform( fx11, fx12, fx21, fx22, l31, l32, lType)

FIXED    fx11, fx12, fx21, fx22;
LONG    l31, l32, lType;
{
  MATRIXLF  matlf;

  matlf.fxM11 = fx11;
  matlf.fxM12 = fx12;
  matlf.lM13  = 0L;    /* must be 0    */
  matlf.fxM21 = fx21;
  matlf.fxM22 = fx22;
  matlf.lM23  = 0L;    /* must be 0    */
  matlf.lM31  = l31;
  matlf.lM32  = l32;
  matlf.lM33  = 1L;    /* must be 1    */
  GpiSetDefaultViewMatrix( hpsClient, 9L, &matlf, lType);
}

/* Calculate and set the default viewing transform based on zoom
   and scroll */
VOID CalcTransform( hwnd)

HWND hwnd;
{
  RECTL    rclClient;
  POINTL    ptlCenter, ptlTrans, ptlScale, aptl[4], ptlOrg,
    aptlSides[12];
  PSEGLIST    psl;
  LONG    l;
  MATRIXLF    matlf;

 /* from bounding rect of picture get center of picture */
  ptlCenter.x = (rclBounds.xLeft   + rclBounds.xRight) / 2;
  ptlCenter.y = (rclBounds.yBottom + rclBounds.yTop  ) / 2;
 /* translate center of picture to origin  */
  SetDVTransform( (FIXED)UNITY
        , (FIXED)0
        , (FIXED)0
        , (FIXED)UNITY
        , -ptlCenter.x
        , -ptlCenter.y
        , TRANSFORM_REPLACE);
/* scale down to 1:1 of bitmap in file  */
  ptlScale.x = UNITY * bmpBitmapFile.cx /
  (ptlTopRight.x - ptlBotLeft.x);
  ptlScale.y = UNITY * bmpBitmapFile.cy /
  (ptlTopRight.y - ptlBotLeft.y);

/* add in zoom scale  */
  ptlScale.x += ptlScale.x * lScale / (ZOOM_MAX + 1);
  ptlScale.y += ptlScale.y * lScale / (ZOOM_MAX + 1);

  SetDVTransform( (FIXED)ptlScale.x
        , (FIXED)0
        , (FIXED)0
        , (FIXED)ptlScale.y
        , 0L
        , 0L
        , TRANSFORM_ADD);
 /* translate center of picture to center of client window  */
  WinQueryWindowRect( hwnd, &rclClient);
  ptlTrans.x = (rclClient.xRight - rclClient.xLeft)   / 2;
  ptlTrans.y = (rclClient.yTop    - rclClient.yBottom) / 2;
/* add in horizontal and vertical scrolling factors  */
  ptlTrans.x -= ptsScrollPos.x - ptsHalfScrollMax.x;
  ptlTrans.y += ptsScrollPos.y - ptsHalfScrollMax.y;
  SetDVTransform( (FIXED)UNITY
        , (FIXED)0
        , (FIXED)0
        , (FIXED)UNITY
        , ptlTrans.x
        , ptlTrans.y
        , TRANSFORM_ADD);

/* discard unused precision in default viewing transform  */
GpiQueryDefaultViewMatrix( hpsClient, 9L, &matlf);
  GpiSetDefaultViewMatrix( hpsClient, 9L, &matlf, TRANSFORM_REPLACE);
            ■
            ■
            ■

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

Volume 4 - Number 6

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


Examining NewWave,  Hewlett-Packard's Graphical Object-Oriented Environment

 Alan Cobb and Jonathan Weiner

NewWave by Hewlett-Packard offers a wide range of advanced features for
Microsoft Windows Version 2.11 graphical environment-based applications. It
is in effect an extra layer on top of the MS-DOS operating system, enhancing
and extending the services provided by MS-DOS1 and Windows2. There are five
areas in which NewWave surpasses Windows: control, communication,
integration, abstraction, and ease of use. Because NewWave controls programs
and provides new methods of communication between them, users can integrate
several programs to do one task easily. Furthermore, NewWave enables the
users to deal with the computer at a higher level of abstraction. That is,
users can use simpler techniques to do broader, more complicated
tasks--similar to the way programmers can do much more with one line of code
in a high-level language than with one line of Assembler. Finally, NewWave
is easy to use. Its advanced help and Computer-Based Training (CBT) systems
make it simple for users to learn how to avail themselves of its services.

At the implementation level, NewWave is built upon multiple Windows programs
and dynamic-link libraries (DLLs), as well as its data files. Windows
developers can access these features by making calls to the NewWave
functional interface and conforming to its interapplication communication
protocols.

Using NewWave

Although under the surface NewWave is constructed from normal Windows
programs, the user interacts with the program in quite a different way.
Instead of beginning work in the MS-DOS Executive with its familiar
directory listing, the first thing a NewWave user sees is the NewWave Office
window, shown in Figure 1. Users can also access a conventional text listing
of the icons, illustrated in Figure 2.

The Office can be resized like any other Windows program, but it usually
covers the entire screen. On its surface is a set of icons representing
familiar office tools such as the Printer, the Waste Basket, and the File
Drawer; data objects such as the file folders, simple text documents and
images; and compound documents built from smaller objects of text, graphs,
and images. While the screen will show only one copy of a tool at a time, it
can show multiple instances of data objects.

To work with a particular data object, just double click on its icon.
NewWave has a record of which tool (EXE file) was used to create that
object's data file. The appropriate program is automatically started and the
data is read into it. Thus, the user is raised above the details of the file
system. NewWave also has a one-step drag-and-drop technique used for
operations like copying, deleting, and printing objects. For example, to
delete a data object or folder of objects, the user just clicks on the
object, drags the mouse to the Waste Basket icon, and drops it in.

Features

One of the principal benefits of NewWave is its superior ability to deal
with compound documents. To get a better feel for NewWave's features, we
will look at a simple demonstration system, consisting of a compound monthly
report document that contains a nested bitmapped image and a nested
spreadsheet. Figure 3 shows the structure of this document.

Compound documents are data objects built from a tree of other nested data
objects. The component objects can be pieces of text, graphs, spreadsheets,
Tagged Image File Format (TIFF) images, or even captured voice recordings or
animation sequences. In the example, the pieces are incorporated into a
compound object and manipulated by a prototype NewWave-capable word
processor called NewWave Write.

There are several methods that can be used to combine the nested objects
into a larger compound document. The simplest is to drag the icon of the
nested object (the source object) and drop it into the larger destination
document at the point where it is to appear. This method moves the document
completely inside the destination, so the separate source icon will no
longer be shown in the Office window. Figure 4 shows the sample object on
the screen after it has been constructed.

But what if you also want to show the source object, say the spreadsheet, in
another compound document at the same time? In that case, you can
simultaneously share the single source with multiple destination objects. To
do this, highlight the source icon with a mouse click, then select Share
from the Edit menu. A reference to the source object is now on the
clipboard. Now you can go into one or more destination documents and use the
clipboard's familiar Paste command to insert a nested view of the source
document. After the pasting, the separate icon for the source will still
appear in the NewWave Office window.

The full power of NewWave is demonstrated when you need to modify the
compound document or one of its components. For example, suppose you decide
to add another column to the nested spreadsheet. To access the spreadsheet,
simply double click on the area where it is nested. Since the NewWave
database of links or views knows which tool and data files were used to
create the nested spreadsheet, it can automatically start the tool
application and load the spreadsheet for modification. So you can see what
is happening, NewWave explodes the nested window and grays the area where it
is normally nested. The exploded window will be the main window of the
application that was used to create the source object. In our spreadsheet
example, the application might be Microsoft Excel. Figure 5 shows the nested
TIFF picture being modified.

When a conventional program needs to support a new data type, new code to
handle that type must be added to the application. Another feature of
NewWave is that it eliminates this duplication of code by logically joining
all application data files to the programs that created and edited them.
When it is time to view or manipulate the source data, the destination
object can simply ask the source object to do it. This is the principal
sense in which NewWave is object-oriented. In fact, a NewWave object is
defined as this paired combination of data and the application required to
manipulate it. NewWave is also object-oriented in that objects pass command
messages back and forth to each other. The standard term "method" is used to
describe the code used by an object to process one of its messages.

What if you start modifying the visually linked image separately while the
larger compound document is closed? NewWave's database of interobject
links--part of the Object Management Facility (OMF) discussed below--takes
care of this also. When the larger report is later opened or printed, it
will see a flag in the database telling it that the graph has changed. The
report object can then ask the graph to rerender the projected view of
itself.

NewWave allows you to paste the report into an even larger compound
document. Nested groups of compound objects can be moved, copied, printed,
erased, or mailed all at once, simply by selecting the overall object with
the mouse and dragging it to the destination or by copying and pasting it
through the clipboard.

Advanced Features

Now we will extend the sample program to include advanced NewWave features.
In its present form the sample monthly report only uses NewWave's visual
links (also called visual views). That is, the source objects are only
connected to the larger destination visually; there is no actual passing of
data between the applications. To pass binary data, such as an array of
integers from a spreadsheet, NewWave uses its second main type of link, the
data passing link.

Figure 6 shows how data passing links can be added to the sample program. We
added two new hypothetical applications, a NewWave-capable terminal program
and a NewWave-capable graphing program. A NewWave Agent task script, which
is like an advanced batch file or macro for Windows programs, has also been
added. The script is used to make all the applications work as a team to
produce the monthly report.

To produce the report, the controlling Agent script first sends the
appropriate commands to the NewWave-capable terminal, causing it to dial all
of the company's regional offices. The Agent script automatically collects
the monthly sales data from each office. Details of the quantities of
products sold are passed via a NewWave data passing link to the spreadsheet.
As in the first part of the example, the spreadsheet uses a visual link to
project the spreadsheet grid into the report, but now it also uses a data
passing link to pass the summarized spreadsheet data to a NewWave-capable
graphing program. The graphing program then uses a visual link to display
itself inside the report.

The abstraction of file folder icons to represent directories and the files
they contain is another advanced feature of NewWave. To illustrate this
feature, the sample also includes the monthly report in a folder of all
monthly reports for the year. The folders can be nested inside each other as
well as inside the File Drawer icon. Folders are opened with a simple double
click. Figure 7 shows the open File Drawer; the Budgets folder within has
also been opened.

The connection between filed objects is accomplished with NewWave's third
type of link, the simple link. Instead of passing views or data these links
simply enclose one object inside a parent container object. Other objects
can also be inserted into or removed from a container object, such as the
Waste Basket, with the drag-and-drop method.

OMF and API Systems

Now that you are familiar with the features of NewWave, we will describe its
major components (some of which were referred to earlier). NewWave consists
of two main systems, the OMF and the API (see Figure 8). The API  in turn is
composed of three smaller systems: the Agent task script system, the Help
system, and the CBT. The OMF is used to record and supervise the visual and
data links between objects. The API Agent script system handles the
recording, playback, and editing of the task scripts used to control NewWave
applications. The API, Help, and CBT systems provide a high-quality,
prewritten, standard foundation for adding help, demos, and training to
NewWave applications.

All the objects in a NewWave system are connected in a tree. Near the top of
the tree is the NewWave Office. It is a parent to every object that appears
in the Office window. For example, the File Drawer is a child of the NewWave
Office, and it in turn has its own children in the form of folders. Figure 9
shows one of the diagnostic utilities that comes with the system; this
utility allows you to traverse the object tree. A child object is enclosed
in a container or acts as the source in a visual or data link. One child can
have multiple parents (a graph object, for example, can have visual views
projected into three parent reports at the same time), which makes the
structure more than a simple tree.

Processors

Every NewWave application contains two systems, the Action processor and the
Command processor, as shown in Figure 10. The Action processor translates
user actions, such as mouse clicks and menu selections, into one of the
commands that the NewWave application can perform. Often, several different
actions will be translated into the same command. For example, using an
accelerator key sequence (such as Alt-F-x) can execute the same command as
selecting a menu item with the mouse (such as clicking on File, then Exit).

The Command processor provides all the actual functionality of the
application. It executes commands that are passed to it from several
possible sources. For example, commands can come directly from the Action
processor as a result of current user keystrokes and mouse movements or be
played back from an Agent script. The Command processor must be able to
handle the full range of verbs in the application's command language. Agent
scripts are built from this set of commands. This is discussed in detail
below.

 Splitting the application's control system into Action and Command
processors makes it much easier for NewWave to implement the Agent macro
facility. That is, when NewWave records an Agent script, it need not concern
itself with the details of the user's actions. It only has to store the
series of commands that result from the Action processor's translation.

At a lower level, the Action and Command processors are supported by several
other processors and components. Figure 11 shows the system in detail. This
design enables the application to run in one of five modes: Playback,
Record, Intercept (for Help), Monitor (for CBT), or Error mode. In Playback
mode, an Agent task script sends commands to the application through the
API. In Record mode, the user's actions are translated into commands and
stored in an Agent task script. Intercept mode is entered when the user asks
for context-sensitive help. The cursor changes to a question mark, and the
user clicks on the item of interest. In Monitor mode, all commands are
passed to the CBT system before they are executed. This allows the CBT
system to discard inappropriate commands during a lesson and guide the user
in the right direction. During Error mode, any error notifications are
rerouted to the controlling Agent instead of to the normal destination,
which is an error message box for the user.

Although the system is complex, the hard part of initial design and
debugging has been done for you. The NewWave Software Development Kit (SDK)
gives detailed examples of how to build the pieces. The components (as
opposed to processors) are supplied by Hewlett-Packard. The user need only
change the variable names in these components to match his or her system. As
shown in the figure, any modeless dialog boxes need to be supported with
their own Action processor.

The Translate to Internal and Translate to External processors convert
commands between Internal and External format. Commands are stored in
External binary form by Agent tasks. This is the form passed to the
application for execution. The application translates this to its own
private Internal format, which can take whatever form is necessary.

Compared to Windows

NewWave has several advantages over Windows. One is the superiority of the
forms of links NewWave has to the methods of interprocess communication in
Windows. In Windows there are no standard equivalents of the visual views
and simple container links found in NewWave. Nor are there Windows
equivalents to the share capability or ability to move, print, copy, and
paste compound documents NewWave possesses. These features make it
significantly easier to organize and maintain complex objects in NewWave
than in Windows. The Windows Dynamic Data Exchange (DDE) protocol does,
however, provide some of the same features as the NewWave data passing
links. DDE moves data between two applications via messages and shared
memory blocks. Both DDE and NewWave data passing links allow one application
to update a linked program automatically without requiring the user to take
any action. For example, one conventional Windows program can read stock
price data from Dow Jones and pass the latest prices via DDE to a graphing
program for real-time display.

The primary advantage that NewWave data passing links have over DDE is that
they provide several fairly complex services that DDE users would have to
rewrite and debug from scratch. Not only would a DDE application need
considerable added code, that code would have to be duplicated the same way
in all the other programs with which the DDE application was going to
communicate. NewWave moves much of that common code out of individual
applications and into one centralized, standardized, operating-system-style
service.

Another way in which the NewWave link services go beyond DDE is that they
are persistent. When you shut down two programs using DDE, the link will be
broken. Under NewWave, a systemwide database in the OMF records a list of
the links among all applications. As soon as a link is created, it is
recorded in the database. When the NewWave programs are closed, the link
persists and will resume operation when the programs are restarted. (DDE
applications, on the other hand, would have to reestablish the link each
time.)

For example, suppose you have linked some numbers from a spreadsheet to a
report you are producing with an editor. The editor doesn't have to be on
constantly, waiting to get possible changes from the spreadsheet. If the
spreadsheet does change, NewWave will automatically set a flag in its link
database, indicating that the report needs to be updated when it is
reopened. When the editor starts again, NewWave can automatically restart
the spreadsheet in the background to rerender the linked data.

If the editor were using conventional DDE, it would have to implement its
own list of the server applications to which it was linked as well as the
type of data passed over each of the links. NewWave removes this burden from
individual applications by providing it as an environmental service.

Another interprocess communication service unique to NewWave is the
snapshot. A snapshot is a special type of object used to reduce the time and
memory overhead required for communication views between two objects.
Usually when a destination object requests the rendering of a fresh view or
more data from a source object, the entire source object must be loaded and
run. This could mean loading an entire spreadsheet application and a large
worksheet just to access three numbers somewhere in the worksheet. NewWave
enables you to provide a small snapshot object that can render only a
particular linked view. If a snapshot is present when a destination object
requests the rerendering of a view, NewWave will reroute the request,
instead of the full application, to the destination object.

A snapshot is implemented as a small DLL with an associated data file. It
loads faster and requires less memory because it is a DLL and not a full
process. A snapshot doesn't have to contain the user interface or any other
code beyond what is required to render that one view. Conventional DDE, on
the other hand, would require both complete applications to be present in
memory in order to pass the new data.

Another way in which NewWave extends Windows is in its task script language,
called Agents. Agents are comparable to extended BAT files that control
Windows programs. Windows Version 2.1 currently has no built-in control
script facility of this kind. Although under Windows it is possible to write
journaling programs that record and play back series of keystrokes and mouse
movements, the NewWave Agents function at a cleaner and more fundamental
level. Unlike journaled keystrokes and mouse movements (which are just
recorded user actions rather than commands), Agents will work regardless of
how many applications are present in the Office and where they are
positioned.

The task language has many standard statements, including opening, closing,
minimizing, and maximizing applications, that correspond to commands shared
by most NewWave programs. The most interesting feature of the Agent
language, however, is its extensibility to support individual applications.
Each NewWave program defines its own command language and implements a
parser to translate it. A full NewWave application must define statements in
its command language to support all its menu items and their parameters. The
goal is for the Agent to be able to do anything the user can do directly.
See the sidebar "NewWave Command and Function Summary," which has some
commands from the Agent task language.

An Agent can control individual applications by communicating with them
directly in their own language of commands. As a result, NewWave
applications no longer need the extra code necessary to support their own
nonstandard internal macro languages. Users can control all their NewWave
applications with a single task language.

Agent scripts can be generated by capturing a series of user commands (see
Figures 12 and 13) or by creating them directly with an ordinary text
editor. The language supports control structures such as loops, branching,
and procedures; it also supports integer, float, and string variables. For
performance, the scripts are compiled into a binary form before being run.

In the future, a built-in scheduling system will allow Agents to perform
tasks at specified times, such as a single time or regularly every day or
week (see Figure 14). An Agent could be told to wait for a trigger event
before it starts running. For example, the receipt of a piece of E-mail
could trigger the data being placed into a report.

Figure 15 shows a simple Agent task script. Note that because this article
was written before the release version of NewWave was ready, the PASTE and
CLOSE commands had to be used in intermediate pcode form.

NewWave also adds to Windows with its user support. NewWave includes
prewritten CBT, Help, and Native Language Support (NLS) systems. NewWave is
in a unique position to offer first-rate Help and CBT services because of
the similarity of its Help, CBT, and Agent macro facilities. All three
systems are concerned with monitoring and controlling the execution of
individual applications. In fact, the CBT lessons are written in an extended
form of the Agent macro language. As you have seen, all user actions must
pass through a NewWave application's Action Processor before being
translated into commands that are executed by its Command Processor. The CBT
system can watch and modify this traffic of commands to control the user's
interaction with the system. The CBT can, for example, intercept, recognize,
and point out correct and incorrect user responses during a lesson. NewWave
provides tools for creating CBT and Help documents. Its built-in CBT
animation development system, shown in Figure 16, helps developers design
documentation and Help files at the same time they are writing the program.

Conventional CBT systems often attempt to simulate the behavior of specific
parts of the application. Rather than adding new simulation code, NewWave
CBT simply uses the application itself by sending its commands directly to
the Command Processor.

Converting Applications

NewWave has built-in support to help convert an application to other
languages and customs. First, a single generic version of the program is
written. Later, nontechnical translation workers can use the tools NewWave
provides to adapt the local character handling (see Figure 17) and customs
support without changing any code.

In order to see how NewWave capabilities can be added to an existing Windows
application, we will look at portions of the code for a Shapes program that
comes with the Windows SDK. The NewWave version is called HPShape. Shapes
does only one thing: it draws one of four geometric figures selected from
its menu--a triangle, an ellipse, a rectangle, or a star. HPShape has been
made into a typical source object that projects its visual view into a
destination object.

The NewWave SDK includes a second program called HPLayout as a sample
destination object. To nest a visual view of HPShape in HPLayout, mark off
an area inside the HPLayout window with the mouse. The nested area is shown
in reverse (white foreground on a black background). To project the HPShape
view into this small area, drag HPShape's icon over it and drop it in. If
you want to manipulate the nested HPShape object, double click on the nested
area to bring up HPShape's main window. Figure 18 shows the compound
document, SDK NW Layout Demo, with its two nested children open. The areas
where they normally appear in the document are gray. The object called
Nestable HP Text is another simple source object that comes with the NewWave
SDK. It projects a visual view of text.

Getting NewWave's additional functionality comes at the price of a fair
amount of extra code. Whereas the simple Shapes WinApp weighs in with only
7Kb of C source code, the HPShape and HPLayout applications require 55Kb and
157Kb of C source code, respectively. Of course, that added code gives you a
help system, a programming language, and data and visual linking capability.
The amount of added NewWave code is also relatively fixed in size. For a
normal sized program it will be a smaller percentage of the total code.

Figures 19 and 20 are calltrees for the programs produced with  Microsoft
CALLTREE.EXE utility. Calltree listings show the hierarchy of function calls
for a C program. The calls made from a function are indented beneath it.
Figure 19 shows the structure of the pre-NewWave Shapes; Figure 20 shows the
HPShape NewWave program. The full source code for three HPShape functions
(ShapeWndProc, ActionProcessor, and CommandProcessor) is shown in Figures
21, 22, and 23.

The data file referred to in the figures is used to record the specific
shape that was being displayed in the HPShape window. This means that even
after the NewWave application is closed, its state persists and when it is
reopened this state is restored.

As with all Windows and Presentation Manager programs, a NewWave program is
essentially a large message processor. The program spends its life
responding to the spectrum of messages that enter its main window procedure.
It separates the messages into general categories and passes them to more
specialized handlers. Figure 24 contains the messages recognized by the
pre-NewWave Shapes; Figure 25 contains the considerably broader list to
which HPShape must respond.

Conventional Applications

In order for applications to exploit NewWave fully, they must be
specifically written to interact with its new interfaces. But while these
fully NewWave-capable applications are being written it is important for
NewWave to be able to interact with any conventional MS-DOS and Windows
programs such as Lotus 1-2-3. NewWave provides several methods for
encapsulating these existing applications to make them more functional in
NewWave.

At its lowest level, NewWave allows existing programs to be run from a menu.
This requires no encapsulation at all. Windows applications as well as
character mode or graphic MS-DOS programs that take over the whole screen
can be run this way. The user can context switch between the program and the
rest of NewWave and use the clipboard to cut and paste between it and other
applications. Integrating a new program to operate at this level  takes only
a few minutes.

Moving up the integration scale requires the encapsulation of the existing
program into a shell created by an interactive installation tool provided by
NewWave. The lowest level of encapsulation takes about an hour. It allows
data files created by the program to be represented as an icon in the
NewWave Office. These objects can be manipulated in most of the same ways
that a full NewWave application can. They can be opened with a double click
or moved, copied, filed, mailed, or discarded with the drag-and-drop
technique. The disadvantages are that they allow neither data nor visual
links to other objects nor can they use the full Agent task language, help,
or training systems.

The highest level of encapsulation requires considerable programming, but
doing that programming is significantly easier than completely rewriting the
application as a native NewWave application. The new code primarily adds
support for data and visual links. It consists of a browser program that
understands the format of the application's data and can talk to the NewWave
OMF. For example, when a destination object sends a message asking an
encapsulated source object to display a view of itself in a given rectangle,
the browser reads the object's data file and displays the data to the
screen. If it is necessary to edit the data, the browser can invoke the
necessary application to do so.

A full NewWave application goes further by providing complete support for
the Agent task language, context-sensitive help, NewWave user interface, and
Computer-Based Training.

Machine Requirements

The minimum hardware that HP recommends for NewWave users is a 286 PC/AT or
100 percent compatible, 3Mb of LIM 4.0 EMS memory, a 20Mb hard disk, and an
EGA display (a VGA is significantly better). Developers should increase this
to 4Mb of EMS memory. A 40Mb or larger hard disk is recommended. Developers
will also need Microsoft Windows/286 Version 2.11, and Microsoft C 5.1 or
higher. As with most Windows or Presentation Manager work, a fast 386
machine significantly increases productivity.

NewWave enhancements to Windows programs come at the cost of increased code,
although that cost can be lessened by choosing the level of encapsulation
your program requires. Windows programmers who are interested in extending
and enhancing their Windows programs must give serious consideration to
programming for the NewWave environment.

A NewWave Glossary:

NewWave has its own definitions for a number of terms. Most of them are
illustrated in Figures 3, 4 and 5.

 NewWave Terms:

Objects All the icons in the NewWave Office window represent either tools or
data objects.

 Office Tools or System Objects Tool objects are tools such as the Waste
Basket, File Drawer, and Printer. They are associated with some type of
fixed system service. They cannot be copied or deleted by the user.

 User Objects User objects are objects that can be freely copied, deleted,
cut, pasted, or shared by the user.

 Data Objects Data objects are the combination of an application that
creates and manipulates data and a specific data file that was created with
it. For example, a word processor and a memo created with it would form one
object. Under NewWave the user doesn't work with applications but rather
with these bundled pairs of application and data.

 Compound Objects Compound objects are built from a combination of smaller
objects. For example, a spreadsheet object could have several small text
note objects attached to it to explain some of the calculations. The
aggregate group can be copied, printed, and moved as a single entity via the
clipboard.

 Container Objects Containers are objects like the File Drawer, File
Folders, and Waste Basket, that are used to hold objects in a group. They
are represented by a single icon in the Office window. Containers can be
opened to show the objects they hold.

 Views or Links Objects can be connected to one another in several different
ways called links or views.

 Visual Views or Links In a visual link, one object projects a view of
itself into another. The projected object is shown nested inside the
destination object. For example, a graph object could be connected with a
visual view to a text report in which it appears. The nested object does all
the drawing of its own view.

 Data Passing Links Data links or views actually pass pieces of binary data
between objects. For example, a communications program could pass stock
prices to a spreadsheet object for analysis.

 Simple Links Container objects are connected by simple links to the objects
they enclose. There is no passing of data or visual views between them.

 Source and Destination Objects The source object is the one either sending
binary data via a data link or else projecting a view of itself via a visual
view into a destination object.

 Child and Parent Objects A child object is either a source object or an
object held within a container. A parent object is either the destination of
a visual or data link or else a container holding other objects. One object
can be simultaneously both a parent to objects below it and a child to
objects above it.

 Other Terms:

 API Although API (Application Program Interface) is normally a generic term
that refers to any functional interface to a subsystem, NewWave uses it to
refer specifically to the interface to its Agent, CBT, and Help systems.

 Methods Objects communicate among themselves and with NewWave by sending
messages to each other. For example, the message DISPLAY VIEW is sent from a
parent object to a child during the setup of a visual link. The code in the
child that processes a particular message is referred to as a method.

 Agent An Agent is like a batch or macro file for controlling data objects,
tools and other programs in the NewWave environment. The Agent's commands
are written in an Agent task script language. Individual scripts are shown
in the NewWave Office as icons.

 Share Although one child object can be moved or pasted completely into a
single parent object, it can also be shared into the same object.A child can
be shared into multiple objects at once. For example, one chart could appear
in several reports at the same time. When the child chart is updated, it
would be shown in its updated form in all the parent report objects.

NewWave Command and Function Summary

The Class Independent Commands are executed by the Agent itself at run time,
independent of any application object that may be open. Most of them either
manipulate task conversational windows or handle flow control of the Agent
task.

Command Name    Description

CLEARWINDOW    Clear a user conversational window

CLOSEWINDOW    Close a conversational window

DEFINEWINDOW    Define a user conversational window

DO    Execute a procedure

EDITBOX    Create an edit box in a conversational

    window

END    Terminate execution of a task

FOCUS    Change focus to a specified object

GOTO    Transfer control to a labeled statement

IF ELSE ENDIF    Conditional execution

INPUT    Create a window to prompt the user for input

JUSTIFY    Justify text in a conversational window

LABEL    Define a label

LOCATE    Position the cursor in a conversational

    window

MESSAGE    Create a message window (OK, RETRY, and

    so on)

ON ERROR DO    Trap on any error condition

ON ESCAPE DO    Trap on the escape key

ON TIMEOUT DO    Trap on a timeout

OPENWINDOW    Open a conversational window

OUTPUT    Output text to a conversational window

PAUSE    Halts execution temporarily

PROCEDURE ENDPROC    Define a procedure

PUSHBUTTON    Draw a pushbutton in a conversational

    window

RETURN    Exit a procedure

SCREEN    Map logical screen and window coordinates

SET ERROR    Set error trapping on or off

SET ESCAPE    Set escape trapping

SET RATE    Sets rate at which Agent executes commands

SET TIMEOUT    Set timeout trapping

TASK ENDTASK    Defines the main body of a task script

TITLEWINDOW    Set caption bar text of conversational

    window

WAIT    Suspend execution until an event is trapped

WHILE ENDWHILE    Looping of control

Class Independent Functions for Data Manipulation

Function Name    Value Returned

EDITBOX    String of text in an edit box

TESTBUTTON    Tells if a given button was pushed

ABS    Absolute value of a numeric argument

ASC    Integer for a given string

CHR    String for a given integer

FIND    Location of substring in a string

INT    A long when passed a real

LEFT    String of leftmost characters

LEN    Number of characters in a string

MID    Extracted substring

MOD    Remainder for two integers

RIGHT    String of rightmost characters

STR    ASCII string given a numeric argument

SYS_ERROR    Error number of last run-time error

NUM    Tells if parameter is numeric

VAL    Numeric value of input string

Class Dependent Commands for the NewWave Office Window

Individual applications have their own unique set of commands that they will
accept. These are called Class Dependent Commands. Below is a subset of the
Class Dependent Commands for the NewWave Office.

Command Name    Description

ABOUT?    Display "About ..." dialog box

ACTIVATE    Change the currently active window

ADD_SELECTION    Select the specified object

ADJUST_WINDOW    Move or size the current window

ALIGN_BY_ROWS    Align the icons in the Office window

AUTO_ALIGNMENT    Set auto_alignment (snap to grid) mode

CHANGE_ATTRIBUTES    Change attributes of a selected object

CHANGE_TITLE    Change an object's title

CLOSE    Close the currently active window

CONTROL_PANEL    Execute the Windows Control EXE

    program

COPY    Copy an object to the clipboard

COPY_TO    Copy an object to a closed container

CREATE_A_NEW    Create a new object

CUT    Delete selected objects to the clipboard

EXPORT_TO_DISK_FILE    Serialize an object's data to a disk file

ICONIC_VIEW    Display objects in HP Office as icons

IMPORT_FROM_DISK_FILE    Deserialize an object from a disk file

LIST_VIEW    Display a container's objects as a list

LOCK_DISPLAY    Display dialog box to record password

MAKE_COPY    Copy all selected objects in the window

MANAGE_MASTERS    Remove objects from the workspace

MANAGE_TOOLS    Change the selection of tools in the Office

MAXIMIZE    Increase the current window's size

MOVE_TO    Move selected objects to a container

OPEN    Open all selected objects

OPEN_SELECTED_OBJECT    Open an object

PASTE    Paste objects from the clipboard

PERFORM    Perform an Agent task

PRINT    Drop selected object on printer icon

PRINT_LIST_OF_OBJECTS    Make a hard copy of container contents

RESTORE    Return a window to its default size

SAVE_AS_MASTER    Save a copy of the object as a template

SELECT    Select one object by class and title

SELECT_ALL_OBJECTS    Select all objects in the current window

DESELECT_ALL    Deselect all the objects in the window

DESELECT    Deselect one object by class and title

SELECT_OPENED    Select an open object by class and title

SEND_TO_MAILROOM    Insert selected objects into the mail room

SET_PASSWORD?    Display the "Password" dialog box

SET_USER_TIME_ZONE    Modify user name and time zone data

SHARE    Share selected objects to the clipboard

SHOW_DOS_PATH    Display path of an MS-DOS application

SHOW_LINKS    Show the links to an object

SHOW_OWN_LINKS    Show the links to an object's parents

OPEN_PARENT    Open a linked parent

STRAIGHTEN_UP    Snap objects to the nearest grid point

THROW_AWAY    Move selected objects to the Waste Basket

TRANSFER_MAIL    Initiate a send or receive mail transfer

Class Dependent Commands for the HPSHAPE Sample Application

This is the unique set of commands for the HPSHAPE sample program discussed
in the text. The commands correspond to the items on HPSHAPE's menus. Notice
that there is some overlap with the Office window's command set.

Command Name    Description

CLEAR    Clear the object's window

CLOSE    Close the currently active window

MAXIMIZE    Increase the current window's size

RESTORE    Return a window to its default size

ELLIPSE    Display an ellipse

RECTANGLE    Display a rectangle

STAR    Display a star

TRIANGLE    Display a triangle

Figure 16

'******************************************************************

'* Simple NewWave task script example. Copies a bitmapped image   *
'* object called "Image: Our New CFO" to the clipboard, then      *
'* pastes this into a compound document called "Demo Report."     *
'* It repeats this append operation in a loop until the user      *
'* decides to exit. Author:  Alan Cobb              *

'******************************************************************

TASK
bContinue# = 1
WHILE bContinue# = 1
  FOCUS OFFICE "NewWave Office"              ' Set keyboard focus.
  SELECT NEWWAVE_WRITE "Demo Report"
  OPEN                                       ' Open report.
  SELECT NWImage "Image: Our New CFO"
  COPY                                       ' Copy to clipboard.
  FOCUS NEWWAVE_WRITE "Demo Report"
  DO PASTE_INTO_REPORT                       ' Paste image.
  DO CLOSE_REPORT                            ' Save result.
  MESSAGE bContinue# "Repeat process?" yesno ' Loop again?
ENDWHILE
MESSAGE bTemp# "Exiting." ok                 ' Pause before exit.
END
ENDTASK

'******************************************************************

'*  Equivalent of PASTE command in pcode form.

'******************************************************************

PROCEDURE PASTE_INTO_REPORT
  PCOMMAND 4
  PCODE "0400CD00"
  RETURN
ENDPROC

'******************************************************************

'*  Equivalent of CLOSE command in pcode form.

'******************************************************************

PROCEDURE CLOSE_REPORT
  PCOMMAND 8
  PCODE "0800020001000100"
  RETURN
ENDPROC



Figure 19:

WinMain                       < Initialization and main event loop.
|   ShapesInit                < Initialize main window.

ShapesWndProc                 < Handle all messages sent to Shapes.
    ShapesCommand             < Process menu selections.
    ShapesPaint               < Paint the requested shape.
        ShapesSetupDC
        DrawRect
        DrawEllipse
        DrawTriangle
        DrawStar



Figure 20:

WinMain    < Initialization and main event loop.
|   ShapeInit    < Initializes HPSHAPE'S window
|   ShapeCreateWindow    < Creates a NewWave main window.

ShapeWndProc    < Handle all messages sent to HPSHAPE.
   TranslateToInternalProcessor
        < Take the external command from the buffer.
         <  Change external command into the internal
        <  command format.
   TranslateToExternalProcessor
          < Convert the internal command format
        <  into the external command format.

    ActionProcessor    < Handle all of the user actions. Prepare
        |    <  them for Command Processor
        InterrogateFromAPI
    < Handle context-sensitive help requests.
        MessageFromOMF    < Process messages sent from the OMF.
            ReadDataFile    < Get the data file for HPSHAPE.
            HasMethod    < Reply to the HAS_METHOD message to
    <  indicate whether a method is supported by
            |    <  HPSHAPE.
            InitView    < Establish a view definition for
                <  the data to be displayed in the
                <  requesting object.
            GetSize    < State the size HPSHAPE needs to be in
            |    <  order to display itself as a view.
            DisplayView    < Paint the view in the DC specified
            |    <  by the destination object.
            CopySelf    < Allow the object to be copied when
                 |    <  contained in another object.
                SaveDataFile
                    < Save the data file that is the last
                    <  shape selected.
           SaveDataFile...
           SaveWindowPosition
    < Saves the last window coordinates in a
        |   |    <  property.
        ShapePaint    < Paint the requested shape.

    CommandProcessor    < Handle any commands passed from the
        |    <  ActionProcessor or API (for example, Agent).
       SaveWindowPosition...



Figure 21:

/************************************************************/
                  ShapeWndProc
  Main procedure to handle all messages sent to HPSHAPE
       COPYRIGHT HEWLETT-PACKARD COMPANY 1987, 1988
/************************************************************/

long FAR PASCAL ShapeWndProc( hWnd, message, wParam, lParam )
    HWND            hWnd;
    unsigned        message;
    WORD            wParam;
    LONG            lParam;
{
    APIRTNTYPE      applRtn;  /* Used as a return value */

APICMDSTRUCT    extCmd;   /* The external command structure for API */
    INTCMDSTRUCT    intCmd;   /* The internal command structure for API */

    /* Function called for windows that are created with            */
    /* NW_CreateWindow to specially handle some messages            */

    if( NW_MessageFilter(hWnd, message, wParam, lParam,
                         (LONG FAR *) &applRtn))
        {
        return (applRtn);
        }

    /* Is API to intercept messages or has an API menu item been    */
    /* selected such as the help or the task?                      */

    if( APIInterceptOn(gAPIModeFlags) || APIHaveMenu (message, wParam))
        {
        /* Is this a message for the API - may set certain flags?   */
        APIUserActionInterface(ghAPI, hWnd, (LPAPIUNSIGNED)&message,
                                 wParam, lParam, API_NO_MODE);
        }

    applRtn = (APIERRTYPE)0L;
    /* Still a command to be handled? Or did APIUserAct take it?  */
    if( APIHaveMessage (message) )
        {
        intCmd wCmd = API_NO_CMD;

        /* Are you currently playing back a message or recorded task? */
        if (APIPlaybackMsg(message))
            /* Translate to the internal format of the application   */
            TranslateToInternalProcessor(message, wParam, lParam,
                                          &intCmd);
        else
            /* You are in record mode or a user interactive command  */
            ActionProcessor(hWnd, message, wParam, lParam, &intCmd,
                            &applRtn);

        /* Is there a command created in the action processor
           that needs to be executed? */
        if( APIHaveCommand(intCmd.wCmd) )
            {
            gapplErr = API_NO_ERR;

            /* Are you in  a CBT? */
            if( APIMonitorOn(gAPIModeFlags) )
               {
                /* Set to external language and pass the external
                   form of the command to the Agent                   */

                TranslateToExternalProcessor(&intCmd,&extCmd);
                APICommandInterface(ghAPI, (LPAPICMDSTRUCT)&extCmd,
                                    API_NO_MODE);
                /* The internal command must be canceled if the    */
                   APICommandInterface or APIClgCommandInterface
                   has nullified the command   */
                if (extCmd.wCmd == API_NO_CMD)
                   intCmd.wCmd = API_NO_CMD;
               }

            /* Has the command been formed and executed? */
            if( APIHaveCommand(intCmd.wCmd) )
                /* Perform the command   */
                CommandProcessor(hWnd, message, wParam, lParam, &intCmd,
                                 &applRtn);

            /* Is a command being played back or being recorded?    */
            if( APIPlaybackOn(gAPIModeFlags) ||
                              APIRecordOn (gAPIModeFlags))
                {
                if( APIRecordOn(gAPIModeFlags) )
                    {
                    /* Translate to ext  lang  and tell Agent that
                      command is complete and ready for next
                      command.                                      */
                    TranslateToExternalProcessor(&intCmd,&extCmd);
                    APIRecordInterface(ghAPI,(LPAPICMDSTRUCT)&extCmd,
                                       API_NO_MODE);
                    }
                /* Return control to the user   */
                APIReturnInterface( ghAPI, gapplErr, API_NO_MODE );
                }

            } /* EndIf of APIHaveCommand   */

        }  /* EndIf of APIHaveMessage   */

    return(applRtn);

} /* End of ShapeWndProc   */



Figure 22:

/********************************************************************/

ActionProcessor
   Handle all of the user actions - set up intCmd for Cmd Processor
         COPYRIGHT HEWLETT-PACKARD COMPANY 1987, 1988

/********************************************************************/

void PASCAL  ActionProcessor (hWnd, message, wParam, lParam, intCmd,
                              pRtn)
    HWND          hWnd;
    unsigned      message;
    WORD          wParam;
    LONG          lParam;
    PINTCMDSTRUCT intCmd;
    LONG          *pRtn;    /* The return value of the procedure */
{
    PAINTSTRUCT   ps ;         /* The paint structure */

    /* This routine actually builds a command */
    switch( message )
        {
        /* Was a paint message sent to update the screen? */
        case WM_PAINT:
            BeginPaint       (hWnd, (LPPAINTSTRUCT)&ps);
            ShapePaint    (hWnd, (LPPAINTSTRUCT)&ps);
            EndPaint    (hWnd, (LPPAINTSTRUCT)&ps);
            break;

        /* Was there a menu selection? */
        case WM_COMMAND:
            /* Was the close option chosen? */
            if (wParam == IDDCLOSE)
                intCmd->wCmd = API_CLOSE_WINDOW_CDCMD;
            else {
                 /* Something was selected from the shape menu */
                 intCmd->wCmd = NEW_SHAPE;
                 intCmd->internal.ICmd = wParam;
                 }
            break;

        /* The window being closed */
        case WM_CLOSE:
            intCmd->wCmd = API_CLOSE_WINDOW_CDCMD;
            break;

        /* Selection from system menu or minimize/maximize */
        case WM_SYSCOMMAND:
            switch (wParam)
                {
                case SC_MINIMIZE:
                    intCmd->wCmd = API_MINIMIZE_WINDOW_CDCMD;
                    break;

                case SC_MAXIMIZE:
                    intCmd->wCmd = API_MAXIMIZE_WINDOW_CDCMD;
                    break;

                /* Request for saving previous coordinates */
                case SC_RESTORE:
                    intCmd->wCmd = API_RESTORE_WINDOW_CDCMD;
                    break;

                default:
                    /* Let windows handle it */
                    *pRtn = DefWindowProc(hWnd, message, wParam,
                                                            lParam);
                    break;

                }  /* End of case WM_SYSCOMMAND */
             break;

        case API_INTERROGATE_MSG:

            /* The message has come via the API */
            *pRtn = InterrogateFromAPI(wParam, lParam);
            break;

        case  API_SET_MODE_FLAGS_MSG:
             if (wParam == API_SET_MODE_ON_FLAG)
                 gAPIModeFlags = gAPIModeFlags | lParam;
             else
                 gAPIModeFlags = gAPIModeFlags & lParam;
             break;

        case WM_OMF:

            /* The message has come via the OMF */
            *pRtn = MessageFromOMF(hWnd, wParam, lParam);
            break;

        default:
            *pRtn = DefWindowProc(hWnd, message, wParam, lParam);
            break;
        }
}  /* End of ActionProcessor */



Figure 23:

/*********************************************************************/

CommandProcessor
  Handle any of the commands passed from the ActionProcessor or API
         COPYRIGHT HEWLETT-PACKARD COMPANY 1987, 1988

/*********************************************************************/

void PASCAL   CommandProcessor (hWnd, message, wParam, lParam, intCmd,
                                pRtn)
    HWND          hWnd;
    unsigned      message;
    WORD          wParam;
    LONG          lParam;
    PINTCMDSTRUCT intCmd;
    LONG          *pRtn;          /* Application return error code */
{
    HMENU         hMenu;
    RECT          rcRect;

    switch( intCmd->wCmd )
        {
        case API_MINIMIZE_WINDOW_CDCMD:
            if (IsIconic(hWnd))
                NW_Restore(hWnd);
            else
                {
                if (!IsZoomed(hWnd))
                     GetWindowRect(hWnd, (LPRECT)&gWinPosn.rcRect);
                NW_Minimize(hWnd);
                }
            break;

        case API_MAXIMIZE_WINDOW_CDCMD:
            if (IsZoomed(hWnd))
                NW_Restore(hWnd);
            else
                {
                if (!IsIconic(hWnd))
                    GetWindowRect(hWnd, (LPRECT)&gWinPosn.rcRect);
                NW_Maximize(hWnd);
                }
            break;

        case API_RESTORE_WINDOW_CDCMD:
            NW_Restore(hWnd);
            break;

        case API_CLOSE_WINDOW_CDCMD:

            SaveWindowPosition(hWnd);
            GetWindowRect(hWnd, (LPRECT) &rcRect);
            ShowWindow(hWnd, SW_HIDE);
            UpdateWindow(hWnd);
            APINotReady(ghAPI, API_NO_MODE);
            if (!OMF_Closing(ghOMF, (LPRECT) &rcRect))
               NoteError();
            break;

        case NEW_SHAPE:
            /* Get the handle to the menu of the current window */
            hMenu = GetMenu(hWnd);

            /* Uncheck the old menu item */
            CheckMenuItem(hMenu, gnShape, MF_UNCHECKED);

            /* Check the new menu item */
            if ((gnShape = intCmd->internal.ICmd) != SHAPE_NONE)
                CheckMenuItem(hMenu, gnShape, MF_CHECKED);

            /* Send a paint message */
            InvalidateRect(hWnd, (LPRECT)NULL, TRUE);
            UpdateWindow(hWnd);  /* Force repaint for every shape,
                                    not just last shape, when playing
                                    back several NEW_SHAPE commands */

            /* Are there views of the shape program in any other    */
            /* object?  Set the new_data flag for all views of      */
            /* shapes */

            if (OMF_SetNewData(ghOMF, 0))
                 {
                 /* Notify the destinations */
                 if (!OMF_AnnounceNewData(ghOMF))
                      NoteError();
                 }

            break;

        default:
            NoteError();
            break;
        }

} /* End of CommandProcessor */

Figure 24:

Conventional Windows applications deal with two primary types of messages.

Messages resulting from user actions

These result from mouse clicks, mouse movements, menu selections, or

keystrokes. They are acted on immediately.

    WM_COMMAND    Results from menu selection

    WM_SYSCOMMAND    User selected something from system

        menu or minimize/maximize

MS Windows housekeeping messages

    WM_CREATE    Sent when CreateWindow is called

    WM_PAINT    Sent to update the screen

    WM_ERASEBKGND    Sent when window background needs clearing

    WM_DESTROY    Terminate the message loop

Figure 26:

Messages resulting from user actions

These messages result from mouse clicks, mouse movements, menu selections,
or keystrokes. They are not acted on directly, but rather are translated by
the Action Processor into commands for the Command Processor in the
application's own task language. (See message type 5.)

    WM_COMMAND    Results from menu selections.

    WM_SYSCOMMAND    User selected something from                     system
menu or minimize/                    maximize.

OMF Messages

These messages come from the OMF as WM_OMF messages. They are used to
communicate either with the OMF itself or with other objects. They each
correspond to an OMF method that this application implements. A method is
the code that is executed in response to a given message: for example, the
code needed to create, open, or terminate this object.

    CREATE_OMF    Respond by reading data file to             remember the
"persisting"                 state of the object when it was
last closed. Was it a     triangle, and

        so on.

    WARM_START    Indicates that the receiving                 object was
shut down in a

        consistent state the last time it                 was run. The
object can treat as             valid any context or state
data that was saved at shutdown.

    OPEN    Respond by reading properties to             find the last size
and position of             window. Move the window there.

    HAS_METHOD    Respond to say if a particular                 OMF
"method" is supported by             HPShape.

    INIT_VIEW    Respond by setting up a view                 specification
for the data to be                 displayed in a  requesting object.

    GET_SIZE    Respond by saying what size                 HPShape needs to
allow it to                  display itself as a view in a
destination object.

    DISPLAY_VIEW    Paints the visual view from                 HPShape in
the DC specified by a             destination object.

    COPY_SELF    Allows this object to be copied             when it
iscontained in another                 object.

    TERMINATE     Respond by saving current shape             in data file.

    DIE_PLEASE    Tells this object to terminate itself.

    WINDOW_TO_TOP     Tells this object to bring its                 window
to the front.

NewWave API messages

    User request

    Agent request

    Interrogate messages

    API_INTERROGATE_MSG

    A request for information from the application.

    API_SET_MODE_FLAGS_MSG

    Used to change mode the application is in, such  as Playback, Record,
and Monitor.

MS Windows Housekeeping messages

    WM_PAINT    Sent to update the screen.

    WM_CLOSE    User closing window.

Internal Messages for the Command Processor

These messages originate inside the application. They may result from a
translated user action or from an Agent script being played back. Each
corresponds to a command in the application's task language.

API_MINIMIZE_WINDOW_CDCMD

Requests application to iconize itself

API_MAXIMIZE_WINDOW_CDCMD

Requests application to grow to take up the whole screen.

API_RESTORE_WINDOW_CDCMD

Requests application to return its window to the previous size.

API_CLOSE_WINDOW_CDCMD

Requests application to close its main  window.

NEW_SHAPE    Rerender the current shape.



Emulating the UNIX RS-232 General Serial I/O Interface Under DOS

 Michael J. Chase

One popular means of transferring information between DOS1 or UNIX
applications and terminals or control devices is the use of RS-232 serial
communication. This article focuses on writing applications that rely on
RS-232 serial communication under both DOS and UNIX. We will explore the
generalized serial I/O interface provided under UNIX and a device driver
that emulates it under DOS.

UNIX programmers have a general interface for asynchronous serial devices
that is independent of hardware; it has many useful features and, once
understood, is very easy to work with. Programmers working in the DOS
environment, however, usually can't use the COM1 or COM2 serial device
interfaces because they are not interrupt driven and do not support
buffering or XON/XOFF handshaking. Third-party communications libraries are
often called upon to help, but most require the C programmer to learn at
least 20  function calls to a proprietary interface--20 more than most
people would prefer to have to learn. Moreover, third-party communication
code usually will not port to UNIX-based systems or to another vendor's DOS
communication libraries.

Documentation for the generalized UNIX serial device seems incredibly terse
and hard to read if you are not familiar with the many details surrounding
asynchronous communications. After some explanation, however, the serial
device interface becomes easy and convenient to use. Usable features
(buffering, XON/XOFF handshaking, watchdog timers, parity control, exception
handling, line disciplines, and so on) of this general interface are
presented so that portable device control software can be written for
general and embedded applications under both UNIX and DOS. Trade-offs in
communications software design (buffer sizes, communication attributes,
error recovery, and so on) are also discussed.

Communication Device Drivers

A major goal of all device drivers is to provide a logical software
interface for applications that isolates them from physical hardware. The
physical hardware can differ; however, the logical software interface to the
hardware (device) remains the same. This is true for block-oriented devices
(disk interfaces) as well as character-oriented devices (serial port
interfaces).

Both UNIX and DOS provide block and character device drivers that allow C
programs to read and write bytes by using standard library calls through the
file system. Familiar I/O function calls such as open, read, write, and
close (level 1 functions) or stream-buffered functions such as fopen, fread,
fscanf, fwrite, fprintf, and fclose (level 2 functions available in the
standard library) can be used to communicate with a file or a serial device.
These functions afford programmers writing serial I/O software in UNIX and
DOS portability across many hardware platforms.

There is one problem though. The default asynchronous device driver shipped
with DOS is not very powerful because it does not allow you to change speed,
parity, buffer control, handshaking, and so on, independent of the hardware.
To control a serial device from a C program, calls to the BIOS must be
made--a serious impediment to portability. Fortunately there are three
viable methods you can use to write serial I/O software for the DOS
environment: you can build your own communications software, use linkable
communications libraries provided by a third party, or use a device driver
capable of emulating the popular UNIX serial I/O interface under DOS.

Building Software

Building a communications functions library from scratch has certain
advantages. It gives you control over exactly what you need, it helps you
learn about communication port hardware, and it gives you ownership of
source code.

By and large, though,  you will be reinventing the wheel. You will need a
hardware debugger and/or a logic analyzer to catch the most subtle bugs,
especially those found in interrupt logic. You will also need to invest a
great deal of time in supporting, updating, and maintaining the software--in
other words, you will become your own technical support staff.

Third-Party Libraries

Third-party add-on communication libraries may help you write serial I/O
programs by providing a C language interface for controlling device
attributes. Your C code, however, will be married to these typically
nongeneral interfaces under DOS. Therefore, you must weigh both the
advantages and disadvantages of this scheme.

An advantage of third-party libraries is their highly granular control over
speed, parity, stop bits, XON/XOFF, buffer management, and so on. Another
advantage is not having to install a device driver, since device drivers
have to be loaded when a system is booted and then remain part of the
operating system.  I/O functions loaded with your programs, however, are not
resident in the operating system, do not consume any space when your program
exits, and do not incur operating system overhead to pass information
through the file system.

One disadvantage is that the interface to a third-party library is not
through the file system; another is that calls to fopen, fread, fscanf,
fwrite, fprintf, and fclose will not work. You must devote the time to learn
a new set of interface functions supported by a given vendor. Some
interfaces are unnecessarily complex; one commercial vendor, for example,
boasts more than 125 serial communication function calls. Another
disadvantage is that simple command line (or batch file/shell script)
redirection is not supported. For example, under DOS the simple command line

C> dir > \device\tty01

will not work. Further, the logical interface is vendor specific; C code
that is portable between DOS and UNIX is nearly impossible to write.
Finally, some third-party libraries are designed as terminate-and-stay
resident (TSR) and consume memory.

 If you only develop for DOS and portability is not an issue, or if you only
need a serial device interface for DOS that emulates the widely accepted
UNIX serial interface, the advantages probably outweigh the disadvantages.
In that case, and with the bridging of the UNIX and DOS worlds, this option
deserves consideration.

Emulating UNIX Serial I/O

A UNIX-compatible serial device driver under DOS provides a number of useful
advantages. First, it gives you sufficient control over speed, parity, stop
bits, XON/XOFF, buffer management, and so on. Second, C code that controls
serial I/O is portable between DOS and UNIX. UNIX programmers need not learn
a new serial I/O interface for DOS and vice versa. Third, the interface is
through familiar file system calls (such as fopen and fprintf) so command
line (and batch/shell) redirection is possible. In fact, the interface is so
general that it allows any language-supporting file I/O (C, Assembler,
Pascal, Clipper, and so on) to use the serial device. Fourth, the
controlling interface is via one standard function call, namely IOCTL. It is
the same under both UNIX and DOS. Fifth, standard DOS-critical error
handlers can be used to trap run-time exceptions.

Emulation, however, presents its own set of problems. Since device drivers
are only loaded once, at boot time, and DOS supports no explicit resource
management, an existing application (a TSR, for example) could corrupt the
device driver by competing for the same communication port hardware. (Add-on
communication function libraries can also suffer the same dilemma.)

The device driver always consumes memory, even though it may not be used.
The sample DOS serial device driver accompanying this article emulates the
UNIX driver by consuming almost 8Kb, excluding buffers. (Code for the device
driver may be downloaded from any MSJ bulletin board--Ed.) Furthermore,
applications depending on the driver cannot load it before executing,
although they can test for its absence and recover. Microsoft documents no
formal mechanism (that is, a DOS function call) for dynamic device driver
loading, except for its mouse driver.

Finally, since DOS is not reentrant,  and must be entered to gain access to
the device driver, buffers cannot be resized at run time. They must be sized
at boot time and remain fixed.

Of the three methods you can use to write serial I/O software in the DOS
enviroment, the best one is emulation of the UNIX serial I/O device driver.
It is the most general solution for a given need; which means it best meets
the goals of providing transportable code, a standard interface, and
minimization of the learning curve. The issues you will need to address when
developing serial I/O devices are discussed next.

Blocking and Nonblocking I/O

Normally when a function call is made requesting information from a block
device (a disk), the function call will return only when the request is
satisfied or when an error occurs. For example, the following call to read
will wait until 10 characters are transferred or an error occurs:

if(read(fd, buffer,10)!=10)
{
   /* process the error */
}

If the read is from the disk, the request is usually not satisfied until the
disk access is complete. Under the DOS operating system, the application
program waits for DOS to service the request completely. Under a preemptive
multitasking system such as UNIX, the program (or process) is put to sleep
(that is, the program blocks) until the request is satisfied; then it is
awakened ( or unblocked) and is eligible to run. Note that while the process
is asleep, the operating system can serve other processes. DOS, however, is
a single-tasking system, so a program must patiently block until DOS returns
control--nothing else can run.

If you have a serial communications link with the same read calling for 10
characters, and no other characters are available, your process will block
under both UNIX and DOS. If nothing shows up, the DOS program may block
forever. The UNIX program also blocks, but other programs can run. Both DOS
and UNIX, however, can be convinced not to block if nothing is available.
Several issues related to blocking and nonblocking I/O must be addressed:
The minimum amount of time and/or number of characters for which a read
request waits to be assembled before being partly or fully satisfied; the
number of m characters (if available) with n requested (m< n), that should
be picked up; whether the request is free-form and just asks for a completed
line (that is, whether all characters are up to a ' \n' or some other line
delimiter); whether or not line editing is available so that mistakes can be
corrected; and how the EOF condition will be detected and handled. These
issues are explored in later examples; as it turns out, they are all options
that can be configured through a general interface.

Asynchronous Driver

There is one general interface for controlling serial I/O parameters under
UNIX--the IOCTL system call. Its interface specification is shown in Figure
1. If you have worked with DOS, you know that there is a similar, but not
equivalent, DOS function call (44H) named IOCTL. Unfortunately, there is no
documented evidence that lets you use IOCTL to control COM1 or COM2
attributes such as bps, parity, and hardware handshaking. (Note that the DOS
IOCTL call allows several operations to be performed other than those
discussed here, especially on block devices.)  BIOS calls must be used.

Microsoft borrowed the UNIX device driver philosophy for DOS, but did not
generalize the DOS asynchronous serial I/O device interface as they did the
disk drive interface. As a consequence, you must use one piece of serial
hardware, the 8250 UART, mapped at a fixed address through the BIOS.

To get around that, DOS device drivers that use the general UNIX serial I/O
interface are available; they replace the COM1 and COM2 drivers provided
with DOS. The UNIX terminal interface serves as an excellent model for DOS,
as have many other UNIX features (such as the hierarchical file system, the
I/O subsystem, redirection, pipes, and environment variables).

Before using the generalized UNIX serial I/O interface, you must know the
following: the bit rate, character data length, parity, and stop bit(s)
requirements for communications; the scheme to be used to manage the flow of
information in both the transmit and receive directions (XON/XOFF, RTS/CTS,
DTR/DSR); how the device handles exceptions such as loss of modem carrier, a
break signal, or ^C; the kind of input or output post-processing, if any,
that needs to be done (for example, CR->NL, NL->NL/CR, convert
cases, expand tabs); whether DOS programs should use binary or text mode
when communicating with a device driver; and how parity, framing, and
overrun errors should be detected and handled.

Getting Started

Just as you must open a disk file (with open or fopen) to modify it, you
also must open a serial device, so that it can be written to, read from, or
advised of new operation attributes. You can use either method shown in
Figure 2; both level 1 and level 2 file I/O functions are provided in the
standard C library. Note that in the call to fopen, "rwb" advises the I/O
system not to insert CR/LF translations within the data and to pass the ^Z
(1aH) character as binary data.

The advantages of level 1 are that there is no buffering overhead in the
standard library and that information specific to the operating system such
as locking and networking, can be communicated. Level 1 provides more
control for character-at-a-time reading and writing. A disadvantage of level
1 is that only read and write are available for I/O. The process can require
a costly context switch to the operating system kernel for each read or
write function call.

Level 2 allows the use of fprintf, fscanf, fgets, fputs, and so on, for I/O.
These are more portable between different operating system environments.
Buffers can be flushed (reset) in the standard library. Context switching to
the operating system for I/O is only necessary when output buffers are
flushed or when input buffers are replenished, not for each call to fread or
fwrite.

A problem with level 2 is that there may be too much buffering overhead. The
device, operating system, and standard library all maintain buffers.
Furthermore, stream buffer overhead for character-at-a-time I/O is more
beneficial for block devices (disks) than character devices (serial I/O
ports). See Figure 3 for more information about I/O paths.

Despite the disadvantages of using level 2 file I/O, it is the best choice
for the application discussed here because of the convenience provided by
the printf and scanf functions. Also, the standard library can be convinced
that the stream buffer it maintains for a file stream is of length 1 with a
call to setbuf. Buffering overhead is reduced to approximately that of level
1 I/O, and fflush need not be called after each fwrite or fprintf
transaction.

Once a device has been opened, it can immediately be read from or written
to. For now, assume that either the device has been initialized or its
default attributes will suffice. You can therefore communicate with the
serial device, as in Figure 4.

Changing Parameters

Next, consider the mechanism for changing serial device attributes. As
stated earlier, the IOCTL function is used to get or set device attributes
passed in a C structure. To use the IOCTL function call, the device must be
opened with the open function, and a valid file descriptor (a DOS file
handle) must be obtained. If the standard library function fopen is used,
the file descriptor can be derived from the file pointer with the fileno
macro found in stdio.h. One C structure, termio, is used to set or get
attributes for all asynchronous devices under UNIX; it is found in the
include file termio.h. Members of the termio structure contain information
on input modes, output modes, control modes, line disciplines, and an array
of eight special control characters. The termio structure is shown in Figure
5.

Each flag in the termio structure is actually a collection of several flags
that produce a bit pattern fitting into an unsigned short. The flags have
the effects listed below.

c_iflag Instructs the device how to react to received input. Break, parity
generation, parity checking, bit stripping, carriage return to new line
mapping, uppercase to lowercase mapping, and XON/XOFF software handshaking
can be set.

c_oflag Instructs the device how to process output. Mapping of case, new
line translation, fill characters, and delays for CR, HT, NL, and BS can be
set.

c_cflag Allows the setting of speed (bps), character size, stop bits, parity
options, and control of data terminal ready (DTR), request to send (RTS),
and data carrier detect (DCD) RS-232 control signals.

c_lflag Enables exception handling, input-queue processing, echoing, line
editing, buffer management, and mapping of special characters.

c_line Selects a line discipline; usually set to 0. The line discipline
selects a mode of mapping (filtering) characters from a raw input queue to a
canonical input queue (this will be explained later). This filter mechanism
allows line editing, conversion of lowercase to uppercase, expansion of
tabs, and so on. Most UNIX systems support only one well-established line
discipline, but other disciplines have been implemented (for example, for
handling synchronous data links supporting SDLC types of communications).

c_cc[NCC] An array of eight characters that can be assigned to represent the
interrupt and quit signals (such as ^C, DEL, and BREAK), the erase
character, EOF, and EOL. Some of the c_cc[] positions have dual meanings
that depend on other flags, specifically the c_cc[4] (EOF, MIN) and c_cc[5]
(EOL, TIME) positions.

All of the flags of the termio structure are set by using the bitwise AND(&)
and OR(|) operators with the constants provided in the termio.h include
file. Figure 6 describes many of the possible flags. They are fully
documented in the manual pages under termio(M) (Xenix) and termio(7) UNIX
System V and by vendors of equivalent drivers for DOS.

The fragment of code shown in Figure 7 demonstrates how to open and obtain
the current attributes for the device /dev/tty01. Flags are assembled to
change the speed to 9600 bps, to change the number of data bits to 7, and to
enable reading.

Once the flags are configured, a call to IOCTL is made to set the
attributes. Set all values of the termio structure to valid values so the
driver will not configure randomly. Also check return codes from library
calls (discussed later).

Data Acquisition

Computer-to-terminal communications is a simple, general, and easily tested
form of data acquisition. The program in Figure 8 will send a request
message to an attached I/O device and expect a 10-byte response back within
2 seconds. If the expected response is received in time, data received back
from the device is written to a log file: otherwise error recovery is
started. Most, but not all, data acquisition devices request responses in a
similar way. If an unsolicited response arrives, it will be buffered for the
next read and is not lost. Selected attributes for the device, which are
given below, are stored in the termio structure.

c_iflag IGNBRK advises the driver to ignore the break sequence completely if
the break signal is received. The driver could otherwise cause an interrupt
signal to be sent to the application. IGNPAR states that any received
characters containing detected parity errors are ignored. IXON enables
software flow control on the output stream; IXOFF enables the same on the
input stream.

c_oflag No flags are selected and all features represented by this flag are
disabled.

c_cflag B4800 selects the bit rate of both the transmit and receive
channels. CS8 advises that 8 data bits be sent and received. CREAD enables
the read system call to transfer characters from the device driver's buffer.
HUPCL drops the DTR signal on the last close of the device (the device can
be opened more than once). CLOCAL advises the device not to examine the DCD
signal as a qualifier when the driver opens the device.

c_lflag No flags are selected, and all features represented by this flag are
disabled.

c_line The default line discipline is 0.

c_cc[0] (VINTR) Not used since the ICANON | ISIG flags are not set.

c_cc[1] (VQUIT) Not used since the ICANON | ISIG flags are not set.

c_cc[2] (VERASE) Not used since the ICANON | ISIG flags are not set.

c_cc[3] (VKILL) Not used since the ICANON | ISIG flags are not set.

c_cc[4] (VEOF/MIN) Since the ICANON flag is not set, c_cc[4] contains the
minimum number of characters that must be collected by the driver before a
read call is satisfied. Otherwise it would represent the EOF character.
c_cc[4] can be used in tandem with c_cc[5].

c_cc[5] (VEOL/TIME) Since the ICANON flag is not set, the c_cc[5] attribute
contains the number of 0.10 second increments the driver should wait for
before returning from a read. Otherwise it would represent the EOL
character. c_cc[5] is a watchdog that will wait until the requested (or
minimum) number of characters is available or the timer has expired,
whichever comes first. This feature allows application programmers to
specify the amount of time to wait for an attached serial device to send
something (for example, after a query has been sent to it). c_cc[4] and
c_cc[5] let you specify the minimum number of characters that must be
collected and/or the maximum amount of time to wait for the characters; no
timing loops are required in the application.

c_cc[6] Reserved.

c_cc[7] (SWTCH) Not used.

Note that the stream buffers for the tty_stdin and tty_stdout have been set
to length 1. This example can be adapted to work with almost any I/O device
that is request/response driven. As characters arrive, they are buffered
until the next read call is satisfied. tx_block and rx_block are simple
character arrays; they could be structures containing more detailed
information about what is sent and received.

Also note that the path name of the device is /dev/tty01. This is a standard
pathname to a UNIX device driver residing in the /dev directory. Curiously
enough, /dev/tty01 is also a valid path under DOS to the device tty01. The
Microsoft C run-time library translates the forward slash ( / ) to the back
slash ( \ ) in file or device pathnames on calls to file system routines.
However, \dev\tty01 must be used at the DOS command line (batch file) level.

Input and Output Queues

Once the serial device attributes have been selected and set and the device
has been opened, I/O may begin. Three queues are maintained by the UNIX
serial device driver--the output queue, which can be written to, and the raw
and canonical queues, which can be read. (Do not confuse these queues with
stdin, stdout, and stderr.)

Characters that are written to the device are copied to the output queue for
transmission. Depending on flag settings, delays and/or translations may be
performed as characters are transmitted. Figure 9 shows the relationship of
the raw and canonical input queues. An application can choose to read
characters from either. The raw input queue is simple: anything received
(excluding parity errors and break sequences) is buffered and passed
directly to the application. When reading from the raw queue, it is possible
to configure the driver to implement a watchdog timer or specify a minimum
number of characters that must be available before a read completes
successfully. The size of the raw queue is limited by a shared pool of
buffers; these buffers, called clists under UNIX, are dynamically allocated
and released. There is no pool of clists under DOS; buffers are usually
fixed, because the device driver is accessed through the DOS file system.
Remember that DOS is not reentrant, so it is nearly impossible to perform
dynamic memory allocation of clists inside any DOS device driver.

The canonical input queue obtains its characters from the raw queue via a
line discipline. The line discipline is analogous to a filter; as characters
are obtained from the raw queue, they may be translated to other sequences.
For example, CR can translate to NL (ICRNL flag). The canonical input queue
is enabled with the ICANON flag, which also advises the device driver to
look for the special characters in the c_cc[] array and take appropriate
action in accordance with  the IBRK and ISIG flags.

Interactive Terminals

Interactive programming requires a special style of coding. People are much
less predictable than machines, so special care must be taken when prompting
and handling responses (valid or invalid). This example assumes that there
is a dumb terminal or PC connected to the serial port, either directly or
through a modem.

The program in Figure 10 is trivial if input is from and output is to the
DOS console device. During an interactive session at a remote terminal on a
serial link, however, special precautions must be taken. When the terminal
attached to the serial link is prompted for an integer, there are three
possible responses: correct, incorrect, or no response. The enclosed switch
statement handles these three cases. This example sets up a terminal with
many of the features that a shell might use for command line interpretation,
notably line editing, character echoing, and abort sequences.

Selected attributes for the device, given below, are stored in the termio
structure. Note that the buffers for the streams tty_stdin and tty_stdout
have been set to length 1.

c_ifla IGNBRK advises the driver to ignore the break sequence completely if
the break sequence is received; otherwise, the driver could cause an
interrupt signal to be sent to the application. IGNPAR states that any
received characters containing detected parity errors are ignored. IXON
enables software flow control on the output stream. IXOFF enables software
flow control on the input stream. CLOCAL disables modem control; DCD is
ignored when opening the device. ICRNL specifies that the CR character is
converted to the LF (' \n ') character on input.

c_oflag OPOST enables the post-processing of output. ONLCR converts the ' \n
' (LF) character to the CR/LF characters on output. OCRNL converts the CR
character to the LF (' \n ') character on output. TAB3 expands the
horizontal tab character to spaces up to the next tab stop. Tab stops
usually occur every eight characters.

c_cflag B4800 selects the bit rate of both the transmit and receive
channels. CS8 advises that eight data bits are sent and received. CREAD
enables the read system call to transfer characters from the device driver's
buffer. HUPCL drops the DTR signal when the device is closed for the last
time. CLOCAL advises the device not to examine the DCD signal as a qualifier
in opening the device.

c_lflag ICANON enables the processing of character filtering from the raw
input queue to the canonical input queue. Line editing and character echoing
is now performed by the device driver. The watchdog for a timer and/or a
minimum number of characters seen in the last example is no longer active.
c_cc[4] and c_cc[5] now contain characters representing EOF and EOL,
respectively. ISIG tells the driver to check each input character against
the special characters designated in the c_cc[] array for INTR, QUIT, and
SWTCH. If any of these characters are found in input, the appropriate action
is taken (that is, signals are sent). ECHO echoes all characters received to
the output queue. ECHOE echoes the designed erase  character as BS-SP-BS,
thus erasing the character in error.

c_line Selects a line discipline; usually set to 0.

c_cc[0] (VINTR) If matched on input, the interrupt signal is sent to the
controlling process. Under DOS, the current process is killed. Note that the
ICANON | ISIG flags are set in c_cflag.

c_cc[1] (VQUIT) If matched on input, the quit signal is sent to the
controlling process.

c_cc[2] (VERASE) If matched on input, the last valid character entered is
erased.

c_cc[3] (VKILL) If matched on input, whatever has been assembled in the
current line is erased.

c_cc[4] (VEOF/MIN) This is the designated EOF character, typically ^Z (DOS)
or ^D (UNIX), although any character will work.

c_cc[5] (VEOL/TIME) represents the EOL character; typically ' \n '; any
character will work, however. This character is the anchor used by the line
discipline routines to mark the end of a line. Typically lines are assembled
and edited by the user. When the EOL character (stored in c_cc[5]) is
received, the line is available for reading by the application.

c_cc[6] Reserved.

c_cc[7] (SWTCH) Not used.

Serial Link

The examples of data acquisition and request/response communications
discussed in Figures 8 and 10 demonstrated important and useful features
when using the UNIX (or compatible) serial I/O device driver. All I/O was
performed with familiar calls to the C standard library. Many useful
features of the UNIX serial I/O device driver can save the C programmer many
headaches--features such as watchdog timeouts, buffering in both the TX and
RX directions, CR and LF translation on both input and output streams, line
editing, tab expansion, and XON/XOFF software handshaking. One common
interface, IOCTL, is used to adjust device parameters using a standard
system call. Serial I/O software is portable between UNIX and DOS systems.

There are other modes of serial link communications and other programming
contexts, but they are usually variations of one of the examples presented.
Advanced techniques of serial communication programming might address any of
the following topics.

Advanced Modem Control If the CLOCAL flag is cleared, the device will wait
for the DCD line to become active for an open call to complete. This allows
the application to block for a modem connection before the device driver
opens. UNIX programmers can set the O_NDELAY flag when performing an open
operation. The open function will then use errno to return immediately,
indicating success (DCD present) or failure (DCD absent) in opening the
device.

Reacting to Interrupt Signals Under UNIX, the reception of DEL or ^\
(changeable defaults in the c_cc[ ] array) sends the signals interrupt or
quit, respectively. Reception of a BREAK sequence will also send the
interrupt signal. If the flags ICANON | ISIG are set, a signal is sent to
the controlling process. Under DOS, some device drivers provide an option to
kill the currently executing process by issuing DOS interrupt 4BH.

Sending the BREAK Character Calls can be made to IOCTL to send the BREAK
character. See the IOCTL command TCSBRK.

Forcing the Driver XON/XOFF State Calls can be made to IOCTL to suspend
output and restart suspended output. See the IOCTL command TCXON.

Handling Parity Errors If parity checking is enabled by setting the PARENB
and PARMRK flags and clearing the IGNPAR flag, a character (X) received with
a parity error is passed as a special sequence (' \x7F ', ' \0 ', X) to
identify the character in error. The character ' \x7F ' is then read as '
\x7F ',' \x7F '.

Recovering from DOS Critical Errors DOS will translate critical errors to
interrupt vector 24H. If the application traps this interrupt, all critical
errors encountered by a device driver should trap the user's exception
handler with sufficient information (device name and type of critical error)
to recover.

Command Line Interface

As mentioned earlier, an advantage to performing serial I/O with a device
driver is having access through the file system. Command line redirection
(and batch/shell files) can communicate with the device. For example, under
DOS the command

C>dir > \dev\tty01

sends a directory listing to a serial device. Note that no mention has been
made of how to change device attributes from the command line. This is
accomplished with the UNIX command stty. The stty command is also capable of
obtaining the current attributes of a device and displaying them. The same
command is available with DOS serial device drivers; however, DOS requires
the device path to be \dev\tty01. Refer to Figure 11 for an example of stty
under both DOS and UNIX.

In the examples in Figure 11, the stty program reopens its standard input,
the serial device /dev/tty01, and sets its attributes to 9600 bps, even
parity enabled, 7 data bits per character, XON/XOFF software handshaking
enabled, and modem control enabled. This configuration allows communication
with a popular laser printer over the serial link. In the two commands shown
in Figure 12, file_1 is sent through a PostScript preparation filter and
finally to a printer attached to /dev/tty01.

System Interface

There are some intrinsic features of the operating system that concern both
UNIX and DOS programmers that you should be aware of when opening a serial
device for I/O. The first concern is how the blocking (or process
suspension) is handled. Under DOS, there is no facility to suspend a process
on the assumption that it will resume running when some event has occurred
(or has not occurred). Under UNIX, a terminal driver can cause the
suspension of a process if there are no characters to read or a line has not
yet been completely assembled via a selected canonical line discipline. Once
characters are ready or a line has been assembled, a suspended (or blocked)
process is awakened to run. Since DOS has no facility that suspends a
process to wait for I/O, any request to a device driver should return
immediately (after checking for set watchdog timers), whether characters (or
lines) are ready or not. If nothing is available, EOF can be returned.

The second concern is that there are differences in the treatment of EOF
indications. DOS supports two access modes for reading and writing character
devices--ASCII text mode and binary mode. If the character 1aH (^Z) is read,
the input from a file or device opened in text mode is terminated (shut
down) and an EOF indication is returned. Even if there are characters
pending, any and all read requests from this point forward are ignored and
the EOF indication is always returned. If the file were opened in binary
mode, the EOF character (^Z) would be passed to the application, and further
requests for characters would be honored.

Text mode also insists on translating LF to CR/LF on output and CR/LF to LF
on input, so I/O in text mode could add characters or delete them. Binary
mode reads and writes exactly what is requested. These features are
intrinsic to DOS.

DOS device drivers should return EOF if there is nothing to read (after
checking for set watchdog timers). A device driver could block the process
attempting the read; the application might then hang forever waiting for a
character. If EOF is returned, the application can do something else and
attempt another read later. Anything that arrives between reads will be
buffered.

When the EOF character, designated by c_cc[4], is received by a UNIX
character device driver, all characters waiting to be read are immediately
passed to the application without waiting for a new line, and the EOF is
discarded. Thus if no characters are waiting in the input queue--that is, if
EOF occurred on the beginning of a line--no characters will be passed back
to the application; this is a standard EOF indication under UNIX.

The third concern is the difference in the treatment of signals. The signal
mechanism available under the UNIX operating system is not available under
the DOS operating system, although most C run-time libraries, most notably
Microsoft C 5.10, have mechanisms that perform similar functions.

The signal( ) function call allows the UNIX and DOS programmer to catch
special messages sent to processes when exceptions such as floating point
overflow, ^C, and modem disconnect occur. Async device drivers under UNIX
are also able to send signals to related processes. These signals are
typically INTR, QUIT, HANGUP, and BREAK. DOS does not provide the signal
mechanism that UNIX does, although DOS drivers can be designed to have
similar functions.

Conclusion

This article explored how to write portable serial I/O software, useful
features of a general serial I/O device interface operating under both UNIX
and DOS. UNIX and its many hybrids all share the same serial I/O interface.
With AT&T, Digital Equipment, Hewlett-Packard, IBM, Microsoft, and other
companies, embracing UNIX as a standard product offering, it simply cannot
be ignored by the professional programmer.

Figure 1

#include   <termio.h>

int ioctl( fd, command, argument);
int fd;
int command;

union
{
   int i_arg;
   struct  termio  *s_arg;
} argument;

ioctl()

Returns 0 if successful in performing the requested command; returns -1
otherwise and errno is set to reflect the reason for the error. EBADF,
ENOTTY, EINTR, EFAULT, EINVAL, EIO, ENXIO and ENOLINK are possible values
for errno; see the include file <errno.h>.

fd

A valid file descriptor (DOS file handle) obtained from a successful call to
open() or fopen(). It must be possible to write to the file to set device
attributes and it must be possible to read from the file to get device
attributes.

command

There are seven commands that tell the device driver how to respond. Note
that the value and type of the last parameter, argument, will depend on the
command selected.

TCGETS

Get current attributes from the device (TCGETA on some UNIX systems).
argument.s_arg points to the structure that will receive the existing
attributes.

TCSETS

Immediately set the passed attributes in the device (TCSETA on some UNIX
systems). argument.s_arg points to the structure that contains  the
attributes to be set.

TCSETAW

Set passed attributes after the output buffer has drained. argument.s_arg
points to the structure that contains the attributes.

TCSETAF

Wait for the output to drain, flush the input queue, then set the new
attributes. argument.s_arg points to the structure that contains the
attributes.

TCSBRK

Send a break sequence for 250 milliseconds. argument.i_arg must be 0.

TCXONC

Start (XON) or stop (XOFF) the transmission of output from the device. If
argument.i_arg is 0, output is suspended; if 1, suspended output is
restarted.

TCFLSH

Flushes the input and/or output queues. If argument.i_arg is 0, flush  the
input queue; if 1, flush the output queue; if 2, flush both the  input and
output queues.

argument

The union argument contains one of two data types:

struct termio * s_arg

or

int i_arg

The type depends on the command being issued. s_arg is a pointer to a C
structure that will be used for setting or getting device attributes. i_arg
is an integer.

Serial Communications Terms

Asynchronous Communications Character-at-a-time transmission. Characters are
randomly sent one at a time, far apart or close together. They are separated
by start and stop bits. (See Framing.)

Baud Symbols per second. Each symbol usually contains analog information for
2^s:n: bits (n = 0, 1, 2, 3, ... ). For example, a symbol that has four
different voltage levels (n = 2) contains information for 2 bits; thus a
baud rate of 600 will yield an effective bit rate of 1200 bps.

Blocked Process If a program (process) is waiting for an I/O completion, it
may be put to sleep; that is, marked as no longer eligible to run. The
process is unblocked (awakened) when the I/O request is complete; it is then
eligible to run.

BPS Number of binary transitions per second. (See Baud.)

Break BREAK asserts the SPACE condition on the serial line for approximately
0.25 to 0.30 seconds. It may cause a framing or overrun error to occur. It
is typically used to reset the serial link to some known state.

DCD Data Carrier Detect. An electrical signal stating the presence or
absence of a modem carrier. The DCD is used to indicate when two or more
modems have established a connection.

DTE/DCTE Data terminal equipment. Usually a terminal device capable of
generating or displaying information. Data circuit terminating equipment.
Usually a device capable of transmitting or receiving data over a chosen
medium. Examples are a modem and a line driver.

DTR/DSR Data Terminal Ready/Data Set Ready. A DTE advises that it is ready
to converse by raising DTR. The DCTE advises that it is ready to converse by
raising DSR. DTR and/or DSR are usually active for the duration of a
conversation.

Flow Control A method or protocol for governing the starting and stopping of
transmission. It is used so that resources can be managed to accommodate
incoming data;  typically  it advises a sending entity that a receiving
buffer is nearly full (stop transmitting) or nearly empty (start
transmitting).

Framing For asynchronous character transmission, framing identifies the
meaning of bits that constitute a character. The number of start bits (1;
SR), information bits (5, 6, 7, or 8; D0..D7), optional parity bit (0, 1;
P), and stop bit(s) (1.0, 1.5, 2.0; SP) are identified.

Example:
| SR | D0 | D1 | D2 | D3 | D4 | D5 | D6 | D7 | P | SP |

Hardware Handshaking Flow control that is managed at the hardware level. The
UART waits for permission to send characters, which is granted through the
RS-232 RTS/CTS signals.

Level 1 Direct I/O The C programmer has the choice of two levels of I/O:
level 1 or level 2. Level 1 I/O sends and receives characters directly from
the operating system (the operating system may buffer characters).

Level 2 Stream I/O The C programmer has the choice of two levels of I/O,
level 1 or level 2. Level 2 I/O sends and receives characters to and from
stream buffers (typically 512 bytes long) maintained by the C standard
library. Level 2 is written as a function of level 1. When write buffers
become full, they are flushed out to the operating system; when read buffers
become empty, more characters are requested from the operating system.

Line Discipline Conversion rules chosen for the c_lflag in the termio
structure. A mapping or filter strategy for transferring characters from the
raw input queue to the canonical input queue.

Mark  An electrical signal that denotes a logical binary 1. Under RS-232,
typically -3 to -25 VDC.

Overrun When too many data bits are received between the start and stop bit,
an overrun condition has occurred.

Parity A bit designated for error-detection purposes. There are two basic
types of parity--even and odd. If the parity bit is designated as odd (or
even), its job is to take a value that makes the total number of 1s in an
asynchronous character transmission odd ( or even); for example, 00010101P;
P = 0 odd parity (P = 1 even parity). Parity errors detect some, but not
all, types of transmission errors.

Ring Buffering A circular buffer or queue of finite size that has no
physical beginning or end. The logical beginning and logical end are
typically tracked with pointers. The next character put in the queue is at
the logical end of the buffer; the next character taken out of the queue is
at the logical beginning. If the buffer is full and a character is put in
the queue, the oldest character in the queue may be overwritten. A ring
buffer of size N will always contain at most the last N - 1 characters
in the queue.

RS-232 Recommended Standard 232. RS-232 is administered by the Electronic
Industries Association (EIA). Electrical characteristics for transmission of
information and signaling information is specified. RS-232 is also known as
CCITT V.24. Physical characteristics of a connector are also specified; for
example, the common DB-25.

RTS/CTS Request to Send/Clear To Send. Hardware signals used in flow
control. A DTE can request permission to send characters by raising RTS and
be granted permission with the activation of CTS by the DCTE.

SDLC Synchronous Data Link Communications. (See Synchronous Communications.)

Software Handshaking Flow control that is managed at the software level.
Software typically sends an XOFF (ASCII DC3; 13H) character to request the
suspension of character transmission (receive buffers are nearly full) and
an XON (ASCII DC1; 11H) character to resume a suspended transmission
(receive buffers are nearly empty).

Space An electrical signal that denotes a logical binary 0. Under RS-232,
typically +3 to +25 VDC.

Synchronous Communications Block-at-a-time transmission. Groups of
characters are sent in blocks of bits. There are no start or stop bits to
separate characters; there are, however, bit patterns (flags) that separate
blocks of bits.

UART Universal Asynchronous Receiver Transmitter. A hardware device whose
primary function is to perform serial-to-parallel conversion of electrical
signals.

XON/XOFF Characters that have been chosen for software flow control. An
entity receiving an XON is advised to resume (begin) transmission. An entity
receiving an XOFF is advised to suspend (stop) transmission. (See Software
Handshaking.)

Figure 2

#include <stdio.h>

#define    SERIAL_DEVICE   "/dev/tty01"

static    int    fd_tty;
static    FILE    *fp_tty;

/***************************************************************/
/*    L E V E L   1   O P E N   M E T H O D     */

if( ( fd_tty = open( SERIAL_DEVICE, 2 ) )  !=  -1 )
{
   /*  fd_tty now a valid UNIX file descriptor */
   /*  fd_tty now a valid DOS file handle      */
}
else
{
   /*  The device can't be opened.  */
}
/***************************************************************/
/*     L E V E L   2   F O P E N   M E T H O D     */

if( ( fp_tty = fopen( SERIAL_DEVICE, "rwb" ) )  !=  NULL )
{
   /*    fp_tty now a valid stream pointer    */
}
else
{
   /*    The device can't be opened.        */
}

Figure 3

I/O Paths: The path a character will take when an application writes it to
an output stream (or reads from an input stream).

 The application opens the device with a call to fopen(). The application
then sets up a buffer, count, and so on, and calls fwrite(). A call to
write() bypasses (2), the C standard library stream buffers.

fwrite() transfers bytes from the user buffer to a stream buffer maintained
by the standard library. Stream buffers are usually 512 bytes in length (see
the definition for BUFSIZ in the include file <stdio.h>).  Writing or
reading bytes to or from a stream buffer saves the overhead of making a
context switch to the operating system. When the stream buffer becomes full,
its contents are flushed out to the operating system with a call to write.

write() transfers bytes from the stream buffer to internal operating system
buffers (known as clists under UNIX) if the destination is a character
device. Under DOS, fixed buffers are most likely maintained by the device
driver itself. If the destination is a block device, write() transfers bytes
from the stream buffer to an operating system buffer (known as the buffer
cache under UNIX).

When the device is capable of transmitting, a byte is transferred from the
appropriate clist to a transmit register.

Finally, the byte held in the transmit register is shifted out to the serial
line designated for transmission.

The following description also applies to the fread() call, with the
exception that the sequence is reversed.

Binary information is shifted in from the serial line designated for
reception and is held in a receive register.

When the device has received a byte, it is transferred from the receive
register to the appropriate clist.

read() transfers bytes from the appropriate clist to a stream buffer
maintained by the C standard library. Note that there are two types of input
queues (clists)--the raw and canonical input queues. The raw input queue
contains exactly what was received. The canonical input queue contains
characters that have been transferred from the raw input queue and possibly
filtered per a selected line discipline.

 fread() transfers bytes from a stream buffer maintained by the standard
library to the buffer provided by the user. When the stream buffer becomes
empty, its contents are replenished by a call to the operating system
function read().

   fread() returns to the application program.

Figure 4

auto   int     in_int;
auto   FILE    *fp_tty;

/* Trivial request and response from the terminal */

/* Assumes fp_tty is opened in "rwb" mode */

do
{
   fprintf( fp_tty, "\nHello Terminal. \nEnter an integer: " );

} while( fscanf( fp_tty, "%d", &in_int )  !=  1 );

Figure 5

#define  NCC  8

struct    termio
{
   unsigned  short  c_iflag;   /* Input modes.     */
   unsigned  short  c_oflag;   /* Output modes.    */
   unsigned  short  c_cflag;   /* Canonical modes. */
   unsigned  short  c_lflag;   /* Local modes.     */

   char      c_line;           /* Line discipline. */

   unsigned  char   c_cc[NCC];  /* Special chars.  */

};

Figure 6

The c_iflag field describes the basic terminal input control.

IGNBRK    Ignore break condition

BRKINT    Signal interrupt on break

IGNPAR    Ignore characters with parity errors

PARMRK    Mark parity errors

INPCK    Enable input parity check

ISTRIP    Strip character

INLCR    Map NL to CR on input

IGNCR    Ignore CR

ICRNL    Map CR to NL on input

IUCLC    Map uppercase to lowercase on input

IXON    Enable START/STOP output control

IXANY    Enable any character to restart output

IXOFF    Enable START/STOP input control

The  c_oflag field specifies the system's treatment of output.

OPOST    Post-process output

OLCUC    Map lowercase to uppercase on output

ONLCR    Map NL to CR-NL on output

OCRNL    Map CR to NL on output

ONOCR    No CR output at column 0

ONLRET    NL performs CR function

OFILL    Use fill characters for delay

OFDEL    Fill character is DEL, else NUL

NLDLY    Select New-Line delays:

    NL0    New-Line character type 0 (no delay)

    NL1    New-Line character type 1 (0.10 second delay)

CRDLY    Select Carriage-Return delays:

    CR0    Carriage-Return delay type 0 (no delay)

    CR1    Carriage-Return delay type 1 (column position
dependent)

    CR2    Carriage-Return delay type 2 (0.10 second delay)

    CR3    Carriage-Return delay type 3 (0.15 second delay)

TABDLY    Select Horizontal-Tab delays:

    TAB0    Horizontal-Tab delay type 0 (no delay)

    TAB1    Horizontal-Tab delay type 1 (column position
dependent)

    TAB2    Horizontal-Tab delay type 2 (0.10 second delay)

    TAB3    Horizontal-Tab delay type 3 (expand tabs to spaces)

BSDLY    Select backspace delays:

    BS0    Backspace delay type 0 (no delay)

    BS1    Backspace delay type 1 (0.05 second delay)

VTDLY    Select Vertical-Tab delays:

    VT0    Vertical-Tab delay type 0 (no delay)

    VT1    Vertical-Tab delay type 1 (2.0 second delay)

FFDLY    Select Form-Feed delays:

    FF0    Form-Feed delay type 0 (no delay)

    FF1    Form-Feed delay type 1 (2.0 second delay)

The c_cflag field describes the hardware control of the terminal.

CBAUD    Baud (Bit) rate:

    B0    Hang up

    B50    50 bps (baud)

    B75    75 bps (baud)

    B110    110 bps (baud)

    B134    134.5 bps (baud)

    B150    150 bps (baud)

    B200    200 bps (baud)

    B300    300 bps (baud)

    B600    600 bps (baud)

    B1200    1200 bps (baud)

    B1800    1800 bps (baud)

    B2400    2400 bps (baud)

    B4800    4800 bps (baud)

    B9600    9600 bps (baud)

    B19200    19200 bps (baud)

    B38400    38400 bps (baud)

CSIZE    Select character size for both transmission and reception:

    CS5    5 bits

    CS6    6 bits

    CS7    7 bits

    CS8    8 bits

CSTOPB    Send two stop bits, else one

CREAD    Enable receiver

PARENB    Parity enable

PARODD    Odd parity, else even

HUPCL    Hang up on last close

CLOCAL    Local line, else dial-up (modem)

LOBLK    Block layered output

The c_lflag field of the argument structure is used by the line discipline
to control terminal functions. The basic line discipline (0) provides the
following:

ISIG    Enable signals

ICANON    Canonical input (erase and kill processing)

XCASE    Select canonical uppercase/lowercase presentations

ECHO    Enable echo

ECHOE    Echo erase character as BS-SP-BS

ECHOK    Echo NL after kill character

ECHONL    Echo NL

NOFLSH    Disable flush after interrupt or quit

The c_cc[NCC] array contains characters that are mapped to special actions.
As characters are received by the device, they can be checked against the
characters stored in the c_cc[] array. This checking is enabled by setting
combinations of the ICANON, ISIG, and IGNBRK flags. If a match is found, a
special action can be performed by the device driver. Characters
representing backspace, EOF, EOL, MIN, TIME, and assorted signals are stored
here.

c_cc[0]    (VINTR)    If matched, an interrupt signal is sent to the

        controlling process.

c_cc[1]    (VQUIT)    If matched, a quit signal is sent to the controlling

        process.

c_cc[2]    (VERASE)    If matched, the last enqueued character received is

        deleted.

c_cc[3]    (VKILL)    If matched, the current line buffer is flushed and the

        line is killed.

c_cc[4]    (VEOF/MIN)    If matched, the device driver assumes that the end
of

        input (EOF) has been reached. Any characters

        pending in the input line buffer are immediately

        passed to the application. This also serves as the

        minimum number of characters to collect in raw

        input mode when ICANON flag is not set.

c_cc[5]    (VEOL/TIME)    If matched, the device driver assumes that an
entire

        line has been assembled; the line is now available

        for reading by the application. This also serves as a

        timeout parameter if the ICANON flag is not set.

c_cc[6]    (Reserved)    Not used.

c_cc[7]    (SWTCH)    If matched, the current foreground process is moved

        to the background. This is the job control switch

        character (UNIX only).

Figure 7

#include  <stdio.h>

#include  <termio.h>

static  FILE    *tty_fp;
static  struct termio   tty_params;


   /* Open the device    */

   tty_fp = fopen("/dev/tty01", "rwb");

   /* Obtain its current attributes; stored at &tty_params */

   ioctl(fileno(tty_fp), TCGETS, &tty_params);

   /* Change desired attributes; stored at &tty_params     */

   tty_params.c_cflag = B9600 | CS7 | CREAD;
    ■
    ■
    ■
   /* Set new attributes */

   ioctl(fileno(tty_fp), TCSETS, &tty_params);

Figure 8

#include    <stdio.h>
#include    <termio.h>   /* Installed with .h files */

#define    TTY_STDIN_FILE_NAME     "/dev/tty01"
#define    TTY_STDOUT_FILE_NAME    "/dev/tty01"
#define    LOG_FILE_NAME           "logfile"

static    char    tx_block[10] = "SEND.DATA";
static    char    rx_block[10];
extern    int    errno;


main()
{
   static    struct    termio  termio_var;
   auto    FILE    *tty_stdin, *tty_stdout, *log_file;
   auto    int    error_flag, i;

   if((tty_stdin = fopen(TTY_STDIN_FILE_NAME, "rb")) ==
           (FILE *)NULL)
       exit(1);

   if((tty_stdout = fopen(TTY_STDOUT_FILE_NAME, "wb")) ==
           (FILE *)NULL)
       exit(2);

   if((log_file = fopen(LOG_FILE_NAME, "wb")) == (FILE *)NULL)
       exit(3);

   /* Length of tty_stdin & tty_stdout stream buffers to 1 */

   setbuf(tty_stdin, (char *)NULL);
   setbuf(tty_stdout, (char *) NULL);

   if(ioctl(fileno(tty_stdin), TCGETS, &termio_var))
       exit(4);

   /* Set up the terminal attributes */

    termio_var.c_iflag = IGNBRK | IGNPAR | IXON | IXOFF;
    termio_var.c_oflag = 0;
    termio_var.c_cflag = B4800 | CS8 | CREAD | HUPCL | CLOCAL;
    termio_var.c_lflag = 0;
    termio_var.c_line  = 0;
    termio_var.c_cc[VEOF] = sizeof( rx_block ); /* MIN  */
    termio_var.c_cc[VEOL] = 20;                 /* TIME */

   if(ioctl(fileno(tty_stdout), TCSETS, &termio_var))
       exit(5);
   else
   {
       for(i = 0; i < 100; i++)
       {
           if(fwrite(tx_block, sizeof(tx_block), 1, tty_stdout) != 1)
               fprintf(stderr,
                       "Can't write to I/O device on tty_stdout\n");

           if(fread(rx_block, sizeof(rx_block), 1, tty_stdin) == 1)
           {
               fprintf(stdout,
                  "Valid response from device received; Logging...");

           if(fwrite(rx_block,sizeof(rx_block), 1, log_file) == 1)
               fprintf(stdout, "Passed.\n");
           else
               fprintf(stdout, "Failed.\n");
                        }
                        else
               fprintf(stderr,
                   "No response from device; errno: %d\n", errno );
       }
       fclose(tty_stdin);
       fclose(tty_stdout);
       fclose(log_file);
   }
   return(0);
}

Figure 9

This figure shows the input and output queues maintained by a typical UNIX
character device driver. As characters are written by the process, they are
placed on the output queue and are eventually sent by the serial link
hardware. As characters are received by the device driver, they are placed
on the raw input queue and may be read directly, without modification, by
the process. The process can also elect to read from the canonical input
queue (the ICANON flag must be set). As characters are received and placed
in the raw queue, they are moved to the canonical input queue via the
selected line discipline. The line discipline acts as a filter; removing,
replacing, or adding characters on the input stream. A common replacement is
translating CR with CR-NL. If the canonical input queue is enabled, line
editing is possible.

Figure 10

#include   <stdio.h>
#include   <termio.h>  /* Installed with .h files */

#define    TTY_STDIN_FILE_NAME     "/dev/tty01"
#define    TTY_STDOUT_FILE_NAME    "/dev/tty01"

extern     int  errno;


main()
{
   static  struct  termio  termio_var;
   auto    FILE    *tty_stdin, *tty_stdout;
   auto    int     error_flag, i_var, rc;

   if((tty_stdin = fopen(TTY_STDIN_FILE_NAME, "rb")) == (FILE *)NULL)
       exit(1);

   if((tty_stdout = fopen(TTY_STDOUT_FILE_NAME, "wb")) ==
                          (FILE *) NULL)
       exit(2);

   setbuf(tty_stdin, (char *) NULL);
   setbuf(tty_stdout, (char *) NULL);

   if(ioctl(fileno(tty_stdin), TCGETS, &termio_var))
       exit(3);

   termio_var.c_iflag =    IGNBRK | IGNPAR | IXON |
               IXOFF | CLOCAL | ICRNL;
   termio_var.c_oflag =    OPOST | ONLCR | OCRNL | TAB3;
   termio_var.c_cflag =    B4800 | CS8 | CREAD | HUPCL | CLOCAL;
   termio_var.c_lflag =    ICANON | ISIG | ECHO | ECHOE;
   termio_var.c_cc[VINTR] = '\177';    /* DEL char         */
   termio_var.c_cc[VQUIT] = '\003';    /* ^^C char         */
   termio_var.c_cc[VERASE] = '\b';    /* backspace        */
   termio_var.c_cc[VKILL] = '@';        /* @ kills line     */
   termio_var.c_cc[VEOF] = '\004';    /* Designated EOF   */
   termio_var.c_cc[VEOL] = '\n';        /* Designated EOL   */

   if(ioctl(fileno(tty_stdout), TCSETS, &termio_var))
       exit(5);
   else
   {
       fprintf(tty_stdout, "\nEnter an Integer > ");

       while(1)
       {
           switch(rc = fscanf( tty_stdin, "%d", &i_var))
           {
               case EOF:
                   break;

               case 0:
           fprintf(stderr, "\nInvalid response\n\n");
          fflush(tty_stdin);
        fprintf(tty_stdout, "\nInvalid response\n\n");
        fprintf(tty_stdout, "\nEnter an Integer > ");
        break;

               case 1:
        fprintf(stdout, "From tty: %d \n\n", i_var);
        fprintf(tty_stdout, "\nEnter an Integer > ");
        break;

               default:
        fprintf(stderr, "fscanf() : %d ?\n", rc);
        break;
           }
       }
       fclose(tty_stdin);
       fclose(tty_stdout);
   }
   return(0);
}

Figure 11

 Under UNIX

$stty 9600 parenb -parodd cs7 ixon ixoff -clocal hupcl < /dev/tty01

 Under DOS

C>stty 9600 parenb -parodd cs7 ixon ixoff -clocal hupcl < \dev\tty01

Figure 12

 Under UNIX

$cat file_1 | PS_filter > /dev/tty01

 Under DOS

C>type file_1 | PS_filter > \dev\tty01


Simplifying Pointer Syntax for Clearer, More Accurate Programming

 Greg Comeau

Pointers containing the address of functions, variables, or objects  clarify
and simplify your code. They are tricky, though, and confusing if carelessly
written. The derivations of pointer syntax that are presented in this
article should make pointer usage perfectly clear to you. While these
derivations are simple, some of them may be new to you or may produce
results you would not have predicted. For example, the output of the program
in Figure 1 may not be obvious, although it is a common and basic C
construct.

An analysis of Figures 2a and 2b will give us a common frame of reference
for this article. In these figures, the value of c is 'a', and the value of
p when p = &c executes is 1000. The value 1000 is the address of c (randomly
chosen for this discussion), and the & signifies that we want to take the
address of the accompanying object operand c.

Therefore p = &c implies that the data contained in p is actually the
address of another variable, which also contains its own data. This seems
logical, since c and p each have their own storage space and can hold
independent data within that storage space. It is easy  to forget this when
your declarations and programs become more complex.

Pointer Uses

Basically, you can do three things with pointers: assign another value to
the pointer, assign the pointer's value to another pointer explicitly by
means of an assignment or implicitly as an argument to a function, or
dereference the pointer.

Assigning another value to the pointer is as simple as coding the assignment
(see Figure 3a). The memory map of the program as it executes will help you
understand exactly how this works (see Figure 3b). Notice how the
reassignment to p simply causes p to change values; this is the normal
functioning of a variable and applies even though we are dealing with a
pointer. The reassignment of a pointer, however, can have devastating
effects on some programs. This is especially true with heap space when free
subroutine calls are not issued between the assignments.

Using the pointer in an expression such as an rvalue shows once again that a
pointer is just another variable. In Figures 4a and 4b, the statement p = &c
assigns the address of c to p. Then the statement p2 = p assigns to p2 the
address of c by using the value contained in p. This does not imply that p2
points at p. Instead the value of p gets copied into p2. This is no
different from

int   i;
int   j;

i = 5;
j = i;

where we know that j will also contain its own copy of the value 5. To prove
this, run the code from Figure 4a to see what the output will be. Note that
it will not print out numbers, like 1000, used in the memory maps. Instead
the %p format specifier of printf will encode the output in segment:offset
format.

The last use of pointers involves the value that the pointer contains
through the unary * operator. The unary * operator is one way to accomplish
indirection and dereferencing of pointers. For the moment, let's take a look
at the last line of Figure 5a. As you can see from the memory map, the
result of this statement is to change the value of c from 'a'  to 'A.' Since
p2 becomes another name for c, we can code

*p2 = 'A';

instead of

c = 'A';

The indirection ability of pointers is very powerful. It allows us to access
named symbolic variables and unnamed variables (identifiers) with the same
ease--without requiring that you keep a lot of information about which
objects you are pointing to, where they are located, and so on. This
indirection can result in faster and smaller code (depending upon the
compiler and hardware in question).

Unary * Operator

Near the end of Figure 5a, c is a char object whose value is 'a' and p2 is a
pointer to a char object whose value is the address of c. *p2 represents
what p2 points to, not the contents of p2 as it may appear to read. For
example, using i and j from the previous example, we know that j = i says j
is equal to the contents of i. There is no reason to expect pointer notation
to function differently. For example, Figure 5a contains the statement

p2 = p;

which requests that the contents of p be placed into p2.

To explain further why *p2 does not mean "the contents of p2," it is worth
noting that when we say j = i, it actually means j = *&i. Read *&i as "a
pointer to the contents of the address of i." A memory map of Figure 5a is
shown in  Figure 5b.

Besides mapping the contents of what p2 points to, *p2 can represent the
contents of the address of p2. Since in this case the content of p2 is 1000,
this expression implicitly becomes  * char *1000--meaning the contents of
memory location 1000, taken to be a char, which is 'A'.

Derivations

Now that we've gotten some of the background out of the way, we can analyze
some concrete pointer examples. PTRINC.C (see Figure 6) shows a small group
of simple pointer operations. These pointer operations illustrate that there
is more going on behind the scenes than is apparent at first glance. The
following discussion is based on a debugging technique described in the
sidebar "In-Line C Program Debugging." For further information, refer to
"Pointers 101: Understanding and Using Pointers in the C Language," MSJ
(Vol. 4, No. 4).

The debug.h file explained in the sidebar is included in the first line of
PTRINC. Examining lines 9 and 10 you will find the declarations char *p and
char *origp, which are two pointers to char objects.

On line 12, p is assigned to the address of a string literal--a group of
characters next to each other in a source program, prefixed and suffixed by
the double-quote character. Internally, this kind of construct is a static
array of characters containing each character that is enclosed in the
quotes, including a terminating null byte which is added by the compiler.
This format is known as ASCIIZ.

A string literal is a good example of an unnamed area of storage. To use a
string literal, we must either set a pointer to its address as PTRINC has
done or use it as an argument to a function where its address becomes
apparent. Most of us have used this context in our first C program by
specifying a string literal as an argument to printf.

For a complete explanation of string literals, especially those involving
multibyte characters, refer to the ANSI C draft (the latest version,
published December 7, 1988, is available from the Computer and Business
Equipment Manufacturers Association in Washington, D.C.) or a relatively new
C text such as The C Programming Language, Second Edition, Kernighan and
Ritchie (Prentice-Hall Inc., 1988).

Line 13 of PTRINC prints the characters 'a' through k in our debug format.
Line 14 assigns origp to array--the same place that p is pointing to. For a
visual representation of the statements in lines 12 to 32, see Figure 7.

At line 17, the value printed for the contents of p is 1234, which is the
same as &array[0]. Note that line 17 does not print the address of p. If we
wanted that, we would have coded printp(&p) to produce an output of &p =
1200. The address of p is a given, in accordance with the top of Figure 7.
An arbitrary location for it has been chosen since it will not affect this
discussion (actual execution of the code will give another result).
Therefore, like any occurrence of text which only mentions p, printp(p)
produces the contents of p; origp = p functions similarly. The value printed
by the execution of line 17 should point at the beginning of the array since
we have not incremented or reassigned a value to p.

Line 18 increments p. As with all pointers, the increment value is
contingent upon the derived or base type of the pointer. In PTRINC, p is a
pointer to char, therefore the increment will be 1, the sizeof(char).

If PTRINC had been written so that p was a pointer to an integer (an int *),
p++ would have a scaling factor of sizeof(int), which typically evaluates to
2 or 4 bytes depending upon the compiler. In C, scaling factors allow
pointers to be manipulated without forcing the programmer to worry about
adjusting the given pointer by hand. The pointers, however, must be of the
proper type. This can cause a problem because the C language does not let
you create pointers to an object with variable size. The objects may be
created in the heap via the various alloc routines. This concept will be
covered in a future article. Note that such objects do not need to be a
linked list and that there are useful cases in which a method for obtaining
the object's length after the allocation is unnecessary (dynamic copies of
ASCIIZ strings, for example).

The output of line 19 reflects the increment of p by 1. Line 18 allowed p to
point at the b within the array, as expected.

Before continuing our examination of PTRINC, we need to discuss side
effects,  sequence points, expression statements, and the comma
operator--features of the C language that relate directly to line 20 of
PTRINC.

Side Effects

A side effect is a change in an object's value due to the evaluation of an
expression. Side effects happen as a result of function calls, assignment
expressions, auto-increment expressions, and auto-decrement expressions.
Unless an expression is void, it produces a value; in the process, it also
produces a side effect.

Side effects involving functions can modify the value of a variable in a way
that might not be clear in a large or complex program. For example, in

int    i;

void f(void)
{
     i++;
}

main()
{
    f();
}

it may appear that the call to f is innocent, but actually it modifies the
value of i. No real harm is done here, although the increment of i may not
be apparent in a larger program or in a program with many statements
occurring in f and main. If, however, we modify this code slightly by
introducing another function named g

int g(void)
{
    i++;
    f();

    return (5);
}

and add

j = g() + i;

to main, the resulting value of j may not be clear. Either way we are
dealing with a side effect.

Code such as this assignment statement

i = 5;

has the side effect of replacing the value of i with 5. As long as an
expression modifies an object, a side effect occurs. Therefore a side effect
can have ill effects.

The classic example of a side effect with an expression statement  is:

a[i] = i++;

In this situation, the compiler has the choice of computing the value of the
subscript before or after it computes the value on the right-hand side of
the equal sign. We would expect this unpredictability to be incorrect, but
the compiler nevertheless makes this decision on its own.

Three more examples are represented by the code:



i = 5;
func(i++, i++);

In the first scenario, the compiler chooses to generate code to perform the
post increment after both i's are evaluated. Both arguments will be 5; the
value of i after the expression can be either 6 or 7.

The i's are post-incremented in the second scenario in the order shown; the
first argument will be 5 and the second one will be 6. The value of i after
the expression will be 7.

The i's are post-incremented in the last scenario in reverse of the order
shown; the compiler is not constrained to evaluate the arguments of a
function in left-to-right order. The second argument will be 5 and the first
one will be 6. The value of i after the expression will be 7.

Generally speaking, the third scenario will usually be true, since most
compilers put the arguments to functions on the stack in reverse order.
This, however, does not necessarily imply that the arguments are evaluated
as they are put on the stack. All side effects of function arguments are
evaluated before calling the function, which makes them consistent and does
not conflict with any of the scenarios given above.

As tricky as these examples are, we can accept them after giving them some
thought. In fact, these examples appear much more sensible than the
statement:

j = i++ + i++;

I will leave this one as an exercise. The possibilities it presents will be
similar to those discussed above.

Expression Statements

Most C programmers do not realize that C is more expression-based than any
of the other popular general-purpose languages. For instance, even though we
typically refer to a statement such as

i = 5;

as an assignment statement, it is actually an assignment expression. A
statement in C can be one of the standard flow control statements such as
for, goto, and break. In the Microsoft C Version 5.1 Optimizing Compiler
manual, the syntax summary on page 229 in Appendix B of the  User's Guide
Language Reference manual informs us that a statement may be of the form:

expression;

The assignment listed above is actually an expression statement containing
an assignment.

We've already examined several situations in PTRINC that prove that this
example is an assignment expression statement. For instance, line 18 of
PTRINC contains p++. Even though this resembles p = p + 1, the syntax is
clearly a construct that allows it to stand alone. Even p = p + 1 results in
a value--this statement not only assigns p + 1 to p, it also returns p. The
return value of the expression is often ignored; this is how statements such
as j = i = 5 can occur without wreaking havoc.

Sequence Points

A sequence point invokes the concept of a hypothetical C machine. In such a
machine, the compiler's code generator must completely follow your C code. A
more precise definition is shown in Figure 8. Every statement in this C
machine must be executed in the order in which it has been coded: statement
order followed by flow of control order. This means that the hypothetical C
machine allows few if any optimizations to take place.

This C machine exists only in theory; in practice no compiler will follow
it. The comma operator and the volatile keyword (which many compilers still
do not support) serve the same function as the hypothetical C machine. The
comma operator is explained in the next section. The volatile keyword was
explained in "A Guide to Understanding Even the Most Complex C
Declarations," MSJ (Vol. 3, No. 5). Their purpose is to ensure that
agreement points exist at appropriate places.

Comma Operator

As with all sequence point operators, the comma operator ensures that its
operand expressions are evaluated in left-to-right order. The result of the
comma operator is always the value of the last expression in the expression
list. Generally, the comma operator cannot force the evaluation of a given
statement sequence, such as a for or block compound statement. The comma
operation is only valid within expressions--it does not control statements,
though it may be used within the controlling expression of a conditional
statement.

The comma operator is different from a semicolon; it occurs in expressions,
while the semicolon terminates statements. Also, note that the comma
operator has nothing to do with the comma that is used to separate function
arguments. In the latter case, the comma serves strictly as punctuation.

PTRINC.C

We've just discussed many features of C; I'll now explain how they pertain
to line 20 of PTRINC. Line 20 of PTRINC contains the statement *p++. To
determine what this means,  look at the operator precedence table (see
Figure 7 for the output of line 20). A precedence table can be found on page
137 of the Microsoft C 5.1 Optimizing Compiler Language Reference.

The precedence table reveals whether line 20 increments the contents of p,
increments p, changes the address of p, increments the contents of the
object p points to, or performs some combination of these.

Some of these cases are obvious. The first two cases are the same for the
reasons stated earlier. Case c cannot be true, since we do not have the
ability to change the address of a variable, especially when that variable
is an identifier.

We are left with the following possibilities: line 20 increments the
contents of p, increments the contents of the object p points to, or
performs some combination of the above.

We know that the associativity of the ++ operator from the operator
precedence table chart means "do not perform the increment until after the
(simple) expression has been evaluated" because it is a post-increment
operator. Since the ++ immediately follows p, it's p that's being
incremented, not the contents of the object p points to.

The simple expression referred to in the preceding paragraph is *p. However,
like the case presented in the previous sections, we don't assign or use the
value of *p. It is discarded--*p goes nowhere and then p is incremented
afterwards. This is equivalent to having coded the following:

*p, p++;

This syntax is a valid C expression and a valid C statement. It demonstrates
that we wouldn't normally want to write code this way. For all practical
purposes, line 20 increments only  p; the implication is that it wasn't
supposed to be written that way. We should be cautious of such situations,
because it's too easy to code a construct such as *p++ without stopping for
a second to see what is actually happening, especially since the compiler is
not going to object.

Sometimes, however, it makes sense to write code that uses the value of this
kind of expression. For example, the assignment to c in the following code
is legitimate and powerful:

char    *p;
char    c;

p = ...;
c = *p++;
...;

Statements like the assignment to c present a style issue because such
coding practices can create hard-to-read code. Instead you could write the
assignment on two lines:



c = *p;
p++;

Such constructs, however, are idioms of C, so whether or not you should use
them is simply a matter of style.

Next Case

In line 22, (*p)++ might increment the contents of p or increment the
contents of the object to which p points.

Since the ++ is outside the parentheses, it is doubtful that they modify p.
Of course, (*p)++ is a legal expression that modifies the contents of p and
++ occurs outside the parentheses in that case. Also, if we envision a case
such as

i++;

being at some point equivalent to

(i) = (i) + 1;

line 22 could be interpreted as:

(*p) = (*p) + 1;

Therefore line 22 increments the contents of the object to which p points.
Case c wasn't worth considering since there is only one ++ operator in the
statement. Figure 7 shows that p has not changed when we print it on line
23. What has changed is that we've added 1 to c; under the ASCII character
set this changes the c to a d. Line 22 could also have been written as
either of the following:

++(*p)
++*p

since a standalone statement such as i++ could be rewritten as ++i. If the
result of these statements were to be used, however, this would not be true
since it makes a difference where ++ is placed, according to the precedence
table.  For example, the expression ++(*p) is evaluated and then
incremented, but the expression  ++*p is incremented first and then
evaluated.

It may not be clear whether the ++ on line 24 applies to the * or the p. It
can't apply to the * without the introduction of some other operand, like
the ++*p above, which is mentioned as a syntax level reference since the two
statements have different meanings. As with all these examples, the operator
precedence table provides the key. Since the ++ binds with the p and is a
pre-increment, the compiler will not let the dereference occur without first
performing the increment. Therefore, line 24 could have been coded



p++, *p;

or:

++p, *p;

Much of what is true about line 20 is also true of this statement. The
effect of 24 is that p is incremented. Line 25 confirms this. On line 26, we
simply check to make sure we know where we are.

Turmoil strikes again on line 27 and this time we have two sets of problems.
First we are going to use the value of the expression instead of ignoring it
as we did in earlier statements. We are left with:

*(p++)

Clearly, p will be incremented, but what's not clear is when this will
happen. At first glance, we would say that it's the same as

p++, *p

which is the same thing line 24 mapped into because expressions in
parentheses must be performed first. We'd be wrong. The statement actually
maps to

*p, p++

which is the same as line 20. The explanation of this is that p++
constitutes a complete subexpression involving a long value no different
from what

while ((c = getchar()) != EOF)

does in its assignments to c. In this case, the subexpression has a side
effect involving a post operator. We know  that the side effect will occur
sometime during that statement and before the next statement in this
situation. The operator precedence table clarifies the situation by showing
that the post ++ operator and the unary * operator are always
right-associative. This means that if we look at the reverse situation of
*p++, it must be interpreted as *(p++) rather than (*p)++. Therefore, the
parentheses on line 27 only order the p, not the p++, since the increment is
performed afterwards.

Line 27 has a second problem. We can see that when line 27 is output and fed
from the C preprocessor into the C parser it looks something like this:

fprintf((&_iob[2]), "*(p++)=%d/%c\n", *(p++), *(p++));

(This code should all be on one line; it is broken here due to space
considerations--Ed.)

Notice that we have ended up with two *(p++) expressions--a statement
containing a macro with a side effect. This is a very subtle problem. The
decimal value of the character printed by the first *(p++) in this example
may actually be different from the character value we expected  because we
still do not know when the p++'s will occur. On my UNIX machine, the p's
were incremented in the reverse of the  order shown, probably because of the
way the arguments on the stack were put. The value for the d is correct, but
the decimal value 101 represents an e (see Figure 7).

Line 28 verifies that there were two increments on my machine. There might
not have been since we cannot predict whether both increments start using
the same value of p or whether one is based on the other. Lines 29, 31, and
32 are self-explanatory except for the third character of the array, which
is now a d rather than a c. This change occurred in line 22.

Summary

This article has introduced and explained many pointer derivations and shown
some common and often unexpected  side effects of using pointers in your
code. A solid understanding of how pointers work, and a willingness to
examine even simple pointer constructions for hidden errors, will help you
to write concise, fast code.

Figure 1

#include <stdio.h>

main()
{
    char    *p = "abcdefg";

    printf("%c\n", *(p++));
}



Figure 3a

main()
{
    char    c = 'a';
    char    d = 'z';
    /* .... */
    char    *p;

    /* .... */
    p = &c;
    p = &d;
    /* .... */
}



Figure 4

main()
{
    char    c = 'a';
    char    d = 'z';
    /* .... */
    char    *p;
    char    *p2;

    /* .... */
    p = &c;
    p2 = p;
    p = &d;
    /* .... */
    printf("%p\n", p);
    printf("%p\n", p2);
}



Figure 5A:

Lmain()
{
    char    c = 'a';
    char    d = 'z';
    /* .... */
    char    *p;
    char    *p2;

    /* .... */
    p = &c;
    p2 = p;
    p = &d;
    /* .... */
    printf("%c\n", *p);
    *p2 = 'A';
}



Figure 6

1   #include "debug.h"
    2
    3   /*
    4    * ptrinc.c
    5    */
    6
    7   main()
    8   {
    9       char    *p;
   10       char    *origp;
   11
   12       p = "abcdefghijk";
   13       prints(p);
   14       origp = p;
   15       prints(origp);
   16
   17       printp(p);
   18       p++;
   19       printp(p);
   20       *p++;
   21       printp(p);
   22       (*p)++;
   23       printp(p);
   24       *++p;
   25       printp(p);
   26       printc(*p);
   27       printc(*(p++));
   28       printc(*p);
   29       printp(p);
   30
   31       prints(p);
   32       prints(origp);
   33   }



Figure 7:

Assume that p and origp are pointers each 4 bytes in length located at
addresses 1200 and 1210, respectively.

Line No.  Address ---->

|         1   1   1   1   1   1   1   1   1   1   1   1

|         2   2   2   2   2   2   2   2   2   2   2   2

V         3   3   3   3   3   3   4   4   4   4   4   4

          4   5   6   7   8   9   0   1   2   3   4   5   output

12       'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' '\0' --> elements of
array



          |

          p points at the beginning of array

13                                                        p="abcdefghijk"

          1   1   1   1   1   1   1   1   1   1   1   1

          2   2   2   2   2   2   2   2   2   2   2   2

          3   3   3   3   3   3   4   4   4   4   4   4

          4   5   6   7   8   9   0   1   2   3   4   5

14       'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' '\0'





          p points at the beginning of array





          origp points at the beginning of array

15

       origp="abcdefghijk"

17                                                        p=1234

          1   1   1   1   1   1   1   1   1   1   1   1

          2   2   2   2   2   2   2   2   2   2   2   2

          3   3   3   3   3   3   4   4   4   4   4   4

          4   5   6   7   8   9   0   1   2   3   4   5

18       'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' '\0'

          A   A

          |   |

       origp  p

19                                                        p=1235

          1   1   1   1   1   1   1   1   1   1   1   1

          2   2   2   2   2   2   2   2   2   2   2   2

          3   3   3   3   3   3   4   4   4   4   4   4

          4   5   6   7   8   9   0   1   2   3   4   5

20       'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' '\0'

          A       A

          |       |

       origp      p

21                                                        p=1236

          1   1   1   1   1   1   1   1   1   1   1   1

          2   2   2   2   2   2   2   2   2   2   2   2

          3   3   3   3   3   3   4   4   4   4   4   4

          4   5   6   7   8   9   0   1   2   3   4   5

22       'a' 'b' 'd' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' '\0'

          A       A

          |       |

       origp      p

23                                                        p=1236

          1   1   1   1   1   1   1   1   1   1   1   1

          2   2   2   2   2   2   2   2   2   2   2   2

          3   3   3   3   3   3   4   4   4   4   4   4

          4   5   6   7   8   9   0   1   2   3   4   5

24       'a' 'b' 'd' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' '\0'

          A           A

          |           |

       origp          p

25                                                        p=1237

26                                                        *p=100/d

          1   1   1   1   1   1   1   1   1   1   1   1

          2   2   2   2   2   2   2   2   2   2   2   2

          3   3   3   3   3   3   4   4   4   4   4   4

          4   5   6   7   8   9   0   1   2   3   4   5

27       'a' 'b' 'd' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' '\0'

          A               A

          |               |

       origp              p

                           or

         'a' 'b' 'd' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' '\0'

          A                   A

          |                   |

       origp                  p

27                                                       *(p++)=101/d

28                                                       *p=102/f

29                                                       p=1239

31                                                       p="fghijk"

32                                                       origp="abddefghijk"

Figure 8

Sequence Point The point at which all side effects of previous evaluations
shall be complete and no side effects of subsequent evaluations shall have
taken place.

Agreement Point The sequence point for some object or class of objects at
which the value of the object(s) in the real implementation must agree with
the value prescribed by the abstract machine.

The sequence points of a C program are:

■    at the call to a function after the arguments to the function have been
evaluated

■    at the end of the first operand of the following operators:

    logical and: &&

    logical or: ||

    conditional: ?

    comma: ,

■    at the end of a full expression such as:

    an initializer

    the expression in an expression statement

    the controlling expression of a selection statement (if or switch)

    the controlling expression of an iteration statement (while, do, or for)

    the expression in a return statement.


In-Line C Program Debugging

DEBUG.H is a header file that performs in-line program debugging. An example
of DEBUG.H (see Figure A) is shown in TESTDEBUG.C (see Figure B).
TESTDEBUG.C includes DEBUG.H in a source file and uses it and three macro
variables to control the output. One macro controls the occurrence of any
debug output, another controls the printing of subroutine entry and exit
points, and the third controls the printing of subroutine names with the
respective debugging information.

The output shown for TESTDEBUG.C demonstrates the advantage of using
DEBUG.H, which is that the argument to the print* and DEBUG* macros allows
the code to be less tedious and more easily read (assuming the debug
information is to be left in). In this program, a statement such as

printd(somevariable)

produces

somevariable = the value of
             somevariable

as its output. This avoids having to code fprintf(stderr, "i=%d\n", i); or a
similar statement whenever you want to see what value a variable holds. For
instance, line 16 in TESTDEBUG.C produces i = 5. The expanded form can
become a nuisance, especially with more complicated variables such as a
member of an element of an array of structures. As in DEBUG.H, there are
five categories of output statements controlled by the last character of the
printx statement. (You can add more.) Use s to print the value of a string,
d to print an int or short, ld to print a long, p to print a pointer, and c
to print a character.

Also note that DEBUG.H contains a group of macros, the printx macros, which
you can use to print unconditionally. This is in contrast to the DEBUGx
macros which have an effect on your code only if you define DEBUGL1 before
you include DEBUG.H. Therefore, TESTDEBUG.C will always print the value of
i, but whether it prints any of the other output, including the subroutine
trace, depends upon DEBUGL1 being an active macro.

Space does not permit us to elaborate upon the internal workings of DEBUG.H.
Remember that for the purposes of this article, print?(var); allows you to
produce var = value. TESTDEBUG.C should be sufficient to demonstrate the
interface to DEBUG.H.

Figure A:

/* debug.h  */

#ifndef DEBUG_H
#define DEBUG_H

/* Depending upon which compiler you have, STRINGIZING should be set to
determine if #v syntax is allowed. For some compilers, this setting may not
be obvious since __STDC__ may not be supported or may be set to an
inappropriate value. */

#if __STDC__ == 1
#define STRINGIZING 0
#else

#define STRINGIZING 1 /* 1 means compiler can use #v syntax. You may need
to swap this line with the other one */

#endif

#include <stdio.h>

#ifndef SUBTRACE
#define SUBSTART(x)
#define SUBEND(x)
#else
#if STRINGIZING == 1
#define SUBSTART(func) fprintf(stderr, "%s()ing\n", #func)
#define SUBEND(func) fprintf(stderr, "%s()ed\n", #func)
#else
#define SUBSTART(func) fprintf(stderr, "%s()ing\n", "func")
#define SUBEND(func) fprintf(stderr, "%s()ed\n", "func")
#endif
#endif

#if STRINGIZING == 1
#define prints(str) fprintf(stderr, #str "='%s'\n", str)
#define printd(dec) fprintf(stderr, #dec "=%d\n", dec)
#define printld(ldec) fprintf(stderr, #ldec "=%ld\n", (long)ldec)
#define printp(p) fprintf(stderr, #p "=%p\n", p)
#define printc(ch) fprintf(stderr, #ch "=%d/%c\n", ch, ch)
/* printc without a side effect is properly written as:
   #define printc(ch) {char c = (ch);fprintf(stderr, #ch "=%d/%c\n",
   c, c); } */
#else
#define prints(str) fprintf(stderr, "str='%s'\n", str)
#define printd(dec) fprintf(stderr, "dec=%d\n", dec)
#define printld(ldec) fprintf(stderr, "ldec=%ld\n", (long)ldec)
#define printp(p) fprintf(stderr, "p=%p\n", p)
#define printc(ch) fprintf(stderr, "ch=%d/%c\n", ch, ch)
/* printc without a side effect is properly written as:
   #define printc(ch) {char c = (ch);fprintf(stderr, "ch=%d/%c\n",
   c, c); }*/
#endif

#define PRINTSUBfprintf(stderr, "%s:", __FILE__)

#ifndef DEBUGL1
#define DEBUG1(a1)
#define DEBUG2(a1, a2)
#define DEBUG3(a1, a2, a3)
#define DEBUG4(a1, a2, a3, a4)
#define DEBUG5(a1, a2, a3, a4, a5)
#define DEBUG6(a1, a2, a3, a4, a5, a6)
#define DEBUGS(str)
#define DEBUGD(d)
#define DEBUGLD(ld)
#define DEBUGC(c)
#define DEBUGP(p)
#define DEBUGCALL(func)
#else
#ifdef DEBUGF
#define DEBUG1(a1) PRINTSUB, fprintf(stderr,"%s",a1)
#define DEBUG2(a1,a2) PRINTSUB, fprintf(stderr,a1,a2)
#define DEBUG3(a1,a2,a3) PRINTSUB, fprintf(stderr,a1,a2,a3)
#define DEBUG4(a1,a2,a3,a4) PRINTSUB, fprintf(stderr,a1,a2,a3,a4)
#define DEBUG5(a1,a2,a3,a4,a5) PRINTSUB, fprintf(stderr,a1,a2,a3,a4,a5)

#define DEBUG6(a1,a2,a3,a4,a5,a6) PRINTSUB,
fprintf(stderr,a1,a2,a3,a4,a5,a6)

#define DEBUGS(str)PRINTSUB, prints(str)
#define DEBUGD(dec)PRINTSUB, printd(dec)
#define DEBUGLD(ld)PRINTSUB, printld(ldec)
#define DEBUGC(c)PRINTSUB, printc(c)
#define DEBUGP(p)PRINTSUB, printp(p)
#define DEBUGCALL(func) PRINTSUB, func
#else
#define DEBUG1(a1) fprintf(stderr, "%s", a1)
#define DEBUG2(a1, a2) fprintf(stderr, a1, a2)
#define DEBUG3(a1, a2, a3) fprintf(stderr, a1, a2, a3)
#define DEBUG4(a1, a2, a3, a4) fprintf(stderr, a1, a2, a3, a4)
#define DEBUG5(a1, a2, a3, a4, a5) fprintf(stderr, a1, a2, a3, a4, a5)
#define DEBUG6(a1, a2, a3, a4, a5, a6) \
               fprintf(stderr, a1, a2, a3, a4, a5, a6)
#define DEBUGS(str)prints(str)
#define DEBUGD(dec)printd(dec)
#define DEBUGLD(ld)printld(ldec)
#define DEBUGC(c)printc(c);
#define DEBUGP(p)printp(p)
#define DEBUGCALL(func) func
#endif
#endif

#endif



Figure B:

#define DEBUGL1
#define DEBUGF
#define SUBTRACE
#include "debug.h"

/* TESTDEBUG.C */

main()
{
int i = 5;
int *pi = &i;
char *s = "string";

SUBSTART(main);

printd(i);
DEBUGD(i);
DEBUGS(s);
DEBUGP(&i);
DEBUGP(pi);

SUBEND(main);
}

When executed, this program prints:

main()ing
i = 5
testdebug.c:i = 5
testdebug.c:s = 'string'
testdebug.c:&i = 0005:0FB8
testdebug.c:pi = 0005:0FB8
main()ed




Integrating Subsystems and Interprocess Communication  in an OS/2
Application

 Richard Hale Shaw

Programming in the OS/2 systems is always a challenging and interesting
experience. One reason is that the OS/2 environment provides a platform for
integrated application development that is richer than its rivals and
outperforms them. Another is that the OS/2 application programming interface
(API) is a robust set of functions that makes even the most complicated
applications much easier to design, develop, test, and complete.

The intent of this series of OS/2 articles (MSJ Vol. 4, Nos. 1 through 6)
has been twofold. First, its purpose was to introduce you to OS/2
programming, showing you how to write multithreaded applications, use the
Vio, Kbd, and Mou subsystems, and allow multiple OS/2 applications to
communicate via interprocess communication (IPC). This final article
presents an application that integrates all of those aims. Second, the
series intended to give you the OS/2 kernel programming knowledge needed to
program for OS/2 Presentation Manager (hereafter PM). Good PM programming
requires a thorough understanding of three elements: the OS/2 multithreaded
application programming environment and application programming interface
(API); PM windowing facilities (and their associated API functions); and PM
event-driven, message-based architecture (a superset of the architecture of
the Microsoft Windows environment).

The first five articles in this series have provided an overview of the OS/2
API. This final article discusses how to use OS/2 IPC to implement an
event-driven, message-based queue that you can use to construct applications
whose architecture is similar to the architecture of PM.

Directory Information

The programs in this article prepare you for PM programming. They assume
that you are familiar with how to work with multiple threads, the various
subsystems, and IPC. The programs integrate many (if not all) of the
concepts presented in the previous articles in the series. They are also
practical; that is, they are either useful in their own right or can be
easily modified to fill a particular need.

The directory information (DI) application presented in this article
consists of three programs built on a client-server architecture. One, a
directory server, gives disk and directory information to any client program
that requests it, provided the client knows how to initiate the request and
use the results. Each of the client applications uses a set of function
calls to generate and process a request for directory information. The
function calls, combined with the server, are a high-level interface to the
OS/2 DosFindFirst function; this interface is simple and easy to use, it
hides the complexities of finding files and managing memory, and it avoids
having to make multiple calls to the DosFindNext function. The server
program can also be modified to run on a network, where it could be enabled
to display information about directories that a program running on a node
might not be able to access. And, although I didn't take this step, the
client functions can be placed in an OS/2 dynamic-link library (DLL), where
they can be used by more than one program at a time even though they are
only loaded once by OS/21.

Two kinds of client programs are presented here. One is a simple
command-line directory utility that I ported to OS/2 and modified to use the
directory information functions. The other is a  directory program for end
users that employs multiple threads to manage input and output and lets the
user work with the keyboard and mouse.

Client-Server Architecture

The DI functions implement a classic client-server relationship. If, for
example, the client asks "C:\OS2\*.* ", the server will respond with a list
of all the files that meet that specification. The client can request the
expansion of multiple filespecs at a time. A filespec may consist of a full
or partial path and a standard MS-DOS operating system file specification,
including wildcard characters. If a path is not included, the client's
current directory is assumed; each filespec can have a different path. Each
filespec can also have a unique matching attribute value. The server will
return the results in a form that makes it easy for other DI functions to
sort them, retrieve them, and make use of the information.

The DI client-server architecture is implemented using OS/2 interprocess
communication. When you start the DI server, it opens a queue that is known
to the DI functions bound into a client program. A client DI function can
pass requests for directory information to the server via this queue, and
the server will create a thread to service the request. The server can
manage up to 20 threads at a time, making it easy for it to handle multiple
demands for directory information. Each server thread receives a request
packet that is sent via the queue from the client. (The queue actually
carries a pointer to a shared memory block that contains the request.) This
request packet includes all the information necessary for the server to
process the request, as well as a pointer to a work area segment (also in
shared memory) where the server thread must place the results. When the
server thread has finished its task, it clears a semaphore in the request
header, which signals the client that the thread's task is complete. Then
the client can use other DI functions to retrieve the results of the
request.

With DI functions, a client program can prepare a request by making one or
more calls to the DiMakeRequest function. When the request has been
prepared, the client can send it to the server using the DiSendRequest
function, which will return when the request is complete. (You could put the
call to this function in a separate thread, so that the application can
continue while the server fulfills the request.) When the thread calling
DiSendRequest returns, the client can retrieve the request information by
calling several different functions that are described later in the article.
Finally, the request information should be released with DiDestroyRequest.
An example of the code required to write such a client program is shown in
the file DISIMPLE.C (see Figure 1).

DI Data Structures

DI functions use the two data structures shown in Figures 2 and 3.  (Full
source code for the figures in this article can be downloaded from any MSJ
bulletin board--Ed.) Figure 2, REQUESTHEADER, is the primary structure and
contains the information the server will need to process and complete the
request. This information is stored in a segment that is known as the
request header segment. This segment is allocated by the first call to
DiMakeRequest (which also allocates the work segment). The information in
the REQUESTHEADER structure includes the following:

■    the client's selector to the work segment

■    the server's selector to the request header segment and a server
selector and pointer to the work segment

■    the handle of the server's queue and the server's process ID (PID)

■    the total size of the header and work segments

■    the number of filespecs that make up the request

■    the number of results found by the server

■    a RAM semaphore that blocks the client thread calling DiSendRequest
until the semaphore is cleared by the server

■    other pointers used by either the client or server

The header also includes an array of one or more structures of type
DIRINFORESULT, one for every filespec contained in the request (see Figure
3). One of these structures is automatically included in a header segment
when the segment is allocated by DiMakeRequest. An application can
subsequently call DiMakeRequest for each filespec to be included in the
request. When  a new filespec is included, DiMakeRequest resizes the header
segment  (using DosReallocSeg) to include space for an additional
DIRINFORESULT structure.

Each DIRINFORESULT structure contains the following information about a
particular filespec:

■    the filespec for which to search

■    the path to the filespec

■    the attributes to use when searching for files

■    a pointer to the first file that the server finds matching the filespec

■    the number of files found by the server for the filespec

■    a pointer used by the functions that retrieve the results from the work
segment

Creating a DI Request

An application program should call DiMakeRequest to create a directory
information request. The DiMakeRequest function is part of DI.C (see Figure
4). DiMakeRequest requires the address of a variable that will be a handle
to the request, the complete path and filespec that the server will expand,
and the attributes that the server should use when expanding the filespec.
The handle is of type PVOID and should be initialized to NULL before the
first call to DiMakeRequest, so that the function will create a new request.
Otherwise, the function will assume that the handle refers to an existing
request. You can add filespecs to the request by repeated calls to
DiMakeRequest with the same handle before you call DiSendRequest.

When you are ready to send a request to the server, your application should
call DiSendRequest. Remember that this call will block until the server has
finished processing the request, so you should place this call in a separate
thread if you need to avoid blocking the main thread of a client
application. You should either call DiDestroyRequest before reusing the
handle or use a different handle for subsequent requests.

DiMakeRequest assumes that the server is running and has created its queue;
otherwise, the function will wait until the server's queue has been created.
Once the server has created its queue, DiMakeRequest will open it using
DosOpenQueue, which returns a queue handle and the PID of the server.
DiMakeRequest will use DosGiveSeg and the server's PID to enable the work
and header segments to be shared with the server. DiMakeRequest will add
each filespec to the request, expand it to include a full path, and create a
new DIRINFORESULT structure for each filespec. The details of this process
and how the DI functions work with the DosFindFirst OS/2 function are
discussed in the sidebar "DosFindFirst and DI Memory Management."

Once an application has finished adding filespecs to a request, it can then
use DiSendRequest to send the request to the server (see Figure 2). The
DiSendRequest function does some housekeeping, writes the request to the
queue, then waits until the server has completed its task and clears the
header semaphore.

DISERVER Program

The DISERVER program handles requests for directory information from client
processes that pass these requests to the server by using its queue.
DISERVER is also a thread server--it creates a new thread to process every
request it receives. DISERVER.C, which contains the entire server program,
consists of two functions: main, which waits for requests and then starts a
new thread to service them, and server_thread, which contains the code that
services the request.

The main thread of DISERVER is fairly simple. It creates its queue and
blocks on a call to DosReadQueue to wait for requests from client processes.
Once it receives a request, it finds an available thread and creates a
specific thread to service the request. It uses an array of SERVERTHREAD
structures that contains a semaphore, a thread ID, a request pointer, a
thread number, and a stack for each thread. To find an available server
thread, the main thread simply looks for an unused structure (identified by
a thread ID set to zero) in this array. Then it sets the thread's semaphore
and creates the new thread. The thread will immediately block on the
semaphore, allowing the main thread to set the thread's ID, request pointer,
and number. Then the main thread clears the thread's semaphore and returns
to the DosReadQueue call to wait for the next request.

There is no need for DISERVER to provide any visual output in routine use,
so you can run it in a PM window, a full-screen session, or in a background
screen group. I occasionally found it useful (and fascinating) to watch its
progress, however, so I left a number of messages embedded in the code.
These will print if you start DISERVER with the /v (verbose) parameter.

Server Threads

Each thread started by the server will block on its semaphore until the
semaphore is cleared by the server's main thread. Then the server thread
will enter a loop, calling DosFindFirst for each filespec in the request.
The server thread does not have to call DosFindNext, since the thread
provides a large buffer in which DosFindFirst can place its results.
DosFindFirst creates a FILEFINDBUF structure for each filename found; these
structures are placed in a buffer area that is specified when the function
is called.

If DosFindFirst indicates that no files are found or that the path doesn't
exist, the returned error value is placed in the DIRINFORESULT structure for
that filespec. If only one file is found and the file's attribute byte has
its directory bit set, the thread will try the call to DosFindFirst again,
but this time with " \*.* " appended to the filename, allowing it to expand
to a single matching directory. Otherwise, DosFindFirst will place the
resulting filenames in the work segment area. A pointer to this area and a
variable indicating the remaining space are adjusted with each pass through
the loop, so that subsequent calls to DosFindFirst will not overwrite
information from previous calls.

If a call to DosFindFirst is successful, the server thread will set the
fields of that filespec's DIRINFORESULT structure for the first file found
and the number of files found. The server thread will also adjust the total
number of files found in the request header. Then the thread enters a loop
to adjust the buffer pointer past the last file found (if verbose mode is
on, it will also print each filename as it passes through the loop), sleep
for one second, and return to the top of the loop in order to process the
next filespec.

Once all filespecs have been processed, the thread will use pointer
arithmetic to calculate the total space used in the work segment. Then it
frees the work segment, clears the header semaphore to notify the client
that it is finished with the request, and frees the header segment. The
server thread then sets its own thread ID to zero, thus notifying the main
thread that it is terminating (and allowing the main thread to reassign the
server thread's data area to a new thread). Finally, it will call DosExit to
terminate itself.

Return to the Client

When the server clears the request header semaphore, the client thread that
called DiSendRequest and blocked on the semaphore can proceed. The thread
using DiSendRequest allocates a new work segment, copies the data and
adjusts the pointers, and frees the old work segment. Then it returns to the
calling thread.

At this point, the client can retrieve the request results. Even though the
client has only made a call to DiMakeRequest for each  filespec and a single
call to DiSendRequest, the results are in; the server has actually done all
the real work.

The client can use several functions to access the results of a request. In
DISIMPLE.C, a call is made to DiGetResultHdl to get a handle to the results
of filespec zero, since this was the first (and only) filespec in the
request. An application can  call DiGetNumResults, however,  to find out how
many filespecs were placed in a request (and, incidentally, how many
filenames were found) and then call DiGetResultHdl for each of the
filespecs. Once it has obtained a handle to a result, an application can
either call DiGetFirstResult and DiGetNextResult to retrieve the name of
each file found, or DiGetFirstResultPtr and DiGetNextResultPtr to retrieve a
pointer to the FILEFINDBUF structure returned by OS/2 for that filename.
Although the FILEFINDBUF structures are not in an array, a client can call
DiBuildResultTbl to create an array of pointers to the results of a request.
The use of DiBuildResultTbl will be demonstrated in one of the client
programs discussed below.

An application should call DiDestroyRequest when it has finished accessing
the results of a DiSendResult call. DiDestroyRequest will call DosFreeSeg to
free the work and header segments and permit  OS/2 to discard the segments.
DiDestroyRequest will  reset the request handle to NULL, too.

The next part of this article is a discussion of two client
applications--the LS utility and the DIPOP program.

LS Utility

The LS utility has an unusual history. I first wrote LS as an add-on utility
for a UNIX-like DOS2 shell. When I began programming for OS/2, one of the
first assignments I gave myself was to port my favorite tools from DOS to
OS/2. I had always found the UNIX LS program more useful and informative
than the DOS DIR command, so my DOS-based imitation of the LS program was
one of the tools I ported to OS/2.

Once the DI routines were complete, I found it a trivial task to modify LS
to use them--and found myself cutting away a great deal of the existing
code, now that the complexities of DosFindFirst and DosFindNext were absent.
LS not only illustrates how easy it is to incorporate and use the DI
routines in an application, it also demonstrates the portability of C; after
all, the program imitates a UNIX utility, was written for DOS, and was
ported to OS/2.

LS, like its UNIX counterpart, takes a series of command-line filespecs,
expands them, and prints the results of the expansion. One or more options
can be specified that control the output of the program (sorting, printing
in columns, paging, filtering out certain types of files, and so on).

LS is only slightly more complex than the DISIMPLE program. It uses the
read_options function to read any command-line arguments, sets the options,
and removes the options from the command line. Then it enters a loop and
calls DiMakeRequest for each of the command-line filespecs. Finally, it
calls DiSendRequest to issue the request to the server.

Upon returning from DiSendRequest, LS calculates the remaining space on the
drive on which the first filespec is found. To do this, it calls
DiGetResultHdl to get a handle to the first filespec and DiGetResultDir to
access the full path to that filespec, which is another convenient use of
the DI functions.

In order to access, sort, and print the results, LS calls DiBuildResultTbl
to build a table of the results. This function will create an array of
pointers to each of the FILEFINDBUF structures placed in the work segment by
the server. DiBuildResultTbl returns a pointer to the results table that the
calling thread can use to access these structures. LS passes the table to
the C library qsort routine, which sorts the pointers in preparation for
printing the file information. (The details of the sort comparisons are
contained in the function qscmp, which can sort the pointers by the
date-time stamp of the file or the filename.)

LS calls the print_entries function in order to access the table and print
the filenames. Print_entries can display the output in a long listing (the
default), with name, date, time, size, and attributes, or as filenames only,
in a single column or multicolumn listing. It can also wait for a keystroke
at the end of each screenful of information and will automatically detect
the screen size of a window. When it has finished displaying the results, LS
calls DiDestroyResultTbl in order to free the table and DiDestroyRequest to
free the header and work segments.

DIPOP Program

The DIPOP client program is more complex. It uses the DI functions to access
the server, expand filespecs, and retrieve directory information; it has a
user interface, and it uses multiple threads to manage input and output.

Like LS, DIPOP is a character-mode directory information program. But DIPOP
is not a command-line utility; it has a real user interface and you can
leave it running in a PM window and use it as needed. DIPOP handles only one
filespec at a time, but you can use either the keyboard or the mouse to
select the drive and directory and to create the filespec used to search for
files. You can also select a combination of attributes to be used. To
facilitate mouse input in a character-mode application, I've adapted the
screen buttons that were used in an earlier article in this series
("Exploring the Key Functions of the OS/2 Keyboard and Mouse Subsystems,"
MSJ Vol. 4, No. 4). Finally, DIPOP implements a message queue scheme that is
reminiscent of the one found in Presentation Manager (see the sidebar
"Create  Message Queues Without Using Presentation Manager").

DIPOP's architecture is inherently multithreaded. Separate threads monitor
mouse and keyboard input and another thread displays the results of each
filespec request in a portion of the screen. The main thread starts each of
the other threads, reacts to information from the keyboard and mouse
threads, creates and sends requests to the DI server, and notifies the print
thread when the server has returned results.

Two key components are essential to the architecture of DIPOP. The first is
the message queues, which the mouse and keyboard thread use to notify the
main thread of input events and which the main thread uses to notify the
print thread that output is available. The other is the use of screen
buttons, which are kept in a table (an array of structures) in DIPOP.C.
Several routines, found in BUTTON.C , are available to initialize, display,
paint and repaint the buttons, and to notify a thread when a mouse click
occurs inside them. Each button is assumed to be three screen rows high, use
a double border on the top and bottom and a single border on the sides, and
be painted as white foreground on red background when highlighted (this is
controlled by a macro at the top of the source file). The structure for each
button includes the text to be displayed in the button, the button's row and
column coordinates, and a button attribute for repainting the button. A
variable controls whether the button is a press button (turns briefly on and
then off when clicked), a toggle button (turns off when clicked and on when
clicked again), or an input button (does not change state when clicked).
Finally, each button contains values that are returned when the left or
right mouse button is clicked on it and an accelerator key is pressed (this
allows the button to be accessed by using the keyboard).

Using the message queues and mouse buttons minimizes the overhead of the
main thread and efficiently delegates the management of input to the
keyboard and mouse threads. The buttons also allow the main thread to create
an input screen easily, and simplify maintenance and modifications by the
programmer. For example, I added a directory attribute button to the
interface long after the rest of the code had been written, spending only a
few moments and adding only a couple of lines of source code.

How DIPOP Works

The use of the screen buttons and the message queue simplifies the operation
of DIPOP immensely. The main thread begins by calling the message queue
function, MsgQCreate, to create its message queue. Next, the program clears
a semaphore that it will use to control the scrolling by the print thread
(signaled by pressing the Pause button). Then the main thread creates each
of the three threads and sleeps briefly, to allow them to get started. The
main thread then calls a set of customized functions that initialize the
buttons and the screen and display the various buttons.

Finally, the main thread enters a loop that contains a large switch
statement. It will block on a call to MsgQGet until it receives an event
message and will use the switch statement as a table of messages and
actions. Unidentified messages are passed, by default, to the buffermgr
function, which will attempt to use the message to manipulate the filespec
entry field. Thus, once the main thread of DIPOP receives an event message
from its message queue, it can process the message swiftly and effectively
and take any necessary action.

All mouse and keyboard input in DIPOP is detected by the appropriate thread.
The mouse thread uses the ButtonPressed function to screen out unwanted
mouse events and to include events for which a screen button was pressed.
The keyboard thread uses the function  AcceleratorPressed to distinguish
button accelerators from other keyboard events. Both of these threads call
the message queue function, MsgQSend, to notify the main thread of these
events. Since all message queue messages are a single unsigned word, the
application is designed to use the high-order bits in the message to pack
additional meaning into the message. This is how ordinary ASCII values, scan
codes, and mouse events are encapsulated into messages (see the SCANCODE
macro and EVENTCODE macro at the beginning of the program).

Most event messages pertain to building a request for directory information,
but there are three exception messages. When the main thread receives an
ENTER message, it calls SendRequest to create the request and send it to the
DI server. After the server has serviced the request and before the function
returns, it places the request on the print thread's message queue,
notifying the print thread that there is work to be done. If the main thread
receives a PAUSE_EVENT message, it will toggle the state of the semaphore
that controls the processing by the print thread. This lets the main thread
control the print thread instantly, allowing the user to control the
scrolling of the display temporarily. If the main thread receives an
ESCAPE_EVENT message, it will close its message queue, destroy any existing
DI request, clear the screen, and exit.

User's View of DIPOP

Figure 5 shows what the user sees when using DIPOP. The screen is divided
into two sections: half of the screen area is in a window that extends from
the upper-left corner of the screen, and the other half is filled with
buttons and runs along the right and bottom sides of the screen.

To build a directory request, users just type a filespec (at the cursor
position in the entry field) and select the drive and directory in which the
server will search for the filespec. To select the drive, the mouse is used
to click the drive button. Clicking the left mouse button will decrement the
drive letter choice; clicking the right button will increment it. As users
change drives, the current directory for each appears along the upper border
of the directory button. The path that appears is always the one that is
used in a DI request. The directory button can be used to see into a list of
subdirectories in the current directory. Users can click on this button to
view the entries in the list. Clicking the right mouse button on the [..]
button  changes to a directory; clicking the left mouse button on the [..]
button changes to the parent directory. When changing directories, the path
on the upper border of the drive button will reflect the change. The [\]
button can be used to jump to the root directory of any drive.

In addition to typing the characters that make up the filespec, users can
use the mouse to click the " * ", " \ ", " . ", and " ? " buttons. When one
of these buttons is clicked, the program inserts the character represented
by the button at the current cursor position in the entry field. Users can
control the scope of the search by clicking the attribute buttons that
appear on the right side of the screen. They can access the attribute
buttons via an accelerator key, which is Alt and the first letter of the
button text; for example, Alt-H toggles the Hidden button.

When users are ready to initiate a  request for directory information, they
press the Enter key or click the Enter button. To pause the scrolling of the
results, they click the Pause button (clicking the Pause button again
resumes scrolling). Pressing Alt-C or clicking the Clear button clears the
entry field and resets the attribute buttons. Users can press Esc or  click
the Esc button to terminate the program.

Improvements

After designing the DI programs presented in this article, I thought of
several improvements that could be made to them. The server could be
modified to use named pipes, which would allow it to be used transparently
over a network. Moving the results from a memory block that belongs to the
server into a memory block that belongs to the client would require some
work--but it could be done. As I mentioned, the DI functions could be
implemented as dynamic-link libraries.

One improvement to DIPOP involves a bug in OS/2 that occurs when it is run
in a full-screen window.  If DIPOP is run in a full-screen window, the mouse
pointer will leave traces of itself on the screen whenever a screen button
is clicked. The code that repaints a button (in the ButtonPress routine)
simply paints the button, sleeps momentarily, and repaints the button.
Unfortunately, there is no coordination between VIO and the mouse driver, so
VIO doesn't take into account that the mouse driver might need to know that
the mouse pointer should be redisplayed in a new color--thus, traces of the
mouse pointer are left on the screen. The only way around this problem is to
store the current location of the mouse pointer before doing any screen
update that might overwrite it, save what is stored under the mouse pointer,
perform the screen update, and restore what was under the mouse pointer.
It's unfortunate that the user has to do this, since this is just what VIO
and the mouse driver are supposed to do.

With the advent of OS/2 Version 1.2, there will be opportunities to make
some minor modifications to the programs so that they will work
transparently with the new High Performance File System (HPFS). These
modifications should allow long, free-form filenames, few or no restrictions
on path punctuation, new extended attributes, and new access control lists.
The changes should also allow the use and display of the additional
date-time stamps provided by HPFS.

The error checking and exception handling in the DI programs can be
improved. The server could be modified to clear all pending semaphores
before it exits and to allocate the thread stacks dynamically instead of
keeping them in a table.

Other improvements to DIPOP might include better error and exception
handling, syntax checking, and a wait indicator. Finally, the most important
improvement would be a graphical user interface with real buttons, list
boxes, and scroll bars. The next step for the program is obvious: a port to
Presentation Manager.

Figure 1

#define INCL_DOS

#include<os2.h>
#include<stdio.h>
#include"di.h"


void main(int argc, char **argv);

void main(int argc, char **argv)
    {
    PVOID requesthdl = NULL, resulthdl;
    USHORT  i, numresults;
    char    filename[40];

    if(argc != 2)
        {
        printf("Usage: disimple filespec\n");
        DosExit(EXIT_PROCESS,0);
        }

    DiMakeRequest(&requesthdl,argv[1],0);
    DiSendRequest(requesthdl);
    DiGetResultHdl(requesthdl,0,&numresults,&resulthdl);

    for( i = 0; i < numresults; i++)
        {
        if(i)
            DiGetNextResult(resulthdl,filename);
        else
            DiGetFirstResult(resulthdl,filename);
        printf("\t%s\n",filename);
        }
    DiDestroyRequest(&requesthdl);
    }


Figure 2

typedef struct _requestheader
    {
    ULONG    RAMsem;    /* RAM semaphore for client      */
    SEL    rselector;    /* Client selector to results    */
    HQUEUE    qhandle;    /* Handle to server's queue      */
    PID    qowner;    /* PID of server (queue owner)   */
    VOID FAR    *resultptr;    /* Server pointer to work area   */
    SEL    serverhsel;    /* Server selector to header     */
    SEL    serverwsel;    /* Server selector to results    */
    PCH    currentdir;    /* Client's dir in work area     */
    PCH    requestspec;    /* Next part of work area        */
    USHORT    size;    /* Current size header segment   */
    USHORT    resultsize;    /* New size of result segment    */
    USHORT    totalresults;    /* Total results found           */
    USHORT    numRequests;    /* Number of requests being made */
    DIRINFORESULT    resultArray[1];    /* Request structures            */
    } REQUESTHEADER;
typedef REQUESTHEADER FAR *PREQUESTHEADER;


Figure 3

typedef struct _dirinforesult
    {
   USHORT    attributes;    /* Attributes this request    */
   PCH    filespec;    /* File spec to use           */
   PCH    currentdir;    /* Current dir this request   */
   PFILEFINDBUF    firstfile;    /* First result               */
   PFILEFINDBUF    nextfile;    /* Next result                */
   USHORT    numfound;    /* Number of files found      */
   USHORT    errorval;    /* Error value returned       */
   struct _dirinforesult    *next;    /* Related structure if found */
    } DIRINFORESULT;
typedef DIRINFORESULT FAR *PDIRINFORESULT;


Figure 4

#define    INCL_DOS
#define    INCL_ERRORS

#include<os2.h>
#include<mt\stdio.h>
#include<mt\string.h>
#include<mt\stdlib.h>
#include<mt\ctype.h>
#include"errexit.h"

#define DICODE
#include"di.h"

#define  lastchar(str)    (str[strlen(str)-1])

void     diInit(PID *qowner, HQUEUE *qhandle);
void     adddriveletter(PCH *filespecs,USHORT driveno);
void     makefpath(char *org,char *result,char *currentpath,
                   USHORT currentdrive);
void     getdriveinfo(USHORT *currentdrive,char *currentpath,
                      USHORT *psize);
USHORT   diallocseg(USHORT size, SEL *oursel,PID other,SEL *othersel);
void     convertptr(VOID **ptr,SEL newsel);

    /* DiMakeRequest
    This function creates or adds to an existing directory information
    request. A new request is created if the pointer (hptr) is set to
    NULL. Note that this is the pointer whose address is passed to the
    function. The filespec is a full path and file specification with
    optional wildcards. The attribute parameter will control the files
    that are found. */

void DiMakeRequest(PREQUESTHEADER *hptr, PCH filespec, USHORT att)
    {
    PREQUESTHEADER    header;
    PDIRINFORESULT    resultstru;
    SEL    hselector,serversel;
    void far    *results;
    USHORT    retval,size,psize = 0,setdir = FALSE;
    HQUEUE    qhandle;
    PID    qowner;
    PCH    requestspec;
    char    resultbuf[_MAX_DIR],*resultfspec;
    char    currentpath[_MAX_PATH];
    USHORT    currentdrive;

    if(!(header = *hptr))
      diInit(&qowner, &qhandle);

    /* Now gather the information to prepare the header:
     ■  Get the current disk drive
     ■  Get the length of the current path
     ■  Get the size of the request arguments (the requestspec)
     ■  Allocate the header segment
     ■  Make it available to the server process */

    getdriveinfo(&currentdrive,currentpath,&psize);

    if(!header)                            /* If no header is allocated */
      {
                                           /* Allocate header segment */
      if(retval = diallocseg(size = sizeof(REQUESTHEADER),&hselector,
                             qowner,&serversel))
        error_exit(retval,"diallocseg");

      header = MAKEP(hselector,0);         /* Make header pointer */
      *hptr = header;
      header->serverhsel = serversel;      /* Allocate work segment */
      if(retval = diallocseg(MAXSEGSIZE,&header->rselector,qowner,
                             &serversel))
        error_exit(retval,"diallocseg");

      results = MAKEP(header->rselector,0) /* Make pointer */

      header->RAMsem = 0L;                 /* Initialize semaphore */
      header->resultptr = MAKEP(serversel,0); /* Set pointer to work area */

      header->serverwsel = serversel;      /* Selector to work area */
      header->numRequests = 0;             /* Set number of requests */

    requestspec = (PCH)results;            /* Set to work area */
      header->currentdir = requestspec;    /* Set pointer to it */
      *requestspec++ = (char)(currentdrive + 'A' - 1);
                                           /* Add drive letter */
      strcpy(requestspec,":\\");           /* And ":\" */
      requestspec += 2;                    /* Move pointer past them */

      DosQCurDir(currentdrive,requestspec,&psize);
                                           /* Add current directory */
      requestspec += (strlen(requestspec)+1); /* Move pointer past dir */
      header->qowner = qowner;
      header->qhandle = qhandle;
      }

    else
      {
      requestspec = header->requestspec;
      qowner = header->qowner;
      qhandle = header->qhandle;

      size = header->size + sizeof(DIRINFORESULT);
      hselector = SELECTOROF(header);

      if(retval = DosReallocSeg(size,hselector)) /* Resize segment */
        error_exit(retval,"DosReallocSeg");
      }

           /* Resultstru always points to the next available structure */
   resultstru = &header->resultArray[header->numRequests];
    strupr(filespec);                         /* Set arg to upper case */
    memset(resultstru,0,sizeof(DIRINFORESULT)); /* Clear structure */
    resultstru->attributes = att;             /* Set attributes */
                                              /* Get full path filespec */
    makefpath(filespec,resultbuf,currentpath,currentdrive);
    resultfspec = strrchr(resultbuf,'\\');    /* Find last backslash */

    if(strcmp(resultbuf,header->currentdir))  /* If not in currentdir */
      {
      strcpy(requestspec,resultbuf);          /* Copy path */
      resultstru->currentdir = requestspec;   /* Set pointer */
      requestspec += (strlen(requestspec)+1);
                                            /* Set pointer to next spot */
      }

    else                                      /* Use default directory */
      resultstru->currentdir = header->currentdir;  /* Set pointer */

    strcpy((char *)requestspec,resultfspec);  /* Copy the filespec */
    resultstru->filespec = requestspec;       /* Set a pointer to it */
    requestspec += (strlen(requestspec)+1);   /* Set to next position */

    header->size = size;
    header->numRequests++;
    header->requestspec = requestspec;
    }

void DiSendRequest(PREQUESTHEADER hdr)
    {
    PCH *resptr,*newptr;
    USHORT offset,retval,i;
    SEL newsel,serverwsel = SELECTOROF(hdr->resultptr);
    PBYTE sheader = MAKEP(hdr->serverhsel,0);  /* Make pointer */

           /* Adjust resultptr to point to available space */
    resptr = MAKEP(hdr->rselector,0);          /* Create ptr to result */
    offset = (hdr->requestspec - (PCH)resptr); /* Get offset to use */
    hdr->resultptr = MAKEP(serverwsel,offset); /* Reset pointer */

           /* Write request to the queue */
    if(retval = DosWriteQueue(hdr->qhandle,0,hdr->size,(PBYTE)sheader,0))
      error_exit(retval,"DosWriteQueue"); /* Wait for server to finish */

    DosSemSetWait(&hdr->RAMsem,SEM_INDEFINITE_WAIT); /* Get new segment */
    if(retval = DosAllocSeg(hdr->resultsize,&newsel,SEG_NONSHARED))
      error_exit(retval,"DosAllocSeg");

    newptr = MAKEP(newsel,0);              /* Make pointer to segment */
    memmove(newptr,resptr,hdr->resultsize); /* Copy from old segment */
    convertptr(&hdr->currentdir,newsel);   /* Convert pointers */
    convertptr(&hdr->requestspec,newsel);

    for( i = 0; i < hdr->numRequests; i++)
      {
      convertptr(&hdr->resultArray[i].filespec,newsel);
      if(SELECTOROF(hdr->resultArray[i].currentdir) == hdr->rselector)
      convertptr(&hdr->resultArray[i].currentdir,newsel);
      convertptr(&hdr->resultArray[i].firstfile,newsel);
      convertptr(&hdr->resultArray[i].nextfile,newsel);
      }

    DosFreeSeg(hdr->rselector);          /* Free old segment */
    hdr->rselector = newsel;             /* Set for new selector */
    }

void DiGetNumResults(PREQUESTHEADER header,USHORT *numresults,
                     USHORT *numrequests)
    {
    *numresults = header->totalresults;
    *numrequests = header->numRequests;
    }

void DiDestroyRequest(PREQUESTHEADER *header)
    {
    USHORT retval;
    PREQUESTHEADER hdr = *header;

    if(retval = DosFreeSeg(hdr->rselector))  /* Free work segment */
      error_exit(retval,"DosFreeSeg");
    if(retval = DosFreeSeg(SELECTOROF(hdr))) /* Free header segment */
      error_exit(retval,"DosFreeSeg");

    *header = NULL;                          /* Set pointer to NULL */
    }

char *DiGetResultFspec(PDIRINFORESULT result)
    {
    static char *p = "*.*";

    if(result->errorval == DIREXPANDED)
        return p;
    return result->filespec;
    }

char *DiGetResultDir(PDIRINFORESULT result)
    {
    static char dirbuf[80];

    if(result->errorval == DIREXPANDED)
        {
        strcpy(dirbuf,result->currentdir);
        strcat(dirbuf,"\\");
        strcat(dirbuf,result->filespec);
        return dirbuf;
        }

    return result->currentdir;
    }

void DiGetResultHdl(PREQUESTHEADER header,USHORT requestnum,USHORT *num,
PDIRINFORESULT *resulthdl)
    {
    *resulthdl = &header->resultArray[requestnum];
    *num = (*resulthdl)->numfound;
    }

void DiGetFirstResult(PDIRINFORESULT result, char *buffer)
    {
    result->nextfile = result->firstfile;
    DiGetNextResult(result,buffer);
    }

void DiGetNextResult(PDIRINFORESULT result, char *buffer)
    {
    if(!result->nextfile)
      {
      *buffer = '\0';
      return;
      }

    strcpy(buffer,result->nextfile->achName);
    result->nextfile =
    (PFILEFINDBUF)(&(result->nextfile->cchName)+
                      result->nextfile->cchName+2);
    }

void DiGetFirstResultPtr(PDIRINFORESULT result, PFILEFINDBUF *ptr)
    {
    result->nextfile = result->firstfile;
    DiGetNextResultPtr(result,ptr);
    }

void DiGetNextResultPtr(PDIRINFORESULT result, PFILEFINDBUF *ptr)
    {
    if(!result->nextfile)
      {
      *ptr = NULL;
      return;
      }

    *ptr = result->nextfile;
    result->nextfile =
    (PFILEFINDBUF)(&(result->nextfile->cchName)+
                      result->nextfile->cchName+2);
    }

void DiBuildResultTbl(PREQUESTHEADER header, PFILEFINDBUF **table)
    {
    SEL tablesel;
    USHORT retval, i;
    PFILEFINDBUF f,*temp;

    if(retval = DosAllocSeg((header->totalresults*sizeof(PFILEFINDBUF)),
                &tablesel, SEG_NONSHARED))
      error_exit(retval,"DosAllocSeg");

    temp = MAKEP(tablesel,0);
    f = header->resultArray[0].firstfile;

    for( i = 0; i < header->totalresults; i++)
       {
       temp[i] = f;
       f = (PFILEFINDBUF)(&(f->cchName)+f->cchName+2);
       }

    *table = temp;
    }


void DiDestroyResultTbl(PFILEFINDBUF **table)
    {
    USHORT retval;
    if(retval = DosFreeSeg(SELECTOROF(*table)))
      error_exit(retval,"DosFreeSeg");
      *table = NULL;
    }

void diInit(PID *qowner, HQUEUE *qhandle)
    {
    USHORT    retval;

/* Try to open the queue to the directory server */
    if(!(retval = DosOpenQueue(qowner,qhandle,DIRINFOQNAME)))
      return;
    if(retval != ERROR_QUE_NAME_NOT_EXIST)
      error_exit(retval,"DosOpenQueue");
    else
      error_exit(retval,
                 "DosOpenQueue - Server probably hasn't opened queue");
    }

void convertptr(VOID **ptr,SEL newsel)
    {
    USHORT offset = OFFSETOF(*ptr);
    *ptr = MAKEP(newsel,offset);
    }

/* Assumes that globals, currentpath and currentdrive are properly
  initialized */
void makefpath(char *org,char *result,char *currentpath,
              USHORT currentdrive)
    {
    char drive[_MAX_DRIVE], dir[_MAX_DIR], fname[_MAX_FNAME],
              ext[_MAX_EXT];
    char currdir[_MAX_DIR], *backup, *outdir;
    USHORT driveno,cdirsize = _MAX_DIR-1, retval;
    strupr(org);                             /* Make the path uppercase */
    _splitpath(org,drive,dir,fname,ext);     /* Get path components */

    /* If we have a full path from the user at this point, we don't have
     to do anything more--whatever they ask for, they get. If we don't have
     a full path, we will need to get the full path to the current
     directory of that drive, and then reconcile the working directory or
     parent directory to end up with the "real" path to the file. However,
     if the path is on the same drive, we can use currentdir and save the
     time of a DosQCurDir call. */

    if(!(*drive))                            /* If no drive letter */
      {
      driveno = currentdrive;
      *drive = (char)(currentdrive+'A'-1);
      strcpy(&drive[1],":");
      }
    else
      driveno = (*drive-'A'+1);

    if(*dir != '\\')                         /* If not a full path */
      {
      if(driveno != currentdrive)
        {
        *currdir = '\\';
        if(retval = DosQCurDir(driveno,&currdir[1],&cdirsize))
          error_exit(retval,
                     "DosQCurDir - probably invalid drive or directory");
        }

      else
        strcpy(currdir,&currentpath[2]);

      if(lastchar(currdir) != '\\')
        strcat(currdir,"\\");

      strcat(currdir,dir);
      strcpy(dir,currdir);
      }

     /* dir now has full path to the filespec, reconcile and restore */
    while(backup = strstr(dir,"\\..\\"))    /* Remove any "\..\" */
      {
      for(outdir = backup-1; (*outdir != '\\') && (outdir > dir);
           outdir--);
                                            /* Now outdir is '\' dest */
       backup += 3;    /* now backup is source '\'*/
       strcpy(outdir,backup);
       }

    while(backup = strstr(dir,"\\.\\"))            /* Remove any "\.\" */
      {
      outdir = backup;
      backup += 2;
      strcpy(outdir,backup);
      }

    _makepath(result,drive,dir,fname,ext);  /* Put it all back together */
    }

void getdriveinfo(USHORT *currentdrive,char *currentpath,USHORT *psize)
    {
    ULONG  drivemap;
    USHORT retval;

    DosQCurDisk(currentdrive,&drivemap);    /* Get current drive number */
    *currentpath = (char)(*currentdrive+'A'-1);
    strcpy(&currentpath[1],":\\");

    *psize = _MAX_PATH;
    if(retval = DosQCurDir(*currentdrive,&currentpath[3],psize))
      error_exit(retval,"DosQCurDir");

    DosQCurDisk(currentdrive,&drivemap);    /* Get current drive number */
    *psize = 0;
    DosQCurDir(*currentdrive,NULL,psize);   /* Get size of current path */
    }

USHORT diallocseg(USHORT size, SEL *oursel,PID other,SEL *othersel)
    {
    USHORT retval = 0;
                                            /* And shareable by server */
    if(!(retval = DosAllocSeg(size,oursel,SEG_GIVEABLE)))
      retval = DosGiveSeg(*oursel,other,othersel);

    return retval;
    }


Create Message Queues Without Using Presentation Manager

Early in the design of the DIPOP program, it became apparent that using
semaphores to pass information and synchronize input from the mouse and
keyboard threads would be awkward and cumbersome. If a semaphore were used,
the mouse or keyboard thread might be blocked for too long while waiting for
the main thread to acknowledge and act on the event. Plus, users could
alternate between clicking mouse buttons and pressing keys; using semaphores
to determine the order of events would be messy. It is also possible that
multiple events could occur and accumulate quickly--which would be
intolerable if the threads were not able to process input efficiently.

A facility was needed that would report keyboard and mouse events in the
order in which they occurred. Such a facility would allow the event
information to accumulate in that order until the main thread acted on them.
In addition, the mechanism would be efficient, simple, and, with luck,
elegant.

The solution was the handful of functions in MSGQ.C (see Figure A). These
functions apply a software interface layer to OS/2 queues and create and
manipulate a series of message queues. OS/2 queues are typically used to
transfer a pointer to shared memory (this is how the DI client-server
functions use them), but they can also transfer a user-defined code in the
form of a 2-byte, unsigned word. You can find more information on OS/2
queues in the article on IPC in this series, "A Complete Guide to OS/2
Interprocess Communications and Device Monitors," MSJ Vol. 4, No. 5. Since
the contents of this user-defined code are determined by the programmer, you
can use it to create your own messages, which can be customized from
application to application or used over again. In addition, queues can be an
efficient way to pass the event code.

Thus, the functions in MSGQ.C allow one thread, the message queue owner, to
use MsgQCreate to create a message queue. This function returns a handle
that the queue owner passes to MsgQGet, where the thread will block until a
message is received and returned. Other threads can send messages to the
queue owner thread by opening the message queue with MsgQOpen, which also
returns a handle. These threads can pass the handle to MsgQSend, along with
a message that is placed in the queue. The message queue functions hide the
details of manipulating and using OS/2 queues; a thread only needs the name
of the queue to create or open it and it can use the returned handle when
referencing the queue.

Message Queues and DIPOP

In DIPOP, message queues are essential to coordinating the activities of the
different threads and passing information from one thread to another. Two
message queues are created: one is owned by the main thread and is used by
the mouse and keyboard threads; the other is the print thread, which uses a
message queue to allow access to the results of DI requests from the main
thread.

After the main thread has created the other threads and completed its
initialization, it calls MsgQGet and blocks until it receives an input event
message from the keyboard or mouse thread. These threads block on keyboard
input (by using KbdCharIn) or mouse input (using MouReadEventQueue). The
keyboard thread can quickly identify a keystroke as an ASCII key, scan code,
or accelerator key, place the key in the main thread's message queue, and
wait for the next keystroke. The mouse thread waits for mouse events,
screens out unwanted events (such as mouse movements when no buttons are
pressed), identifies important mouse clicks, and places the corresponding
messages in the message queue. Then it resumes waiting for the next mouse
event. The main thread, meanwhile, remains blocked much of the time, waiting
for event messages to appear in its queue and reacting to them when they do
appear. The result is that the main thread's code is a large switch
statement that resembles the kind of message-processing code found in
Windows3 and in PM programs.

This design worked so well that I decided to use it again when I wrote and
tested the print thread code. It was easy to have the print thread create a
message queue, block on the call to MsgQGet until the main thread posted a
message, and use the message to display the results of a DI request. The
messages generated by the keyboard and mouse threads are largely keyboard
codes with the high-order bits set to indicate whether they are from the
text buttons used in DIPOP. Since a message passed to the print thread is
the selector of the request header segment, the main thread uses the
SELECTOROF macro to extract the selector from a request header pointer or
handle, and that selector is the message that is placed in the queue. The
print thread uses the MAKEP macro to convert this selector back into a
pointer or handle to the request.

Finally, a welcome side effect of using the message queues in DIPOP was the
compactness of the code for the mouse and keyboard threads. With so little
to do, these functions stayed small, tight, and efficient.

Advantages

Message queues let you create a message-based, event-driven architecture
that simplifies the workings of your program and the organization of your
application's code. They let you focus the program's code on reacting to
events; for example, user input. This makes the job of writing the code much
simpler. (Just make sure you have a way to handle any foreseeable input.)
Message queues also make it easier to write more efficient code, since a
thread will wait for something to do and then promptly do it.

In DIPOP, message queues are the means of coordinating input and output
among multiple threads. You won't have to worry about flags or semaphores or
the order of events--that's taken care of for you. Message queues allow each
thread to monitor a source of input and independently report events to
another thread, and for the events to be stored and retrieved in a simple,
orderly fashion, without additional programming overhead.

Message queues also allow more flexibility in designing a program and
simplify stepwise refinements to the code. If, for example, a new capability
is needed, you simply define the input (such as a keystroke or mouse click),
define the steps for processing that input (such as adding a case to the
switch statement), and you're done.

A multithreaded program architecture allows for a proper division of the
labor; that is, each thread is delegated the responsibility of handling a
specific task. Message queues help ensure the smooth operation and flow of
information from one thread to another, since they allow each thread to
report important events and return to what the thread does best--waiting for
the next event to occur.

DosFindFirst and DI Memory Management

The most important benefit of the DI functions and server is that they give
applications a simpler, more effective, more efficient interface to the OS/2
file -searching functions. DosFindFirst and DosFindNext fit several
application contexts flexibly, but their main purpose is to make it easy to
port applications to OS/2 from DOS. Before exploring how to use them more
effectively under OS/2, here's a brief look at their DOS predecessors.

Finding Files under DOS

When a DOS application needs to expand a wildcard filespec, it uses two
functions of Int 21h. Typically, the application gains access to the DOS
disk transfer area (DTA) and passes the filespec to function 4Eh of Int 21h.
This function places the name, size, attributes, and date-time stamp of the
first file found into the DTA. Then the application enters a loop and
generates the interrupt again, this time with function 4Fh, and retrieves
the information on each file until no more files are found.

This process is illustrated in two real-mode sample programs--DIRREAL1.C and
DIRREAL2.C. The former illustrates how the entire process is accomplished in
C. The latter shows how high-level additions to Microsoft's real-mode C
run-time library simplified the process somewhat.

Under OS/2, you can create a  protected-mode equivalent, like DIRPROT.C.
That program demonstrates how the DosFindFirst and DosFindNext functions
simplify a port of a DOS program to OS/2 by keeping the logic of the
application intact. To port the code, just replace the Int 21h  calls with
DosFindFirst  and DosFindNext, adjust the code to use the OS/2 FILEFINDBUF
data structure, and you're done.

Although this approach to searching for files must be followed under DOS, it
isn't particularly efficient for OS/2. First, the calling application has to
loop through multiple calls to the DosFindNext function and there is no way
to determine how many of these calls must be made ahead of time. The loop
could take a long time to finish if the number of files is large. Second,
the application can process information on only one file at a time, or it
must manage memory in an attempt to store all the file information found.
There are two accepted approaches to storing all of the information. One is
to allocate space for each piece of file information dynamically. The other
is to make an assumption about the number of files to be found, allocate the
space to hold them, and add to that space when necessary. Either approach
can slow down an application if a large number of resulting files is found.

DosFindFirst and DosFindNext

In some OS/2 applications, using a loop with DosFindFirst and DosFindNext is
reasonably efficient, especially when you're searching for a single file or
when time and memory management are not critical to the calling thread. To
use DosFindFirst, an application must supply a pointer to a filespec, a
directory handle, an attribute word in which at least one bit should match a
bit in the attributes of the resulting files, a buffer in which to place the
results, the length of the buffer, and a variable in which the application
specifies the number of files to find. (OS/2 will replace the variable with
the number of files found.) DosFindNext requires the same parameters, except
for the filespec and attribute word.

DI Approach

Although the DOS-like approach to finding files makes it easy to port
applications from DOS to OS/2, DosFindFirst can be used more efficiently.
Indeed, DosFindFirst is quite capable of returning all the files found in a
single call, eliminating the need to call DosFindNext at all. The trick is
to provide a memory buffer big enough to hold all the resulting FILEFINDBUF
structures.

The DI functions and the DI server make providing that memory buffer easy.
They allocate a full 64Kb segment for use in the DosFindFirst call and place
all of the results found in this segment. Then they essentially resize the
segment, so that when DiSendRequest returns to the calling thread, the
application owns only the memory that is necessary to hold the results.

The DI functions assume that all results found for a single request,
including those returned by the expansion of multiple filespecs, will fit
into a single 64Kb segment. What this means, for example, is that if an
application made a request with 10 filespecs and each expanded to 100 files,
in which every filename occupied a full 13 characters, the result would
occupy 36,000 bytes. (A full 13-character filename with the terminating NULL
makes a FILEFINDBUF structure that is 36 bytes long; thus the result is 10 x
100 x 36, or 36,000 bytes.). A more likely situation would be 10 filespecs
expanded to 200 files each in which the average length of the filenames was
eight characters--in this case, the results would need a total of 62,000
bytes (10 x 200 x 31). This would leave 3535 bytes to spare. Although for
most applications 64Kb is a safe assumption, an adjustment should be made
under OS/2 Version 1.2 to use multiple work segments and allow for a longer
average filename.

Managing Memory

When an application calls DiMakeRequest to create a new request (with a
handle initialized to NULL), the function allocates the header segment and a
65,535-byte work segment and makes them shareable with the server, which
will use the work segment for calls to DosFindFirst. Later, when the server
makes this call, it will ask DosFindFirst to return all files found in one
call, eliminating the need to call DosFindNext.

When a filespec is added to a request via DiMakeRequest, the full path and
filespec are stored in the work segment. The function also calls
DosReallocSeg to enlarge the header and append a new DIRINFORESULT structure
to it (pointers in this structure are set to the path and filespec). Thus,
the DI functions can access these structures as a contiguous,
variable-length array from an offset in the header, eliminating the
complicated work with pointers that would be necessary if the structures and
filespecs were intermixed. Since the actual number of structures is
determined by the numRequests field of the header structure, the header can
be extended indefinitely. In addition, this method allows filespecs and
paths of varying length to be stored and accessed with little or no effort.
As a footnote, I should mention that if a filespec's path is the same as the
application's (stored in the work segment on the first call to
DiMakeRequest), the path is not stored; its DIRINFORESULT pointer is instead
set to the application's path.

The additions to the beginning of the work segment do not have too great an
impact on the remaining space. A large number of filespecs (perhaps 20) with
long paths (say an average of 50 characters each) would occupy about 1000
bytes--leaving 64,535 bytes for the resulting filenames after expansion.
Thus, the workspace available to the server when it first calls DosFindFirst
is 65,535 minus the space occupied by the filespecs and paths.

Just before DiSendRequest places a request in the server's queue, it adjusts
the server's workspace pointer to an offset beyond the paths and filespecs
in the work segment. As I indicated earlier, the server asks DosFindFirst to
return, in one call, all files found that match the filespec. After each
call, the server adjusts the workspace pointer to point beyond the most
recent FILEFINDBUF results. Thus, each call to DosFindFirst in a request
uses a smaller buffer space that is found at offsets further and further
into the work segment. When a server thread is finished with a request, it
calculates the space occupied by the results in the work segment and stores
that result in the request header for use by DiSendRequest. Then it clears
the request header semaphore, frees the header and work segments, and
terminates itself.

At this point, the client owns both segments; OS/2 will not discard shared
segments until both processes have freed them. It would be ideal if the
client could adjust the work segment down to the size occupied by the
results of the request--there's no need for a 64Kb segment if only 4Kb,
16Kb, or even 20Kb are being used. Unfortunately, you cannot use
DosReallocSeg to shrink a shared segment. Therefore, as soon as the server
thread handling the request has freed the work segment and is about to free
the header segment and terminate itself, it prepares a new work segment,
which is the same size as the occupied portion of the old one. Then it
copies the contents of the old segment to the new one and adjusts the
appropriate pointers in the header to use the selector of the new work
segment. Finally, DiSendRequest frees the work segment (which OS/2 will now
discard) and returns. Note that the client can now call DiDestroyRequest in
order to free the header and the new work segment.

The DI functions and server make as much memory available as possible in the
calls to DosFindFirst, then shrink the memory to the required size when the
results are in. The overhead of memory management is relatively small, and
the DI functions conveniently handle multiple filespecs in a request, while
hiding the workings of the code from the client application. Further, the
time spent managing memory is made up by eliminating the calls to
DosFindNext and packaging the results in a form that makes it easy for
applications to employ functions like DiBuildResultsTbl to access them. You
could use the DI functions in an application any time the application needs
to use wildcards to search for more than one file, when time is critical,
and when you want to preserve the simplicity of an application.


Exploring Dynamic-Link Libraries with a Simple Screen Capture Utility

 Kevin P. Welch

A new programming concept that the Microsoft Windows environment has
introduced to personal computers is dynamic linking and dynamic-link
libraries (DLLs). In Windows1, dynamic linking refers to the manner in which
a function call in one module is dynamically related to object code in
another. DLLs contain a collection of functions that are linked dynamically.

Although DLLs are not directly executable and don't receive messages, they
are the building block on which the entire Windows interface is based.
However, despite this relationship, DLLs remain somewhat mysterious to many
Windows programmers.

The PRTSC utility is a relatively simple application that uses a DLL to
capture screen images and place them on the Windows clipboard. When screen
capture is activated, the Alt-PrtSc key combination copies a bitmap
representation of the client area of the active window,  the window frame,
or the entire display to the clipboard.

Besides serving as a useful documentation utility for your programs, PRTSC
also demonstrates how to employ DLLs, keyboard hook functions, and the
Windows clipboard. In fact, this utility is very similar to one in ClickArt
Scrapbook+, published by T/Maker Company; both are powerful and
full-featured extensions of the Windows clipboard.

System Hooks

The section of the Windows Version 2.0 Programmer's Reference manual on the
SetWindowsHook function is very intriguing. This function was not thoroughly
documented in previous versions of Windows, and developers were advised to
leave it alone until it became a formal part of the documented application
program interface (API).

System hooks are particularly interesting because they enable you to
intercept various messages before they are processed by Windows or
dispatched to an application. With this capability you can check for
interesting keystrokes (as PRTSC does), monitor application or system
messages, and even record all system events for subsequent playback.

System hooks are necessarily  a shared resource; when you install a hook you
can affect all applications. Also, because of expanded memory considerations
under LIM 4.0, most hook functions must be in DLLs to ensure that they are
never paged out and made inaccessible.

Associated with each system hook is a filter function, an
application-supplied routine that is installed into a chain of functions and
called whenever events of a specified type occur. Filter functions normally
have the format shown in the code in Figure 1.

The nCode parameter typically specifies whether the filter function should
process the information or call the DefHookProc function and pass it back to
the system. The wParam parameter provides additional information on the
event, usually defining the context in which the event occurred. Finally,
the lParam parameter is used to transfer additional information that further
clarifies the event in question. This could be a far pointer to a message
data structure or simply a set of binary flags that describe the event.
Using this filtering scheme, you can define system hooks that intercept
and/or process the kinds of events shown in Figure 2.

Once defined, system hooks are inserted into the chain with the
SetWindowsHook function. When you call this function you provide a code that
specifies the type of hook you are installing, followed by a
procedure-instance address of the appropriately defined filter function.
Note that this function must be exported in the library's module definition
file. Although libraries can use a function address directly since they can
only have one data segment, applications must use MakeProcInstance to
retrieve a procedure instance.



lpfnOldHook = SetWindowsHook
            (nHookType,
             (FARPROC)HookFn );

The value returned by the SetWindowsHook function is the procedure-instance
address of the filter function previously installed in the chain, if any
such filter exists. This value should be saved as needed when passing
messages down the chain with the DefHookProc function.

Of these system hooks, all but the application message hook must be defined
within the context of a DLL. The application message hook intercepts only
task-specific messages and is not subject to the same limitations as the
other system hooks.

While they are in use, system hooks can seriously degrade the performance of
Windows. Because of this, you should restrict their use to special-purpose
applications or development tools that help you to debug and test an
application. When  you have finished using a system hook, you should
promptly remove the filter using the UnhookWindowsHook function :



UnhookWindowsHook
(nHookType,(FARPROC)HookFn );



PRTSC Library

The PRTSC utility consists of a single DLL that contains one assembly
language and four conventional C functions. Of these five routines, the
keyboard hook function is perhaps the most interesting and unusual.

Although DLLs like PRTSC form the basis of the Windows API, programmers who
are  new to Windows seldom understand them. This is partly due to the fact
that they can be loaded only once and are in effect resources for use by
other applications. Further complicating matters is the need to write at
least one assembly language routine when developing a DLL.

When you link a conventional Windows application with LINK4, it defines
_astart as the program entry point. This function is defined in the standard
Windows object library you link to your program and is responsible for
performing the required housekeeping chores prior to calling WinMain, the
perceived entry point for the application. When _astart is called, the CPU
registers are defined as shown in Figure 3.

Unfortunately, a DLL like PRTSC requires a different scheme. By design,
PRTSC, like all other libraries, operates without a stack segment, using the
caller's stack in place of its own. This difference is reflected in the use
of LIBRARY instead of NAME for the program name in the module definition
file PRTSC.DEF.

When you make this change, LINK4 is instructed to use LibInit as the program
entry point in place of _astart and to define the CPU registers as shown in
Figure 4. LibInit, defined in PRTSC1.ASM (see Figure 5), is then responsible
for performing any required housekeeping, including initialization of the
local heap. Unfortunately, this subtle variation and the subsequent assembly
language programming have prevented some people from experimenting with
DLLs.

With some extra code, the Windows object library could include an entry
point for DLLs that performs this initialization and calls something like
LibInit with the library instance handle and a pointer to the command line.
The programmer would then write LibInit in his or her favorite language and
perform any desired initialization steps before returning control to the
system. In the case of PRTSC, LibInit simply calls PrtScInit, which is
defined in PRTSC2.C , where all library initialization is performed. See
excerpts in Figure 6. (The full source code can be downloaded from any of
the MSJ bulletin boards--Ed.)

During initialization PrtScInit checks the current display adapter type,
activates the keyboard hook, and iteratively searches for the MS-DOS
Executive window. The search ends when it has found the window, which is
identified by the Session class name. Note that lstrcmp is used instead of
strcmp because the szClassName variable is defined on the stack and is not
directly accessible when using a near pointer (remember that SS != DS in
DLLs).

Using the handle to the MS-DOS Executive window, PrtScInit appends a Screen
Capture... menu option to the end of the Special pull-down menu and
subclasses the entire window. This effectively enables the library to
intercept any message sent to the Executive window, including the message
generated by our newly appended option.

Although this may not be the most cooperative way to display the screen
capture control panel, it demonstrates how one application, in this case a
library, can subclass another. However, this is probably not an acceptable
programming practice for a commercial application.

The next function in PRTSC2.C is PrtScFilterFn. This routine effectively
filters all of the messages sent to the MS-DOS Executive window. The only
two messages of interest are the ones generated when the user selects the
Screen Capture... menu option or when the window is destroyed.

When subclassing an application with a function like PrtScFilterFn, there
are four ways in which messages can be handled, as shown in Figure 7. The
first message PrtScFilterFn intercepts is the one generated when the user
selects the Screen Capture... menu option from the MS-DOS Executive window.
In this case it displays a dialog box using the PrtSc dialog box template
contained in the resource file appended to the library. Because the
CMD_CAPTURE message is of interest only to the PRTSC library, it is not
passed  to the real window function. Also note that because the PRTSC
library, like most other DLLs, has only one data segment, the dialog box is
displayed without creating a procedure instance (using MakeProcInstance) of
the dialog box window function.

The second message that PrtScFilterFn intercepts is the WM_DESTROY message.
In this case, screen capture is automatically turned off (assuming it is
active) and the MS-DOS Executive window filter function is removed. This
ensures that screen capture is not left active without a means of
controlling it.

Most of the intercepted messages are ignored by PrtScFilterFn and passed to
the real window function; otherwise, the window would not work and the
system would probably stop.

Following the PrtScFilterFn in PRTSC2.C is the PrtScDlgFn function. This
routine is responsible for processing all the messages relating to the
screen capture control panel that is displayed when the Screen Capture...
menu option is selected. In this function, the keyboard hook function is
inserted or removed when the user selects a new screen capture mode. Again,
note how the PrtScHook function is used directly, without calling
MakeProcInstance to create a procedure-instance address.

The last function in PRTSC2.C is PrtScHook. This is the most complicated
routine in the library; it is responsible for intercepting each keystroke
and checking whether it is the Alt-PrtSc key combination.

The first thing this function does is examine the nCode parameter to see if
some action is expected. If so, the virtual keycode and corresponding
keyboard state are examined to see if Alt-PrtSc has been entered. When
encountered, the top-level window handle is determined and the screen
coordinates of the capture area calculated. In cases where window boundaries
extend beyond the screen dimensions, appropriate clipping is performed to
ensure a completely defined image.

The remainder of the function is devoted to copying the screen image and
placing it on the Windows clipboard. When this  is completed, the clipboard
is closed, which causes a WM_DRAWCLIPBOARD message to be sent down the
clipboard viewer chain. All associated resources are then removed. If any
part of this process fails, a message box is displayed, providing some
feedback to the user on the problem that was encountered.

Building PRTSC

Building a DLL is only slightly different from constructing a traditional
Windows application. Before doing this, however, you will need to enter or
download the following source files: PRTSC (make file), PRTSC.DEF (module
definition file), PRTSC.H (header file), PRTSC.RC (resource file),
PRTSC1.ASM (assembly language startup code), and PRTSC2.C (C source code).

In addition to these source files, you must install the following software
tools on your system: Microsoft Macro Assembler Version 5.0 or later,
Microsoft C Compiler Version 5.0 or later, and the Microsoft Windows
Software Development Kit (SDK) Version 2.03 or later. When you have all the
tools and source files in place, you can create the PRTSC library by
entering the following command:

MAKE PRTSC

Using PRTSC

Once you have created the PRTSC library, try it out by running Windows and
double clicking PRTSC.EXE. When PRTSC is loaded, a small message box will be
displayed in the center of your screen indicating that the Alt-PrtSc key
combination is available for screen capture to the clipboard.

If you select the Screen Capture... option under the Special pull-down menu
of the MS-DOS Executive window, you can view the current settings, turn
screen capture off, or select another capture mode. The Screen Capture menu
option will be checked whenever Alt-PrtSc is active.

If you run additional MS-DOS Executive windows after loading PRTSC, the
Screen Capture... menu option will be listed only on the first instance. If
you accidentally close this instance, screen capture will be turned off and
you will no longer be able to load the library; only one instance of a
library is allowed,  and it has already been loaded. With a little effort,
you can enhance the PRTSC library to prevent a lock-out by subclassing a
second MS-DOS Executive instance when the first window is closed.

Another feature of the PRTSC library you can experiment with is the
monochrome conversion option, assuming you have a color display system. As
you can imagine, the capture of color screen images to the clipboard can
consume large amounts of system memory. Also, many Windows applications
handle such bitmaps incorrectly, especially when they are transported in
files to computers with different display subsystems.

By choosing the Convert to Monochrome option you can automatically convert
the screen images you capture to monochrome by using the conversion routines
built into GDI. Although in certain cases this color mapping will produce
unexpected results, like mapping yellow to black, it will perform acceptably
with minor adjustments to your system color palette.

You should also try to capture menus using PRTSC library. If you hold down
Alt-spacebar, the system menu of the active window will appear. By keeping
Alt depressed and using the cursor movement keys you can bring up other
pull-down menus. Still holding Alt down, if you press PrtSc you can copy the
contents of the visible menu (perhaps with some other information) to the
clipboard. You can then edit out the parts you don't want and include the
menu with the documentation associated with your application.

Figure 1

FAR PASCAL FilterFunction( nCode, wParam, lParam )
    int    nCode;
    WORD    wParam;
    LONG    lParam;
{
    if ( nCode == DO_SOMETHING ) {
        /*** DO YOUR PROCESSING HERE ***/
    } else
        DefHookProc( nCode, wParam, lParam, &lpfnOldHook );
}


Figure 2

Keyboard Hook    Any keyboard event

Journal Record Hook    Any message retrieved from the event queue

Journal Playback Hook    Any message played back to the event queue

Application Message Hook    Messages to your application

System Message Hook    Messages to any application in the system

Window Procedure Hook    Messages to a window function (debug only)

Get Message Hook    Messages retrieved by GetMessage (debug only)

Figure 3

BX    Requested stack size

CX    Requested heap size

DI    Handle to application instance

SI    Handle to previous application instance

ES    Program segment prefix

Figure 4:

DI    Handle to library instance

DS    Library data segment

CX    Requested heap size

ES:SI    Pointer to command line

Figure 5

; WINDOWS SCREEN CAPTURE - DYNAMIC LIBRARY
;
; LANGUAGE : Microsoft Assembler 5.1
; SDK : Windows 2.03 SDK
; MODEL : small
; STATUS : operational
;
;

    Extrn    PrtScInit:Near

_TEXT    SEGMENT    BYTE PUBLIC 'CODE'
    ASSUME    CS:_TEXT
    PUBLIC    LibInit

LibInit    PROC    FAR

    Push    DI    ; hInstance
    Push    DS    ; Data Segment
    Push    CX    ; Heap Size
    Push    ES
    Push    SI    ; Command Line

    Call    PrtScInit

    Ret

LibInit    ENDP
_TEXT    ENDS

End    LibInit


Figure 6

#include <windows.h>
#include <string.h>
#include "prtsc.h"

/* global data */
BOOL    bMono;        /* Convert to monochrome?  */
WORD    wArea;        /* Current capture area    */
WORD    wColors;        /* Number of system colors */
HWND    hWndDOS;        /* Handle to DOS session   */
HANDLE    hInstance;        /* Library instance handle */
FARPROC    lpfnDOSWnd;        /* DOS session function    */
FARPROC    lpfnOldHook;        /* Old hook function       */

/* PrtScInit( hLibInst, wDataSegment, wHeapSize, lpszCmdLine ) : BOOL
    This function performs all the initialization necessary to use the
    screen capture dynamic-link library. It is assumed that no local heap is
    used; therefore there is no call to LocalInit. A nonzero value is
returned if the initialization is successful. */

BOOL PASCAL PrtScInit( hLibInst, wDataSegment, wHeapSize, lpszCmdLine )
HANDLE    hLibInst;
WORD    wDataSegment;
WORD    wHeapSize;
LPSTR    lpszCmdLine;

{
    extern BOOL    bMono;
    extern WORD    wArea;
    extern WORD    wColors;
    extern HWND    hWndDOS;
    extern HANDLE    hInstance;
    extern FARPROC    lpfnDOSWnd;
    extern FARPROC    lpfnNewHook;

    HDC    hDC;        /* Handle to temporary DC */
    HWND    hWndFocus;        /* Window that has focus  */
    char    szClassName[32];        /* Temporary class name   */

    /* Initialization - Alt = PrtSc active */
    bMono = FALSE;
    wArea = CAPTURE_WINDOW;
    hInstance = hLibInst;

    lpfnOldHook = SetWindowsHook( WH_KEYBOARD,(FARPROC)PrtScHook );

    hWndFocus = GetFocus();

    hDC = GetDC( hWndFocus );
    wColors = GetDeviceCaps( hDC, NUMCOLORS );
    ReleaseDC( hWndFocus, hDC );

■
■
■
}

/* PrtScFilterFn( hWnd, wMessage, wParam, lParam ) : LONG FAR PASCAL
   This window function processes all the messages received by the MS-DOS
   session window. When the user selects the Screen Capture... menu option
   this function displays the screen capture control panel. All other
   messages are passed on to the window without modification. */

LONG FAR PASCAL PrtScFilterFn( hWnd, wMessage, wParam, lParam )
HWND    hWnd;
WORD    wMessage;
WORD    wParam;
LONG    lParam;

{

    /* Trap appropriate messages */
    switch( wMessage )
        {
    case WM_COMMAND : if ( wParam == CMD_CAPTURE )
        {
            DialogBox( hInstance, "PrtSc", hWndDOS,
                    PrtScDlgFn );
            return( 0L );
        }
        break;
    case WM_DESTROY : /* Window being destroyed - unhook
            everything */
        if ( wArea )
        {
            wArea = CAPTURE_OFF;
            UnhookWindowsHook( WH_KEYBOARD,
                        (FARPROC)PrtScHook );
        }
        SetWindowLong( hWndDOS, GWL_WNDPROC,
                    (LONG)lpfnDOSWnd );
        break;
    default :
    break;
    }

    /* Pass message on to window */
    return(CallWindowProc(lpfnDOSWnd,
            hWndDOS,wMessage,wParam,lParam) );

}

/* PrtScDlgFn( hDlg, wMessage, wParam, lParam ) : BOOL;
   This function processes all the messages that relate to the PrtSc
   dialog box. This function inserts or removes the keyboard hook
   function, depending on the user's selection. */

BOOL FAR PASCAL PrtScDlgFn( hDlg, wMessage, wParam, lParam )
HWND    hDlg;
WORD    wMessage;
WORD    wParam;
LONG    lParam;

{

    switch( wMessage )
    {
    case WM_INITDIALOG :    /* Initialize dialog box */
        CheckDlgButton( hDlg, DLGSC_MONOCHROME, bMono );
        EnableWindow( GetDlgItem(hDlg,DLGSC_MONOCHROME),
                (wColors > 2) );
        CheckRadioButton( hDlg, DLGSC_OFF, DLGSC_SCREEN,
                DLGSC_OFF + wArea );
        break;
    case WM_COMMAND :    /* Window command */

        /* Process submessage */
        switch( wParam )
     {
        case DLGSC_OFF : /* Turn screen capture off */
            if ( wArea )
            {
                wArea = CAPTURE_OFF;
                UnhookWindowsHook( WH_KEYBOARD,
                            (FARPROC)PrtScHook );
                EnableWindow( GetDlgItem(hDlg,
                        DLGSC_MONOCHROME),FALSE );
            }
            CheckMenuItem( GetMenu(hWndDOS), CMD_CAPTURE,
                        MF_UNCHECKED );
            break;

        case DLGSC_CLIENT : /* Capture client area of active window */
        case DLGSC_WINDOW : /* Capture active window */
        case DLGSC_SCREEN : /* Capture entire screen */
            if ( !wArea )
            {
                lpfnOldHook = SetWindowsHook(WH_KEYBOARD,
                            (FARPROC)PrtScHook );
                EnableWindow( GetDlgItem(hDlg,
                        DLGSC_MONOCHROME),
                        (wColors > 2) );
            }
            wArea = wParam - DLGSC_OFF;
            CheckMenuItem( GetMenu(hWndDOS), CMD_CAPTURE,
                        MF_CHECKED );
            break;
            case DLGSC_MONOCHROME : /* Capture image in
                            monochrome */
            bMono = !bMono;
            break;

        case IDOK :
            EndDialog( hDlg, TRUE );
            break;
        default : /* ignore everything else */
            break;
        }

        break;
    default : /* message not processed */
        return( FALSE );
        break;
    }

    /* normal return */
    return( TRUE );

}


/* PrtScHook( nCode, wParam, lParam ) : WORD
   This function is called whenever the user presses any key. The Alt =
PrtSc
   key combination is trapped, and a bitmap copy of the desired portion of
   the screen is copied to the clipboard. The return value is FALSE if the
   message should be processed by Windows; the return value is TRUE if the
   message should be discarded. */

WORD FAR PASCAL PrtScHook( nCode, wParam, lParam )
int    nCode;
WORD    wParam;
LONG    lParam;


{
    extern BOOL    bMono;    /* Convert to monochrome? */
    extern WORD    wArea;    /* Area to capture */
    extern FARPROC    lpfnOldHook;    /* Old keyboard hook */
    WORD    uWidth;    /* Width of bitmap */
    WORD     uHeight;    /* Height of bitmap */
    WORD     wDiscard;    /* Return value */
    POINT    ptClient;    /* Client point */
    RECT    rcWindow;    /* Window rectangle */
    HDC    hScreenDC;    /* Handle to screen DC */
    HDC    hMemoryDC;    /* Handle to memory DC */
    HWND    hActiveWnd;    /* Handle to active window */
    HBITMAP    hOldBitmap;    /* Handle to old bitmap */
    HBITMAP    hMemoryBitmap;    /* Handle to memory bitmap */

    if (nCode == HC_ACTION)
    {
         /* This check traps the Alt = PrtSc key combination using bit
           29 for the Alt key and bit 31 for the key being released. */

    if ((wParam==VK_MULTIPLY) && ((lParam&0xA0000000)
                    ==0xA0000000))
    {

    ■
    ■
    ■
         case CAPTURE_WINDOW :
    /* Retrieve active window dimensions */
        GetWindowRect( hActiveWnd,&rcWindow );

        break;

    case CAPTURE_SCREEN :
       /* Retrieve dimensions of entire screen */
        rcWindow.top = 0;
        rcWindow.left = 0;
        rcWindow.right = SCREEN_WIDTH;
        rcWindow.bottom = SCREEN_HEIGHT;

            break;
    }

                   /*     Note that the window boundaries that need to be
        adjusted as part of the window may not be visible on
        the screen. This eliminates the problem of  a
        partially defined bitmap. */

        /* Adjust boundaries to screen clipping region */
            if (rcWindow.right > SCREEN_WIDTH)
            rcWindow.right = SCREEN_WIDTH;

            if (rcWindow.bottom > SCREEN_HEIGHT)
            rcWindow.bottom = SCREEN_HEIGHT;

            if (rcWindow.left < 0)
            rcWindow.left = 0;

            if (rcWindow.top < 0)
            rcWindow.top = 0;

        /* Compute display size of window */
            uWidth = rcWindow.right - rcWindow.left;
            uHeight = rcWindow.bottom - rcWindow.top;

        /* Open clipboard */
            if ( OpenClipboard(hActiveWnd) )
            {

        /* Empty clipboard */
            EmptyClipboard();

        /* Create screen DC & compatible memory DC's */
            hScreenDC = CreateDC( "DISPLAY", NULL, NULL,
                            NULL );
            hMemoryDC = hScreenDC ?
                                     CreateCompatibleDC((hScreenDC):NULL;

        /* Create color or monochrome bitmap */
            if (hMemoryDC && hScreenDC)
            {
            hMemoryBitmap = CreateCompatibleBitmap(bMono ?
                        hMemoryDC :hScreenDC,uWidth,
                        uHeight);
            } else
                hMemoryBitmap = NULL;

                if ( hMemoryBitmap )
                {

        /* Select bitmap & copy bits */
                    hOldBitmap = SelectObject
                                                   (hMemoryDC,
                                                    hMemoryBitmap);
                    BitBlt(
    /* Dest DC    */    hMemoryDC,
    /* x upper left    */    0,
    /* y upper left    */    0,
    /* Dest width    */    uWidth,
    /* dest height    */    uHeight,
    /* Source DC    */    hScreenDC,
    /* Source X upper left    */    rcWindow.left,
    /* Source Y upper left    */    rcWindow.top,
    /* ROP code    */    SRCCOPY);

                    SelectObject(hMemoryDC, hOldBitmap );
                    SetClipboardData(CF_BITMAP,
                            hMemoryBitmap);

            } else
                MSGBOX( hWndDOS,"Insufficient memory!");

                /* Close clipboard */
                CloseClipboard();

                /* Delete DCs */
                if ( hMemoryDC )
                DeleteDC( hMemoryDC );
                if ( hScreenDC )
                DeleteDC( hScreenDC );

                } else
                    MSGBOX( hWndDOS,
                                         "Unable to open clipboard!" );

                } else
                    MSGBOX( hWndDOS,
                "Active window is iconic!" );

                } else
                    wDiscard = FALSE;

                } else
                    wDiscard = (WORD)DefHookProc( nCode,
                            wParam,
                            lParam,
                            (FARPROCFAR *)
                            &lpfnOldHook);

                /* Return value */
                return( wDiscard );

                }


Figure 7

Ignore the message and block it

Ignore the message but pass it on

Handle the message and block it

Handle the message and pass it on


CHECKERS PART I  [Design Goals for Building a Complete Graphical
Application]

 Charles Petzold

How about a nice game of checkers? Soon you will be able to play a game of
checkers under OS/2 Presentation Manager. In the next few issues of MSJ we
will present a complete checkers program for Presentation Manager (hereafter
referred to as PM) called CHECKERS.EXE. You can play CHECKERS against
yourself, the program, a person across a network, or an external
dynamic-link library that implements a checkers-playing strategy.

Writing a game program is an excellent way to learn about a graphical user
interface, because games make use of graphics and are often highly
interactive. You can use this article and the following ones to create a fun
game that demonstrates many aspects of OS/2 and Presentation Manager
programming, including graphics, keyboard and mouse input, menus, dialog
boxes, and child windows. All too often the programs published in books and
magazines are very short. They show programming techniques in isolation.
These articles, on the other hand, will illustrate structure and
organization associated with real-world coding, including the use of
dynamic-link libraries (for the checkers-playing strategy), named pipes (for
playing the game over a network), and multiple threads. We'll look at
algorithms for working with logical structures that are common in games, and
perhaps even some object-oriented techniques that will help generalize
several components of the program. Because of its detail, CHECKERS will be
longer than any other program this magazine has published.

This article describes what I intend to put into the program and some of the
problems I expect to encounter. Code with the components of the program that
draws the checkerboard and playing pieces, as shown in Figure 1, will be
published in the next issue. Using the additional user interface code from a
future issue, you'll be able to play a game against yourself.

Modes of Play

CHECKERS will feature several basic modes of play available from a menu
option. You can:

■    Play against yourself, by alternately playing the black and white
pieces.

■    Play against the computer (the default). The logic of the
checkers-playing strategy will be implemented in a dynamic-link library.

■    Play against an alternative dynamic-link library. If you would like to
code your own checkers-playing strategy, you can create a dynamic-link
library and hook into it from CHECKERS.

■    Play one dynamic-link library against another. In this case, you just
sit back and watch the game.

■    Play against another person, running a copy of CHECKERS across a
network. This facility will use named pipes.

■    Re-create a previous game. CHECKERS will allow you to store a log of a
game (using standard checkers notation) as an ASCII file with the extension
CKR. The program will also allow you to load a CHR file, to re-create the
moves of the game at a speed you define.

Program Structure

Of course, all these options require that some serious consideration be
given to the program structure. The best approach seems to be controlling
games with a "supervisor" that I intend to implement as a Presentation
Manager object window. (Object windows are not visible on the screen, but
they can receive and send messages like other windows. You can use an object
window to implement object-oriented programming techniques through
Presentation Manager architecture rather than through the syntax of your
programming language.) The supervisor will be responsible for maintaining
the current board layout and keeping a log of the game.

As is usual, menu commands will be processed by the program's client window.
When you initiate a new game from the menu, the client window will inform
the supervisor that a new game has been requested and who the players will
be.

If the game is to be played from a log file, the supervisor will be
responsible for re-creating the game. Otherwise, each of the possible
players (the user, a dynamic-link library, or a named pipe connection) is
known to the supervisor as a window handle. Thus, for each game, the
supervisor has two window handles, one for the black player and one for the
white player.

Alternating between black and white, the supervisor will use a message to
notify a player window when it should make a move. Depending on who the
players are, these player windows can get information about a move from the
user, a dynamic-link library, or a named pipe.

The player windows will inform the supervisor when a move has been
completed. The supervisor can then notify the other player window about the
move and request that that window make a move. The supervisor will also be
responsible for determining when a player has won a game, and perhaps even
for determining when a game has ended in a draw.

The general program structure (as I conceive it now) is shown in Figure 2.
The client window and the user window are normal Presentation Manager
windows. The supervisor window, dynamic-link library access window, and
named pipe window are object windows.

Suppose that you're playing black and that your opponent is a dynamic-link
library playing white. In this case, the supervisor window has two window
handles: black is the handle of the user window and white is the handle of
the dynamic-link library window. The supervisor sends the user window a
message telling it to make a move. The user window displays an appropriate
mouse pointer and waits for you to make a move. When the move is completed,
the user window informs the supervisor window of the move. The supervisor
then sends a message to the dynamic-link library window requesting a move.
When the dynamic-link library determines what the move should be, it sends a
message back to the supervisor window. The supervisor informs the user
window of this move so that the display can be updated. The supervisor then
sends a message to the user window asking for the next move.

When one of the players is a dynamic-link library or another person over a
named pipe, multiple threading will be required while the supervisor window
is awaiting word of the next move. The dynamic-link library access window
and the named pipe windows will be responsible for creating these threads.

Notation/Representation

I mentioned earlier that CHECKERS will be capable of storing a log of the
game in an ASCII file. This log will use standard checkers notation, which
is shown in Figure 3. The black squares are simply numbered 1 through 32.
The numbering makes more sense if you turn the board around so that black is
on top, as is usually done when showing checkerboard layouts in books.
(Figure 3 is shown with black on the bottom to be consistent with Figure 1.)

A game can be represented by showing each move with the starting and ending
square numbers separated by a dash. Here's the beginning of a game with two
jumps in the first four moves:

Black: 10-15
White: 24-19
Black: 15-24
White: 28-19

There are only 32 squares on which pieces can reside, making it very
convenient to represent the board in a C program. The number of black
squares corresponds to the minimum number of bits in a long integer, as
required by the ANSI C standard. By simply subtracting 1 from each square
number, the board positions can correspond to the bits of a long integer,
where 0 is the least significant bit and 31 is the most significant bit.

Such a representation is discussed in Christopher S. Strachey's paper
"Logical or Nonmathematical Programs" originally published in Proceedings of
the Association for Computing Machinery Conference, Toronto (1952, pp.
46-49) and reprinted in Computer Games I, edited by David N.L. Levy
(Springer-Verlag, 1988). Although Strachey's notation is pretty obvious once
you start working with it, it's probably not something I would have stumbled
upon myself.

At any time during a game, the board can be represented using three 32-bit
integers: white (W), black (B), and king (K). For example, at the beginning
of a game, the three integers have the following values:

B = 00000FFFH
W = FFF00000H
K = 00000000H

There's a little redundancy in this notation because each square requires 3
bits (one in each of the integers) for a total of 8 different states. In
reality, each square can be only one of 5 states (empty, black, white, black
king, and white king), but the notation is so convenient that we can ignore
the waste.

You can apply bitwise operations to these integers to derive some other
characteristics of the board. For example, the squares on which black kings
currently reside can be represented in C notation as:

B & K

You can determine all the empty squares (E) using:

E = ~B & ~W

As we'll see, this type of logic is very important in the CHECKERS program,
both in determining whether a user is making a legal move and in determining
all possible moves a piece can make.

For example, there are 9 positions on the board where an unkinged black
piece can move from a square position n to a square position (n+3) without
jumping an opponent's piece. You can represent these positions with a
variable called M:

M = 00E00E00E0H

To calculate the current unkinged black pieces that might be able to move to
squares with numbers that are 3 higher than their current position, use the
following formula:

B & M

To calculate the destinations of these pieces you can simply shift left by 3
bits:

(B & M) << 3

But it's only possible to make these moves if the destination square is
empty:

((B & M) << 3) & E

Shifting this expression to the right by 3 bits gives us a 32-bit integer
that describes the position of all the current unkinged black pieces that
can move to a square 3 higher in number:

(((B & M) << 3) & E) >> 3

White kings can make these same moves:

(((W & K & M) << 3) & E) >> 3

Of course, this gets more complex when you take into account the pieces that
can move to squares that are 4 higher or 5 higher in number, and when
considering the possible jumps. But the concepts remain the same. It is an
extremely useful type of representation that avoids otherwise lengthy logic.

Visuals and Interaction

As you can see in Figure 1, CHECKERS will feature a checkerboard in its
client window. The window that draws the board and receives keyboard and
mouse input will be a child of the client window. The supervisor
communicates with this child window and two others to request information
about moves and to inform the window about moves made by the opponent. The
board's colors may be changed using a menu option. The checkerboard looks
three-dimensional and is sized to fit within the client window while
maintaining the correct aspect ratio.

To move the pieces, you can use either the mouse or the keyboard. Using the
mouse you'll be able to pick up a piece from a square and move it to another
square. The easiest way to do this would be to have predefined mouse
pointers that look like the pieces. But the pieces will not generally be the
same size as a mouse pointer, so PM's normal pointer logic can't be used.
Instead, you'll move bitmaps around the window. CHECKERS will also have a
keyboard interface. You can move the mouse pointer to a square using the
cursor movement keys, pick up a piece by pressing the space bar, move the
piece to another square, and set it down using the space bar again.

CHECKERS will prevent a user from making an illegal move and require that
the user make a jump when one is available and continue jumping until no
more jumps are possible. This logic may also be implemented in a
Presentation Manager object window.

Playing Strategy

Playing a game against the program is the feature many users will find most
appealing about CHECKERS. This feature requires that the program include a
reasonable checkers-playing strategy. This strategy will be as simple as
possible, because PM programming is difficult enough in itself.

The checkers-playing strategy will look ahead through all possible moves,
counter-moves, counter-counter-moves, and so forth (to a level that I'll
have to determine empirically), and determine the best move by calculating a
simple score, where an unkinged piece is counted as 1 and a kinged piece is
counted as a number somewhere between 1 and 2.

My standards are very low: I want the CHECKERS program to beat me (or come
to a draw) most of the time, and that should not be difficult at all! I
claim no skill in playing checkers. I am not interested in developing the
best possible checkers-playing strategy right now. However, CHECKERS will
provide a framework for others who are interested in this aspect of the
program. Because the checkers-playing strategy will be implemented in a
dynamic-link library, readers of MSJ can develop their own checkers-playing
modules that CHECKERS can easily access. The interface to this dynamic-link
library will be documented in a future issue. The game will also be able to
play two dynamic-link libraries against each other, so we could conceivably
we can stage checkers tournaments where the humans would watch while the
programs played.

Now it's time for this programmer to stock up on necessary beverages, lock
himself in his room, disconnect the phone, post a note on his door saying
"E-mail only," and get to work. I'll emerge in time for the next issue to
discuss the code that draws the checkerboard and the playing pieces.

The Rules of the Game

The game of checkers (called draughts in Great Britain) has obscure
beginnings. It possibly originated in the 1500s as a merging of chess and
the Spanish game Alquerque de doze, but it may have come from the Orient,
related to Parcheesi and tic-tac-toe.

Checkers is played on a chess board, which is an 8-by-8 grid of alternating
black and white squares. The squares are not always black and white--the
black squares may be any dark color and the white squares any light
color--but the squares are referred to as black and white regardless of
their actual colors. The official colors in US tournaments are green and
buff. I decided to use green and light gray for the default square colors,
as shown in Figure 1.

The two players sit on opposite sides of the board. The board is oriented so
that the edge of the board closest to each player has a black square on the
left and a white square on the right.

The game is played with 12 black pieces and 12 white pieces. (Again, these
are the colors used to refer to the pieces. The actual colors may be
different. Some commercial games of checkers use black and red pieces, but
in tournament play the pieces are usually red and white.) One player
controls the black pieces and the other controls the white pieces.

At the beginning of the game, the pieces are arranged on the 12 black
squares closest to each player, as shown in Figure 1. The players alternate
turns, with black moving first.

Initially, pieces can move forward (that is, towards the opponent's side of
the board) and to a diagonally adjacent, unoccupied black square. If a
diagonally adjacent black square is occupied by an opponent's piece, and the
black square beyond that one is unoccupied, a player must move his or her
piece to the unoccupied square. The opponent's piece is jumped (or captured)
and removed from the board. The move must continue with additional jumps if
they are available.

Here's where the rules get controversial. Many players observe the tradition
of "huffing." If a jump is available and a player does not take the jump,
the opponent can require that the player take back the move, or that the
piece that did not take the jump be forfeited and removed from the board.
However, most contemporary rulebooks take a different approach: if a jump is
available, a player must take it, and must continue jumping opponent's
pieces until no longer able to do so. This is the rule I will impose in the
Presentation Manager CHECKERS program. There will be no huffing.

When a piece reaches the opponent's edge of the board, it is kinged or
crowned. This is indicated by placing another piece of the same color on top
of the piece. When a piece is first kinged, its move ends even if it can
continue jumping. In subsequent moves, kings can move forward or backward
along the diagonals.

A player wins after capturing all the opponent's pieces or when the opponent
can no longer move any piece. Many games between two good players end in
draws. Usually a draw must be decided by a referee based on the inability of
either player to gain any advantage after 40 or 50 moves.

There are some variations of the game. Some versions of checkers allow kings
to make long jumps, passing unoccupied black squares on a diagonal and
landing on the square beyond an opponent's piece. Sometimes (particularly in
Russia), the game is played on a 10-by-10 board. The PM CHECKERS program
will play the standard game only.


SpyGlass: A Utility for Fine Tuning the Pixels in a Graphics Application

 Kevin P. Welch

One of the most time-consuming aspects of programming in the Microsoft
Windows environment  is getting things to look just right on the screen.
Invariably (it seems) any user interface change creates alignment problems
requiring several tweaks of one or two pixels before correct alignment is
restored. Although such off-by-one errors are easy to see on a coarse,
low-resolution display, they become a little more difficult when using a
high-quality one with resolution in excess of 100 pixels per inch.

SpyGlass is a BLOWUP.EXE inspired utility (see "BLOWUP: A Windows Utility
for Viewing and Manipulating Bitmaps," MSJ Vol. 2 No. 3) that enables you to
enlarge selected portions of your display dynamically while maintaining a
constant pixel-to-aspect ratio. In addition, it serves as a simple
demonstration of several subtle graphics device interface (GDI) programming
techniques that might be of use in your own applications. And although
SpyGlass won't eliminate off-by-one errors, it will make them a little
easier to find when tuning your application.

Using SpyGlass

To use SpyGlass, click inside the window client area with the left mouse
button. A small rectangle (in proportion to the SpyGlass window) will appear
in place of the mouse cursor. Then, while the left button is depressed and
you drag the mouse around, the rectangle acts like a cursor. It becomes a
magnifying glass, dynamically magnifying whatever portion of the display it
covers, thereby enlarging the images in the SpyGlass window.

If you click the right button while dragging the mouse, the screen image
enclosed by the magnifying glass will be enlarged inside the SpyGlass
window. You can click the right button as many times as you like, taking
pictures of various portions of the display (see Figure 1). If you hold down
the right button while dragging the mouse, a continuous series of
enlargements is produced inside the viewport. The enlargements may appear a
little jerky on an 80286-based computer, but they look quite nice on an
80386 machine--especially one with a hardware graphics coprocessor.

Releasing the left button when you finish dragging the mouse will copy the
final image from the SpyGlass window to the clipboard. If you want to
capture different sized images, you can change the magnifying glass
proportions by adjusting the SpyGlass window dimensions or by selecting a
different enlargement factor from the application's system pull-down menu.

Coordinate Systems

Each Windows application maintains its own coordinate system. SpyGlass is no
exception. But unlike most other Windows applications, SpyGlass performs all
its work in screen or display coordinates.

The origin for most application coordinate systems is defined as the
upper-left corner of the client area. Although this is normally adequate
when you work within your own window, it is insufficient for SpyGlass since
the magnifying glass can roam all over the display. Therefore, both the
magnifying glass and the viewport area are defined in terms of screen or
display coordinates (DC). This enables SpyGlass to use the screen display
context or DC when performing each enlargement.

As mentioned previously, the size of the magnifying glass is determined by
the dimensions of the SpyGlass client area, adjusted by the currently
selected magnification or enlargement factor. When the zoom factor is one,
the interior portion of the magnifying glass is the same size as the
SpyGlass client area. As you increase the enlargement ratio, the magnifying
glass becomes proportionally smaller (see Figure 2).

Although the current implementation of SpyGlass supports only a small number
of enlargement ratios, you could easily change the source code to include a
wider range. If you experiment with other values, be aware that SpyGlass
will be most efficient when using powers of two for enlargement factors.
This is because the underlying StretchBlt function operates best when
doubling or quadrupling each pixel size instead of performing many
fractional enlargements. If you get even more adventurous, you could
experiment with values less than one.

Understanding SpyGlass

To build SpyGlass, you will need the Microsoft Windows Version 2.1 Software
Development Toolkit (SDK) and a Microsoft C Compiler (Version 5.0 or later).
Before constructing the program with the MAKE SPYGLASS command, you will
need the files listed in Figure 3. (The files  are available for downloading
from any of the MSJ bulletin boards--Ed.)

Structurally, SpyGlass is a relatively simple program, acting in most
situations like a bitmap clipboard viewer. Internally, SpyGlass consists of
only two functions--a main procedure that defines the SpyGlass window and
retrieves or dispatches all application messages, and a function that
processes all window-related messages.

 Throughout the source code, note the use of the macros defined in
SPYGLASS.H . The WIDTH and HEIGHT macros (see Figure 4), for example,
compute the corresponding width and height of a given rectangle,
respectively. Both use a feature of the C compiler preprocessor called token
pasting. To see how token pasting works, note that each instance of the
variable x is preceded by a double number sign (##). When the ## is
interpreted by the preprocessor, it allows tokens to be used as actual
arguments that can be concatenated to form other tokens. Without this
replacement capability, you would not be able to access the individual
structure elements of a token within a macro.

As is the case with most Windows applications, the SpyGlass window message
processing function is the heart of the program. From an operational
perspective, four major events are of interest; they are listed in Figure 5
and discussed below.

SpyGlass is activated when the left mouse button is depressed inside the
window client area. This causes a WM_LBUTTONDOWN message to be sent to the
application. Since it is possible to receive extraneous mouse messages, a
check is made to see if the window is in an inactive state. If it is, mouse
capture is enabled via a SetCapture function call. This causes the next
important event: notifying the window of all subsequent mouse movements via
a series of WM_MOUSEMOVE messages.

Mouse movements are captured by SpyglassWndFn. Since SpyGlassWndFn is by
nature reentrant (or called before it returns), a Boolean flag is set to
indicate the active window state. This allows the function to screen out
unwanted messages, acting only on those of immediate interest.

Besides capturing mouse movements, SpyGlassWndFn defines both the viewport
and the magnifying glass rectangles in screen coordinates. The viewport
region is calculated by retrieving the window client area (in client
coordinates) and converting them to screen coordinates. The magnifying glass
region is also based on the size of the window client area, but it is
adjusted using the selected enlargement factor and by aligning the origin
with the current mouse position. The resulting rectangle is then used to
replace the normal mouse cursor.

Note how the magnifying glass dimensions are enlarged by one pixel in each
direction. This allows the program to work with the interior of the
rectangle without erasing the inverted line around the border. The resulting
visual effect is considerably smoother and eliminates unnecessary redrawing.

When the magnifying glass rectangle is tied to the mouse and WM_MOUSEMOVE
messages are received, the rectangle is hidden and then redrawn in the new
mouse position. Before processing the message, the function checks whether
the capture flag is set. This check allows it to distinguish the message
from those mouse messages received when capture is inactive.

Operationally, the mouse movement messages are handled in two different
ways. If the magnifying glass intersects the SpyGlass window, it is redrawn
after the viewport has been updated. This additional delay (associated with
the overhead of transferring the screen image from the magnifying glass to
the viewport) causes the magnifying glass to flicker and appear
unresponsive. If the magnifying glass does not intersect the SpyGlass
window, it is immediately repositioned, followed by the transfer of the
screen image to the viewport (see Figure 6).

An earlier version of SpyGlass did not attempt to take advantage of those
two situations. Instead, it only redrew the magnifying glass after updating
the viewport. But several users commented on the unresponsive magnifying
glass, prompting this algorithmic change and resulting additional complexity
to the program. Unfortunately, this seems to be the case with many other
user-interface issues: in theory they are simple, but they actually require
considerably more tuning than one might expect.

One situation still ignored by SpyGlass is when the magnifying glass extends
beyond the borders of the display. Currently the results produced are highly
dependent on the characteristics of the active display driver. Some drivers
automatically erase areas outside the screen, others leave them as they
were, and still others leave them undefined. Typically, issues such as this
only surface during the last few weeks of beta testing and sometimes they
stay unresolved indefinitely.

Another event of interest occurs when the right mouse button is depressed.
This sends a WM_RBUTTONDOWN message to SpyGlassWndFn. If the magnifying
glass is active, the selected portion of the screen is enlarged in the
viewport. As with the WM_MOUSEMOVE message, the magnifying glass is hidden
if it intersects with the SpyGlass client area. Note how the BitBlt function
is used in place of StretchBlt to transfer the screen image when the
enlargement factor is one. This is because the BitBlt function is inherently
faster than StretchBlt, especially when implemented by the device driver at
the hardware level.

The final event of interest occurs when the left mouse button is released at
the end of a drag operation. The resulting WM_LBUTTONUP message causes the
mouse capture to be released, the magnifying glass to be erased, and the
original cursor to be restored. In addition, the final viewport image is
copied to a bitmap, which is subsequently transferred to the clipboard.

One critical step of clipboard data management should be emphasized. This
involves the handling of the memory bitmap containing the viewport image.
Note how this bitmap is unselected from the memory display context before
being placed on the clipboard. Bitmaps that are transferred to the clipboard
while they are selected in a display context are usually inaccessible to
members of the clipboard viewer chain, potentially causing some unusual data
management problems.

You should know that calling SetClipboardData causes the immediate
transmission of a WM_DRAWCLIPBOARD message down the clipboard viewer chain.
Since SpyGlass is a member of the viewer chain, the capture flag is not
reset until SetClipboardData returns. This eliminates an extra update of the
SpyGlass client area; an update that would be visible to the user.

Variations

Once you understand the inner workings of SpyGlass, you might want to try
some experiments with the program. One interesting effect can be achieved
when you set the enlargement factor to one and capture recursive images of
SpyGlass itself. The resulting display is very much like pointing a
high-quality video camera at its own output monitor (see Figure 7).

If you are a little more adventurous, you can change SpyGlass, disabling
some of the subtleties discussed earlier with a few well-placed comments. If
you try this, it will be clear that the additional complexity does make a
difference and transforms a good implementation into a great one.

If you feel even more intrepid, you can try converting SpyGlass to run under
OS/2 Presentation Manager (hereafter PM). In addition to the normal Windows
to PM conversion tasks, you will have to pay attention to a few important
details.

The first of these details involves the way in which PM handles menus. When
you convert SpyGlass you will have to retrieve the handle to the submenu in
the zero position of the system menu explicitly before you can attach and
manipulate the various enlargement commands. Second, instead of creating a
new display context for the DISPLAY device, you will be able to retrieve a
handle to the screen presentation space directly. And when working with this
presentation space, you will be able to use a single GpiBitBlt function call
in place of the Windows BitBlt and StretchBlt equivalents.

Third, since PM doesn't support the clipboard viewer chain concept, you will
have to remove all the code that relates to the clipboard viewer chain. OS/2
systems contain provisions for only one active clipboard viewer. Finally,
when you try to place the final bitmap image on the clipboard, you will have
to go through a complicated process of retrieving a memory display context
(for further information, refer to the SNAP.EXE source code distributed with
the Microsoft OS/2 Software Development Kit Version 1.1).

The end result of your changes to SpyGlass will be a program that acts much
the same in both Windows and PM. The same underlying design and tuning
principles used with the Windows version apply equally well to PM. In
addition, these enhancements can also be applied in your own applications.
Although these subtle refinements are easy to perform with SpyGlass,
performing such magic with a large application can be extremely
time-consuming. But the results are well worth the extra effort.

Figure 3:

SPYGLASS    Make file for application
SPYGLASS.DEF    Module definition file
SPYGLASS.H    Header file
SPYGLASS.RC    Application resource file
SPYGLASS.C    Source code
SPYGLASS.ICO    Icon referenced by resource file

Figure 4

#define    WIDTH(x)    (##x.right - ##x.left)
#define    HEIGHT(x)    (##x.bottom - ##x.top)

Figure 5

Message    Response
WM_LBUTTONDOWN    Mouse capture activated
WM_RBUTTONDOWN    Selected portion of screen enlarged
WM_MOUSEMOVE    Magnifying glass moved
WM_LBUTTONUP    Mouse capture deactivated