PCjs Machines

Home of the original IBM PC emulator for browsers.


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

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

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.


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 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

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 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

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

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

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
          │                ▓▓▓▒▒░░░▒▒▓▓▓
                           ▓▓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
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.


Figure 8B: Sample Microsoft Excel DDE command.


Figure 10:  Charting and analysis features of QuotChart

volume histogram
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

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

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

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

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

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,

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,

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

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

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

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);


  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

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'

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?


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

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

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

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

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

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

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


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

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

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

  ■  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

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

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

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

  ■  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

  while (! done)
      everyEvent, &theEvent))
    switch (theEvent.what)
       case mouseDown:
       case updateEvt:

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

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

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);
       case WM_CREATE:
         doCreate(hWnd, wParam, lParam);

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

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

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

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

  ■  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'

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)
    switch (theAppEvent.event_type)
  case CMD_EVENT:
    doCommand( &theAppEvent);
    doMouseDown( &theAppEvent);

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

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;
  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 *)
    ptr->first_field = some_value;
    ptr->second_field = another_value;

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

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-

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

  ■  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)

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

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

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"),

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

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

  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

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

There are several practical ramifications of this particular MS-Windows

  ■  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);

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

    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);

Figure 3:  Take One of Windows Version

/* from HELLO.C */
     case WM_PAINT:
          BeginPaint(hWnd, (LPPAINTSTRUCT) &ps);
#ifdef OLD_CODE
          StopSign(hWnd, ps.hdc);
          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;
    scale = 1;
    SetMapMode(hDC, MM_ISOTROPIC);
    SetWindowExt(hDC, 18, 18);
    SetViewportExt(hDC, r.right, r.bottom);

Figure 6:  Take Three of Windows Version

/* from HELLO.C */

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

/* 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))


/* 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);
        BeginPaint( hWnd, (LPPAINTSTRUCT)&ps );
        EndPaint( hWnd, (LPPAINTSTRUCT)&ps );

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


/* 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);


/* 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  /**/
typedef unsigned short WINDOW;  /* HWND */
typedef unsigned short HPIC;    /* HANDLE */

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_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"
#include <memory.h>
#include "windows.h"

#include "canvas.h"

typedef struct {
    WINDOW window;
    short x,y;
    HPIC pic;
#ifdef MAC
    WindowRecord wRecord;
    short scale;
    PAINTSTRUCT ps;             /* includes HDC */
    short right,bottom;
    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)
#define CANVAS_PIC(hCanvas)       (canvas[hCanvas].pic)
#ifdef MAC
#define CANVAS_GP(hCanvas)        ((GrafPtr)
#define CANVAS_WP(hCanvas)        ((WindowPtr)
#define CANVAS_SCALE(hCanvas)     (canvas[hCanvas].scale)
#define CANVAS_PORTRECT(hCanvas)  (CANVAS_WP(hCanvas)-
#define CANVAS_PAINTSTR(hCanvas)  (canvas[hCanvas].ps)
#define CANVAS_HDC(hCanvas)
#define CANVAS_HWND(hCanvas)      ((HWND)

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);

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 */

/* 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 */

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

void kill_canvas(hCanvas)
    HCANVAS 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);                                  \
#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));      \

void move(hCanvas, x, y)
    HCANVAS hCanvas;
    short x,y;
#ifdef MAC
    MAYBE_SWITCHING_PORT(hCanvas, Move, x, y);
    UPDATING_XY(hCanvas, MoveTo, +=, x, y);

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

void move_to(hCanvas, x, y)
    HCANVAS hCanvas;
    short x,y;
#ifdef MAC
    UPDATING_XY(hCanvas, MoveTo, =, x, y);

void line_to(hCanvas, x, y)
    HCANVAS hCanvas;
    short x,y;
#ifdef MAC
    UPDATING_XY(hCanvas, LineTo, =, x, y);

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

    *x = CANVAS_GP(hCanvas)->pnLoc.h *
    *y = CANVAS_GP(hCanvas)->pnLoc.v *
    *x = CANVAS_X(hCanvas);
    *y = CANVAS_Y(hCanvas);

#ifdef MAC
#define SCROLL_BAR_WIDTH        16

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

void get_size(hCanvas, x, y)
    HCANVAS hCanvas;
    short *x, *y;
#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;
    *x = CANVAS_RIGHT(hCanvas);
    *y = CANVAS_BOTTOM(hCanvas);

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

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

    if (! CANVAS_HDC(hCanvas))
    SetMapMode(CANVAS_HDC(hCanvas), MM_ISOTROPIC);
    SetWindowExt(CANVAS_HDC(hCanvas), units, units);
    SetViewportExt(CANVAS_HDC(hCanvas), x, y);

void begin_update(hCanvas)
    HCANVAS hCanvas;
#ifdef MAC
    BeginPaint(CANVAS_HWND(hCanvas), &CANVAS_PAINTSTR(hCanvas));
    /* CANVAS_HDC() is set via CANVAS_PAINTSTR() */

void end_update(hCanvas)
    HCANVAS hCanvas;
#ifdef MAC
    EndPaint(CANVAS_HWND(hCanvas), &CANVAS_PAINTSTR(hCanvas));
    CANVAS_HDC(hCanvas) = 0;        /* important! */

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

void close_picture(hCanvas, hPic)
    HCANVAS hCanvas;
    HPIC *hPic;
#ifdef MAC
    *hPic = CANVAS_PIC(hCanvas);
    *hPic = CloseMetaFile(CANVAS_HDC(hCanvas));
    CANVAS_HDC(hCanvas) = CANVAS_PIC(hCanvas);     /* restore old DC */

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

void kill_picture(hPic)
    HPIC hPic;
#ifdef MAC


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

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);
    MessageBox(GetActiveWindow(), buf, "Debug!", MB_OK);

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,
                            (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));
        if (closed_canvases == NUM_CANVASES)

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

void StopSign(HCANVAS hCanvas)
    if (! hPic)
        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()
    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


Figure 13:  Environment-independent memory allocation functions from

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

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

VOID        env_ReAllocMem(handle,size)
MEMBLOCK *  handle;
MEMSIZE     size;
#ifdef MAC
    *handle = GlobalReAlloc(*handle,size,GMEM_MOVEABLE);

MEMPTR      env_GraspMem(handle)
MEMBLOCK    handle;
#ifdef MAC
    return (MEMPTR) *handle;
    return (MEMPTR) GlobalLock(handle);

VOID        env_UnGraspMem(handle)
MEMBLOCK    handle;
#ifdef MAC
    HUnlock((Handle) handle);
    GlobalUnlock((HANDLE) handle);

VOID        env_FreeMem(handle)
MEMBLOCK    handle;
#ifdef MAC
    DisposHandle((Handle) handle);
    GlobalFree((HANDLE) handle);

MEMSIZE     env_CompactMem(size_wanted)
MEMSIZE     size_wanted;
#ifdef MAC
    return (MEMSIZE) CompactMem(size_wanted);
    return (MEMSIZE) GlobalCompact(size_wanted);

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 *

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

        /* 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 */

        /* 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 */

        /* 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  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  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

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
  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 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

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

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

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

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.


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

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 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.)


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

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

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

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

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

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

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

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

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.


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

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

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

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.


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

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

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

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.


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

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

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

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

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

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

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:


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:


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


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.


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

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.


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.


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_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? */

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 );

/* Application main program. */

VOID main()
    QMSG        qmsg;

    /* Initialize application, quit if any errors */

    if( ! SleuthInit() )

    /* Main message processing loop */

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

    /* Application termination */


/* 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 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;

    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 */


    /* 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 */


    if( ! pInfoBase )
      pInfoBase = malloc( sizeof(INFO) );
      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" );
      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 = WinQueryWindow( hwnd, QW_NEXT, FALSE ) )
       SleuthGetWindowTree( hwndChild, sLevel+1 );

/* Initialize the application. */

BOOL SleuthInit()
    HDC         hps;
    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 |
        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 */

          "%08lX [%04X] {%s} (%d,%d;%d,%d) %s%s%s",
          SleuthClassName( pInfo->szClass ),
          szQuote, pInfo->szText, szQuote

      /* Increment to next INFO entry */

    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!                                                  */

VOID CDECL SleuthPaint1( szFormat )
VOID CDECL SleuthPaint1( szFormat, ... )
    CHAR *      szFormat;
    va_list     pArgs;
    CHAR        szBuf[160];

    va_start( pArgs, szFormat );

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

    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;
      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;

      case SB_LINEDOWN:
        sDiff = 1;

      case SB_PAGEUP:
        sDiff = -sPageSize;

      case SB_PAGEDOWN:
        sDiff = sPageSize;

        sDiff = sPos - sOldPos;

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

      case SBX_BOTTOM:
        sDiff = 30000;

        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 );

      msg == WM_HSCROLL ?  -sDiff*sCharX : 0,
      msg == WM_VSCROLL ?   sDiff*sCharY : 0,

    /* 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! */


      /* 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:
            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:
          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 */


    /* 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 );
    FlushEvents( everyEvent, 0 );
    SetEventMask( everyEvent );
    InitDialogs( 0L );

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


    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;

    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 =
        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 =
        NULL, WC_DESKTOP, _szNull,
        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

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;
      if( ! hwndParent  ||  hwndParent == HWND_DESKTOP )
        hwndParent = _hwndDesktop;
      if( hwndParent == _hwndDesktop )
        ucKind = WK_MAIN;
        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 );

      /* 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;
          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);
          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;


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

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

    /* 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;
      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 =
          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;

    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 */

        (**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) );
          (**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;
          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;
          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 );
          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 );
          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,
                         SHORT2FROMMP(mp2) );
            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' );


    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;

      case inDesk:

      case inDrag:
        fid = FID_TITLEBAR;

      case inGoAway:
        fid = FID_SYSMENU;

      case inGrow:
        fid = FID_SIZEBORDER;

      case inMenuBar:
        hwnd = _hwndMenu;
        if( ! hwnd )
          return FALSE;

      case inSysWindow:
        SystemClick( pEvent, pwin );
        return FALSE;

      case inZoomIn:
      case inZoomOut:
        usHitHi = sArea;
        fid = FID_MINMAX;

        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

    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 );
      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;
    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

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

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

ICOM Simulations, Inc.
648 S. Wheeling Road
Suite 10
Wheeling, IL 60090
(312) 520-4440

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

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

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

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

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

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

  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

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

HARDERR.EXE is the OS/2 critical error handler.

PMSHELL.EXE is the Presentation Manager Task Manager (for OS/2 Version

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

  ■  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.


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-

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:


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
  c: && cd\ha && ha

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

                          │                                  │          │
                                                           SPOOL     (other)
   │          │           │           │          │
  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




 RUN=c:\cmd.exe /c c:\os2\pbin\mestart.cmd

 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

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

  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.


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.


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

  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.


For batch file usage, this command restores the current working directory
and environment to the one saved by a previous SETLOCAL command.


This command defines an alternative command processor for use in processing
a batch file.


Specifies an alternative keyboard layout for users outside of the United


For batch file usage, this command preserves the current drive, directory,
and environment for later restoration by ENDLOCAL during a batch file run.


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.


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

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:


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

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

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:

  shell=c:\command.com /P /E:500
  protshell=c:\shell.exe cmd.exe /k c:\os2init.cmd
  run=c:\os2\pbin\spool.exe c:\os2\spool /o:lpt1
  run=c:\cmd.exe /c c:\cset

Sample 2:

  REM CONFIG.OS2 FOR OS/2 v.1.1 and PM
  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
  set path=c:\os2;c:\os2\bin;c:\os2\pbin

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

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

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

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

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

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

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

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

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
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

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] ;
     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

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) ;



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,

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

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

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 ;
     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

■    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

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

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

If the current area filling pattern is PATSYM_SOLID, the code

GpiBeginPath(hps, idPath);
GpiCharStringAt(hps, &ptl,
             cch, szText);

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);
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

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,
GpiStrokePath(hps, ID_PATH,

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

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

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

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

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
    Value = X

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

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

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 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

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:

AltKey = PEEK(&H417) AND 8
    "the Alt key is depressed"

By using a dedicated function, you can replace all that code with a single
statement like:

    "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

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

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

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

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

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

Drive$ = "C"
Dir$ = GetDir$(Drive$)


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

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:

DECLARE SUB WordWrap (X$, Wide)


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$
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
     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
          IF POS(0) > 1 THEN PRINT  'if cursor didn't wrap next line
              EXIT FOR                  'done with this block
           END IF
    LOOP WHILE Pointer% < Length%       'loop until done


Sample 2:

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

INPUT "Enter a foreground color value (0 to 31): ", FG
INPUT "Enter a background color value (0 to 7) : ", 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

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:

1    main()
2    {
3    struct {
4        int a;
5    } s = { 1 };
6    }

1    main()
2    {
3    static struct {
4        int a;
5    } s = { 1 };
6    }

1    main()
2    {
3    struct {
4        int a;
5    } s1, s2;
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    };
6    struct s foo;
7    struct s *pfoo = &foo;
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;
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)-\
3    main()
4    {
5        struct chara {
6        char    chara;
7    };
9    struct charab {
10        char    chara;
11        char    charb;
12    };
14    struct huh {
15        char    chara;
16        int     intb;
17    };
19    struct huh2 {
20        int     intb;
21        char    chara;
22    };
24    printf("%d\n", sizeof(char));
25    printf("%d\n", sizeof(int));
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));
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:


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

■    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

■    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 */
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;
15    convert.segoff.segment = token; /* get segment number */
16    convert.segoff.offset = 0;    /* offset is zero within the
                                           segment */
18    return (convert.ptr);    /* assume segment &
                    offset overlay ptr
                                         perfectly */
19    }
21    main()
22    {
23    int token = getmemory(); /* RSX memory call */
24    char    *p = toktoptr(token);
26    printf("%d\n", p);
27    }

Figure 6b:

1    #define getmemory()    20 /* e.g. simulate RSX call */
3    char    *
4    toktoptr(token)
5    short   token;
6    {
7    return ((char *)(((long)token) <<
            (sizeof(short) * 8)));
8    }
10    main()
11    {
12    int token = getmemory(); /* RSX memory call */
13    char    *p = toktoptr(token);
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

Figure 7:

1    #include <stdio.h>
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    };
18    struct genericrecord {
19    int     recordtype;
20    union codedrecords cr;
21    };
23    struct genericrecord gr;
25    main()
26    {
27    /* ... */
29    switch(genericrecord.recordtype) {
30    case TYPE1:
31        /* code that uses genericrecord.cr.type1.? */
32        break;
34    case TYPE2:
35        /* code that uses genericrecord.cr.type2.? */
36        break;
38    case TYPE3:
39        /* code that uses genericrecord.cr.type3.? */
40        break;
42    default:
43        fprintf(stderr, "Invalid Record Type!!!");
44        exit (1);
45        break;
46    }
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

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

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

Figure 8:

1    #define v int    /* don't use void in this example since
                    it's a keyword */
3    main()
4    {
5    int x;
6    v   y;
8    x = y;
9    }

1    typedef int v;    /* don't use void in this example since
                    it's a keyword */
3    main()
4    {
5    int x;
6    v   y;
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

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,

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

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

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

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


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

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

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, 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

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

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)

     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);

    /* 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

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:

          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:

         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:


; 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

  "^o", PW_FILE_OPEN
  "^n", PW_FILE_NEW

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)
    case wParam == PW_FILE_OPEN        /* ^O or menu */
    case wParam == PW_FILE_NEW         /* ^N or menu */
                .  <other cases omitted... >
    case wParam == PW_HELP             /* F1 or menu */
    case wParam == PW_COMMAND_MODE     /* slash or Esc */
/* 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,

will load the name of the activity being edited into the edit control named

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

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 */

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)

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)
    case wParam == IDOK
       end(self, IDOK);
    case wParam == IDCANCEL
       end(self, IDCANCEL);
      ^1;        /* ignore it */

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)

/* 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);
          getItemText(self, INPUT),
          getItemText(self, OUTPUT));

/* 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.


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

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

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

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

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,


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

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

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.


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

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

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

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(
      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

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

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:


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


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.


# compilation flags
CFLAGS=-AM -c -Gsw -Osal -W2 -Zp

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


NAME                    COLORS
DESCRIPTION             'Multiple Document Interface'
STUB                    'WINSTUB.EXE'

DATA                    MOVEABLE MULTIPLE

HEAPSIZE                2048
STACKSIZE               2048

   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
   POPUP "&File"
      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 "&Exit",           IDM_EXIT
      MENUITEM "A&bout...",       IDM_ABOUT

RedMenu MENU
   POPUP "&File"
      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 "&Exit",           IDM_EXIT
      MENUITEM "A&bout...",       IDM_ABOUT
   POPUP "&Red"
      MENUITEM "&0 %",            IDM_0
      MENUITEM "&25 %",           IDM_25
      MENUITEM "&50 %",           IDM_50
      MENUITEM "&75 %",           IDM_75
      MENUITEM "&100 %",          IDM_100, CHECKED

GreenMenu MENU
   POPUP "&File"
      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 "&Exit",           IDM_EXIT
      MENUITEM "A&bout...",       IDM_ABOUT
   POPUP "&Green"
      MENUITEM "&0 %",            IDM_0
      MENUITEM "&25 %",           IDM_25
      MENUITEM "&50 %",           IDM_50
      MENUITEM "&75 %",           IDM_75
      MENUITEM "&100 %",          IDM_100, CHECKED

BlueMenu MENU
   POPUP "&File"
      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 "&Exit",           IDM_EXIT
      MENUITEM "A&bout...",       IDM_ABOUT
   POPUP "&Blue"
      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

   "0",        IDM_0
   "2",        IDM_25
   "5",        IDM_50
   "7",        IDM_75
   "1",        IDM_100

   IDS_TITLE,              "Multiple Document Interface"
   IDS_MAINCLASS,          "MdiMainClass"
   IDS_COLORCLASS,         "MdiChildClass"

MainNew  DIALOG   50, 50, 144, 60
   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

MainAbout   DIALOG   22, 17, 156, 100
    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

/* MDI section of file */

MdiMenu MENU
   MENUITEM "&New",               IDM_NEWWINDOW, GRAYED
   MENUITEM "&Tile",              IDM_ARRANGE, GRAYED
   MENUITEM "&Hide",              IDM_HIDE, GRAYED
   MENUITEM "&Unhide...",         IDM_UNHIDE


MdiUnhide   DIALOG 50, 50, 132, 68
   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

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 */


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,
      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 );

         case DLGNEW_GREEN:
            ColorCreate( hwndColors, COLOR_GREEN );

         case DLGNEW_BLUE:
            ColorCreate( hwndColors, COLOR_BLUE );
         FreeProcInstance( lpProc );

      case IDM_OPEN:

      case IDM_ABOUT:
         /* About dialog box */
         lpProc = MakeProcInstance( MainDlgAbout, hInst );
         DialogBox( hInst, "MainAbout", hwndColors, lpProc );
         FreeProcInstance( lpProc );

      case IDM_EXIT:
         /* Tell application to shut down */
         PostMessage( hwndColors, WM_SYSCOMMAND, SC_CLOSE, 0L );

   case WM_DESTROY:
      PostQuitMessage( 0 );
   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" );

   case COLOR_GREEN:
      hmenuChild = LoadMenu( hInst, "GreenMenu" );

   case COLOR_BLUE:
      hmenuChild = LoadMenu( hInst, "BlueMenu" );
      hAccel = LoadAccelerators( hInst, "BlueAccel" );

   /* Create */
   hwndChild = MdiChildCreateWindow( szClass,
      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:

      case IDM_CLOSE:
         PostMessage( hwndChild, WM_SYSCOMMAND, SC_CLOSE, lParam );

      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 );

      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 );
   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 )
      SendMessage( hDlg, WM_COMMAND, DLGNEW_RED, 0L );
      iReturn = TRUE;

   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 );

      case IDOK:
         EndDialog( hDlg, iButton );

      case IDCANCEL:
         EndDialog( hDlg, 0 );
   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 )
      iReturn = TRUE;

   case WM_COMMAND:
      EndDialog( hDlg, TRUE );
   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

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

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

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


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

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 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

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

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

■    Never assume that one routine will execute before another.

■    Never assume that a given routine will execute for a given number of

■    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

■    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

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:


or a thread can terminate the entire process via:


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,

    while(TRUE)    /* replace this with
          ;           code for main program */

void keyboard_thread(void) /* keyboard thread code */
    KBDKEYINFO keyinfo;

        KbdCharIn(&keyinfo,IO_WAIT,0);  /* wait for keystroke */
        if(keyinfo.chChar == ESC)     /* if ESC pressed, 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

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

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

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

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

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  */

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:














































Figure 13:

#define    INCL_DOS
#define    INCL_SUB

#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;



    while(TRUE)    /* insert code for main program here */
        if(count > 3)

void keyboard_thread(void)  /* keyboard thread code */
    KBDKEYINFO keyinfo;

        KbdCharIn(&keyinfo,IO_WAIT,0);  /* wait for keystroke */
        if(keyinfo.chChar == ESC)     /* if ESC pressed, break */
    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

#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

#if !defined(FALSE)
#define FALSE   0

#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 #     ",
    "                    ",

char *hello_str43[LINES43+1] =
    "                    ",
    "                    ",
    "    Hello, world!   ",
    "  from Thread #     ",
    "                    ",
    "                    ",

char *hello_str50[LINES50+1] =
    "                    ",
    "                    ",
    "    Hello, world!   ",
    "                    ",
    "  from Thread #     ",
    "                    ",
    "                    ",

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 */
                    THREADSTACK,NULL) == -1)

    viomodeinfo.cb = sizeof(viomodeinfo);
    VioGetMode(&viomodeinfo,0);            /* get video info */

    maxrows = viomodeinfo.row;
    maxcols = viomodeinfo.col;

        case 25:
            helloptr = hello_str25;
            numlines = LINES25;
        case 43:
            helloptr = hello_str43;
            numlines = LINES43;
        case 50:
            helloptr = hello_str50;
            numlines = LINES50;
        default:                 /* fail if not 25,43,50 lines */

    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))))
        frames[i]->frame_cleared = FALSE;
        frames[i]->startsem = 0L;

    i = RAND();     /* get first random frame */

    /* set up random appearance */

    for(row = col = 0; loops < maxframes ; )    /* set row/col
                                                   each frame */
            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 */

        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,
                (void far *)frames[i])) == -1)
            maxframes = i;      /* reset maxframes on failure */

    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];

        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++)
        else                   /* else frame not in use */
            p = helloptr;      /* print message */
            for( line = 0; **p; line++, row++, p++)

            /* write id # in frame */


        /* toggle use flag */

        frameptr->frame_cleared = !frameptr->frame_cleared;

void keyboard_thread(void)
    KBDKEYINFO keyinfo;

        KbdCharIn(&keyinfo,IO_WAIT,0); /* wait for keystroke */
        if(keyinfo.chChar == ESC)      /* break if ESC pressed */
    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

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

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];




Figure B:

#define    MAXTHREADS    32
void printf(char *fmt,...)
static long printfSems[MAXTHREADS] =

char formatbuffers[MAXTHREADS][BUFSIZ];
int semno;

for(semno = 0; semno < MAXTHREADS; semno++)
assert(semno < MAXTHREADS);




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:


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)


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.


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

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

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

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");


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",

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),
         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,

 else {  /* send negative ACK using their DDESTRUCT */
      pDDEStruct->fsStatus &= (~DDE_FACK);
      WinDdePostMsg((HWND)lParam1, hwnd, WM_DDE_ACK, pDDEStruct,

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));
 else {  /* send negative ACK using their DDESTRUCT */
      pDDEStruct->fsStatus &= (~DDE_FACK);
      WinDdePostMsg((HWND)lParam1, hwnd, WM_DDE_ACK, pDDEStruct,


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,
    else {
 else {    /* Send a negative ACK using their DDESTRUCT */
    pDDEStruct->fsStatus &= (~DDE_FACK);

WinDdePostMsg((HWND)lParam1, hwnd, WM_DDE_ACK, pDDEStruct, TRUE);


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 {


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;
       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

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);


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;
   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  */


Figure 10:

MRESULT APIENTRY MasterDDEWndProc(hwnd,message,lParam1,lParam2)


    DDEstrptr = (PDDESTRUCT)lParam2;

    case WM_DDE_DATA:




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

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

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

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");

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");


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)NULL, WS_VISIBLE,
                                      0,0,0,0, hwnd, HWND_TOP,
                                      ++nConvID, (PVOID)NULL,
           WinDdeRespond(lParam1, hwndConv, "Client",


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,
                                HWND_TOP, ++winDDEid, (PVOID)NULL,
         WinSetWindowULong(DDEconversationHWND, WW_CONV_HWND,
         WinSetWindowULong(WinQueryWindow(hwnd, QW_PARENT, FALSE),

         /* send a request for initial data */
         DDEstrptr = st_DDE_Alloc(sizeof(DDESTRUCT) +
         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) +
         DDEstrptr->offszItemName = (USHORT)sizeof(DDESTRUCT);
         strcpy(DDES_PSZITEMNAME(DDEstrptr), "Graphics");
         WinDdePostMsg((HWND)lParam1, DDEconversationHWND,
                       (ULONG)WM_DDE_ADVISE, DDEstrptr, TRUE);
     /* free the memory */


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) +
             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);
             pGDEData->cBytes = lPhFigCnt;
             strcpy(pGDEData->szItem, "phone");
             pGDEData->pGpi = (unsigned char far *)((LONG)pGDEData +
             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);
     else {  /* post negative ACK using their DDESTRUCT */
          pDDEStruct->fsStatus &= (~DDE_FACK);

WinDdePostMsg(lParam1, hwnd, WM_DDE_ACK, pDDEStruct, TRUE);



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) +
            pDDEStruct->offszItemName = (USHORT)sizeof(DDESTRUCT);
            strcpy(DDES_PSZITEMNAME(pDDEStruct), "Graphics");
            WinDdePostMsg(hwndEnum, hwnd, WM_DDE_REQUEST,
                          pDDEStruct, TRUE);
        WinLockWindow(hwndEnum, FALSE);


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 {
     else {    /* Send a negative ACK using their DDEStruct */
        pDDEStruct->fsStatus &= (~DDE_FACK);
        WinDdePostMsg(lParam1, hwnd, WM_DDE_ACK, pDDEStruct, TRUE);

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);


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) {
                      WinQueryWindow(hwnd, QW_OWNER, FALSE),
                      MPFROMSHORT(ICM_END), (MPARAM)gde_ptr);
       } else {  /* replace if advise */

       if (DDEstrptr->fsStatus && DDE_FACKREQ) {
           DDEstrPtrAck = st_DDE_Alloc(sizeof(DDESTRUCT),
           WinDdePostMsg((HWND)lParam1, hwnd, (ULONG)WM_DDE_ACK,
                           DDEstrPtrAck, TRUE);



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,
                          hwnd, WW_CLOSE) | WIN_CLOSING_FLAG);
       henum = WinBeginEnumWindows(DDEanchorHWND);
       while (hwndenum = WinGetNextWindow(henum)) {
           WinSetWindowULong(hwndenum, WW_CONV_FLAGS,
                     WinQueryWindowULong(hwndenum, WW_CONV_FLAGS) |

tohwnd = (HWND)WinQueryWindowULong(hwndenum,WW_CONV_HWND);

DDEptr = st_DDE_Alloc(sizeof(DDESTRUCT) +
           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);
     else {
         WinPostMsg(hwnd,WM_QUIT,0L,0L); /* quit if no
                                            conversations open  */


Figure 25

/* post terminate to server, tell client, and die */

if (!WinQueryWindowULong(WinQueryWindow(hwnd, QW_OWNER, FALSE),

       WinDdePostMsg((HWND)lParam1, hwnd, WM_DDE_TERMINATE,
                     NULL, TRUE);


Figure 26

/* decrement conversation count and delete picture */
                     WinQueryWindowULong(hwnd, WW_CONVCOUNT) - 1);
   if (WinQueryWindowULong(hwnd,WW_CLOSE) &&
       !WinQueryWindowULong(hwnd, WW_CONVCOUNT)){


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


Initializes the VMM. Must be called at the beginning of the application
before any memory is requested.


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

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

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

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

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"


char far *s;


/* Note--this assumes
   large memory model */
if ((h = MyAlloc(4)) ==
/* perform error
   processing */


if ((s = MemDeref(h)) !=
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");

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

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

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.


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

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

 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

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

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

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.


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

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

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

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

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

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

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

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

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

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.


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

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

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


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

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

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).


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

■    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:




DosGet ModHandle


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(
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(
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(
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(
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(
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.


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

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

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

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


#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,
USHORT    far    _loadds pascal do_list(PSZ, PHDIR, USHORT,

void    do_usage(void);
void    main(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 +

 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\
 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");

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;

        printf("Error in initial call to debug\n");
    *name = (CHAR)NULL;
        if(!strncmp(tmp_buf, "exit", 4))
        if(!strncmp(tmp_buf, "file=", 5))
            strcpy(name, tmp_buf + 5);
        if(!strncmp(tmp_buf, "len=", 4))
            max_len = atoi(tmp_buf + 4);
            buf_ptr = calloc(max_len, 1);
        if(!strncmp(tmp_buf, "cnt=", 4))
            num_files = atoi(tmp_buf + 4);
        if(!strncmp(tmp_buf, "hand=", 5))
            sscanf(tmp_buf + 5, "%x", &handle);
        if(!strncmp(tmp_buf, "attrb=", 6))
            sscanf(tmp_buf + 6, "%x", &attrb);
        if(!strncmp(tmp_buf, "list", 4))
            do_list(name, (PHDIR)&handle, attrb,
            (PFILEFINDBUF)buf_ptr, max_len,
            (PUSHORT)&num_files, (ULONG)0);
        if(!strncmp(tmp_buf, "debug=", 6))
            if(!debug(!strncmp(tmp_buf + 6, "on", 2)))
                printf("Error in subsequent call to debug\n");
        if(!strncmp(tmp_buf, "show", 4))
            show((PFILEFINDBUF)buf_ptr, num_files);
        if(!strncmp(tmp_buf, "go", 2))
            do_find(name, (PHDIR)&handle, attrb,
            (PFILEFINDBUF)buf_ptr, max_len,
            (PUSHORT)&num_files, (ULONG)0);
        if(*tmp_buf == '?')




Figure 2


#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,
                            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,
    *hptr = save_handle;
    *num_ptr = save_num;


EXPORTS do_list

Figure 3


#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    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);
                printf("Buffer Overflow - Increase Buffer Size\n");

        case  ERROR_DRIVE_LOCKED:
                printf("Drive Locked\n");

        case  ERROR_FILE_NOT_FOUND:
                printf("File: %s not found\n", name_ptr);

                printf("Invalid handle: %d\n", *hptr);

                printf("Invalid Parameter\n");

        case  ERROR_NO_MORE_FILES:
                printf("Ran out of files\n");

                printf("Can;t allocate another Search Handle\n");

        case  ERROR_NOT_DOS_DISK:
                printf("Not a DOS Disk\n");

        case  ERROR_PATH_NOT_FOUND:
                printf("I can't locate that Path\n");

                printf("Unknown error in FindFirst: %d\n", 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? */

        /* some DLL already loaded. Requested DLL? */

        stat = DosGetModHandle(mod_name[debug_flag],

        /* 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,
                printf("Couldn't retrieve loaded DLL Name. Error
                code is: %d\n", stat);
                printf("Currently Loaded DLL is: %s\n",

            /* free the already loaded module, whatever
             * it is */

            /* current handle is for requested DLL.
             * Simply return */
            printf("DLL (%s) already loaded\n",


    /* wrong DLL is now freed */
    /* try to load the requested DLL, and get the entry
     * points */

    if(stat = DosLoadModule(fail_buf, FAIL_BUF_LEN,
        printf("Couldn't load: %s (stat is :%x)\n",
                mod_name[debug_flag], stat);
        printf("DLL problem was in: %s\n", fail_buf);
        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);
        printf("Routine address is: %lx\n", routine);

    /* module loaded, entry point returned, so we return */


EXPORTS    do_find
EXPORTS    debug

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.


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.


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

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

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

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

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.


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

Figure 1


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


    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


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


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:

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

    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

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;
6   main()
7   {
8       s1    s1instance;
10       /* ... */
11   }

Figure 2

1  typedef struct {
 2      s2      *s2ptr;
 3      char    s1data[100];
 4  } s1;
 6  typedef struct {
 7      s1      *s1ptr;
 8      char    s2data[100];
 9  } s2;
11  main()
12  {
13      s1    s1instance;
14      s2    s2instance;
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

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

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;
 6   struct {
 7       s1      *s1ptr;
 8       char    s2data[100];
 9   } s2;
11   main()
12   {
13       s1    s1instance;
14       s2    s2instance;
16       /* ... */
17   }

Figure 4

1    struct s1_tag {
 2        struct s2_tag  *s2ptr;
 3        char    s1data[100];
 4    };
 6    struct s2_tag {
 7        struct s1_tag  *s1ptr;
 8        char    s2data[100];
 9    };
11    main()
12    {
13        struct s1_tag s1instance;
14        struct s2_tag s2instance;
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    };
 7    struct sometag {
 8        struct anothertag  anotherinstance;
 9            /* This is an error since a
10               description of anothertag
11               is not in scope */
12    };
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

Figure 6

1    typedef struct s2_tag s2;
 2    typedef struct s1_tag s1;
 4    struct s1_tag {
 5        s2  *s2ptr;
 6        char    s1data[100];
 7    };
 9    struct s2_tag {
10        s1  *s1ptr;
11        char    s2data[100];
12    };
14    main()
15    {
16        struct s1_tag s1instance;
17        struct s2_tag s2instance;
19        /* ... */
20    }

Figure 7

1    typedef struct s1_tag {
 2        struct s2_tag  *s2ptr;
 3        char    s1data[100];
 4    };
 6    typedef struct s2_tag {
 7        struct s1_tag  *s1ptr;
 8        char    s2data[100];
 9    };
11    main()
12    {
13        struct s1_tag s1instance;
14        struct s2_tag s2instance;
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

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;
 6    struct ps2 {
 7        char    *p;
 8        char    array[512 - sizeof(char *)];
 9    } v2;
11    struct ps1 f1()
12    {
13        v1.p = "hi there";
14        return (v1);
15    }
17    struct ps2 f2()
18    {
19        v2.p = "HI THERE";
20        return (v2);
21    }
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    }
34    func(struct ps1 v1, struct ps2 v2)
37    {
38        printf("%s\n", v1.p);
39        printf("%s\n", v2.p);
40    }
41    func2(struct ps1 *v1, struct ps2 *v2)
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  };
 6  struct temp *
 7  func()
 8  {
 9      struct temp temp; /* yes, structure tags *and*
10                           structures can have the
11                           same name */
13      return (&temp);
14  }
16  main()
17  {
18      struct temp *temp;
20      temp = func();
21      /* <expressions using temp->???> */
22  }

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


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

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

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

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

Figure 10

1   struct s1tag {
 2       int    s1var;
 3   } s1;
 5   struct s2tag {
 6       int    s2var;
 7       struct s1tag *ps1;
 8       struct s1tag s1inst;
 9   } s2;
11   struct s3tag {
12       int    s3var;
13       struct s2tag *ps2;
14       struct s2tag s2inst;
15   } s3;
17   struct s3tag *ps3;
19   main()
20   {
21       s1.s1var = 99;
23       s3.s2inst.s1inst.s1var = 5;
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);
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);
42       ps3->ps2 = &ps3->s2inst;
43       ps3->ps2->s1inst.s1var = 22;
44       printf("s3.s2inst.s1inst.s1var=%d\n",
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

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 };
 5   struct s2tag {
 6       int    s2var;
 7       struct s1tag *ps1;
 8       struct s1tag s1inst;
 9   } s2;
11   struct s2tag *ps2;
13   struct s1tag
14   s1returner()
15   {
16       return (s1);
17   }
19   struct s1tag *
20   ps1returner()
21   {
22       return (&s1);
23   }
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   }
35   void example2()
36   {
37       struct s1tag s1holder;
38       struct s1tag *ps1holder;
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   }
48   struct s2tag
49   s2returner()
50   {
51       return (s2);
52   }
54   struct s2tag *
55   ps2returner()
56   {
57       return (&s2);
58   }
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   }
67   void example4()
68   {
69       printf("%d\n", ps2returner()->ps1->s1var);
70   }
72   main()
73   {
74       example1();
75       example2();
77       s2.s1inst.s1var = 54321;
78       example3();
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) +

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

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]) +

you would code:

ppacket = (struct
        apacket *)malloc(
offsetof(apacket, data) +

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.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).


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]))
 4   struct s1tag {
 5       char    charray[20];
 6   };
 8   struct s1tag s1[10];
 9   struct s1tag *ps1;
11   main()
12   {
13       int     i;
14       int     j;
15       char    *cp;
17       printf("%d/%d=%d\n", sizeof(s1),
18                  sizeof(s1[0]), HBOUND(s1));
20       for (i = 0; i < HBOUND(s1); i++)
21      for (j = 0; j < sizeof(s1[0].charray); j++)
22          s1[i].charray[j] = '\0';
24       ps1 = &s1[5];
25       ps1->charray[2] = 5; /* Note that this is ASCII 5 */
26       (*ps1).charray[2] = 5; /* not '5'                 */
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   }


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 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

■    $$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

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

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.


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

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

; 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

_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


            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


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

ovly_int    endp


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 */

    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");

    if (eh.M_sig != 'M' || eh.Z_sig != 'Z') {
    errputs("Found non-EXE signature!\r\n");
        if (eh.remain_len == 0)
            sectionsize = (long)eh.page_len * 512;
            sectionsize = (long)(eh.page_len - 1) * 512 +

        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 +
            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 +
                    *target_ptr += pspaddr + 0x10;
            return 0;
        } else {
    nextpos = startpos + sectionsize;
            /* round up to 512-byte bound */
            nextpos = (nextpos + 511L) & ~511L;
    if (nextpos >= filesize) {
    return 1;
    return 1;

Figure 4

.model  LARGE,C

            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
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
myclose     endp


Figure 5


#include <stdio.h>
#include <string.h>

void p1(void);
void p2(void);

extern char executable_name;

void main(int argc, char **argv)
    int i;

    argc = argc;

    printf("This is main\n");
    for (i = 0; i < 1000; i++) {


#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);


#include <stdio.h>
void p2()
   printf("In p2, whose address is %Fp\n",p2);
   printf("\t 10 %% 3 = %d\n",10 % 3);

Figure 6


            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   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
            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
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


            extrn  _errputs:far
            .model LARGE
p1_msg      db     'This is p1...',13,10,0

p1          proc   far
            public p1
            lea    ax,p1_msg
            push   ds
            push   ax
            call   _errputs
            add    sp,4
p1          endp



            extrn   _errputs:far
            .model  LARGE
p2_msg      db      'This is p2...',13,10,0

p2          proc   far
            public p2
            lea    ax,p2_msg
            push   ds
            push   ax
            call   _errputs
            add    sp,4
p2          endp


Figure 7


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.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)+

Figure 8

/* smerge - merge source and assembly files */

#include <stdio.h>

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");

    sline = 0;

    asmsrc = fopen(argv[1],"r");
    csrc = fopen(argv[2],"r");
    mergefile = fopen(argv[3],"w");


    totslines = 0;
    while (fgets(buffer,128,csrc) != NULL)

    while ((fgets(buffer,128,asmsrc)) != NULL) {

        /* special hack for MSC -> assembly ...
           kill the "__acrtused" extrn */

        if (strstr(buffer,"__acrtused") != NULL)

        if (strncmp(buffer,"; Line",6) == 0) {
            sscanf(buffer,"; Line %ld",&line);
            if (line <= totslines) {
                while (sline < line) {
        } else

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.


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.


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.


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

■    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

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

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

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",

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

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:


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



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




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

_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]

    mov    ax,[XMM_Initialized]

_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

_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
    mov    dh,bl            ; Put error return in DH

    pop    bp            ; Restore the stack

_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
    pop    bp            ; Restore the stack

_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
    pop    bp            ; Restore the stack

_XMM_MoveExtended endp

_TEXT    ends


Figure 5


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 -     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)


  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");

  /* Is it a version 2.00 driver (or higher)? */
  if (XMM_Version() < 0x200)
      printf("This program requires an XMS driver version 2.00 or \

  /* 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));

  /* 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));

  /* Fill the screen with garbage. */
  for (i=0; i < 22; i++)
      for (j=64; j < (64+81); j++)

  /* Prompt the user. */
  printf("Press Enter to restore the screen:");

  /* 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));

  /* Free the extended memory buffer. */

  printf("Program terminated normally.");

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

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

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

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
    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


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

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

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,

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   */

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     */

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       */

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

    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.


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.


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

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.


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:


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,


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   */

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    */

Figure 9

typedef struct _SCALEFACT
unsigned rowScale;    /* vertical factor    */
unsigned colScale;    /* horizontal factor    */

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.


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

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

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.


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

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


If function is successful:

Carry flag is clear.

Other registers are preserved if MS-DOS version 3.0 or later, destroyed if

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:


The end of the entire set of strings is indicated by an additional null

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:


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.


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:



Similarly, CHILD.ASM can be assembled and linked into the file CHILD.EXE as

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 EX