PCjs Machines

Home of the original IBM PC emulator for browsers.


Microsoft Systems Journal (Vol. 3)

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

Microsoft Systems Journal Volume 3


Vol. 3 No. 1 Table of Contents


Preparing for Presentation Manager: Paradox(R) Steps Up to Windows 2.0

Ansa Software is developing versions of the Paradox data base product for
the 80386, OS/2, UNIX(R)/XENIX(R), and Microsoft(R) Windows environments,
all largely sharing the same code. This article explains how Ansa ported
their large MS-DOS(R) application to the Windows environment for one of
the new versions.

Converting Windows Applications For Microsoft(R) OS/2 Presentation Manager

Microsoft OS/2 Presentation Manager offers the best of the Microsoft Windows
operating environment and the protected-mode OS/2 operating system. The
sample program SayWhat highlights the similarities and differences between
Presentation Manager and Windows programming techniques.

Programming Considerations In Porting To Microsoft(R) XENIX(R) System

XENIX System V/386, Microsoft's version of UNIX for the 386, supports
virtual memory, demand paging, and both 286 and 386 mode execution. This
article outlines considerations that help programmers choose between 16- and
32-bit program models when developing applications for XENIX V/386.

HEXCALC: An Instructive Pop-Up Calculator For Microsoft(R) Windows

HEXCALC, a pop-up window hexadecimal calculator, explores a method of using
dialog box templates to define the layout of child window controls on a
program's main window, a technique illustrating features of both Microsoft
Windows Version 2.0 and the OS/2 Presentation Manager.

Effectively Using Far and Huge Data Pointers In Your Microsoft(R)
C Programs

Programs that use "huge" pointers to access very large amounts of data are
often affected by the expense of using relatively slow 32-bit arithmetic.
Under certain conditions, huge pointers can be converted to far pointers,
resulting in a significant decrease in access time.

EMS Support Improves Microsoft(R) Windows 2.0 Application Performance

Windows 2.0 uses expanded memory to bank-switch Windows applications,
allowing multiple large programs to run simultaneously as if each were
the only application running.The article examines how Windows' exploitation
of LIM 4.0 expanded memory support can affect your Windows application.


There has been much speculation about the amount of effort required to move
applications from the Windows 2.0 environment to the OS/2 Presentation
Manager. With varying estimates and opinions ringing in our ears, we decided
to devote a major part of this issue to exploring what's involved in
programming for the Presentation Manager.

We turned to Michael Geary, who spent a great deal of the last year
exploring Presentation Manager. He provides us with a step-by-step guide to
moving a Windows 2.0 application to Presentation Manager. To illustrate the
differences, we have includeded side-by-side the complete code listings of
both versions of his tutorial program.

Charles Petzold, inveterate Windows expert, also provides insight on the
topic with parallel Microsoft(R) Windows 2.0 and Presentation Manager
versions of his instructive HEXCALC program.

Ansa Software has had notable success with the MS-DOS(R) version of its
Paradox(R) database. Currently under development are Paradox versions for
Microsoft Windows, OS/2 Presentation Manager, UNIX(R) 5.3 and XENIX(R),
as well as protected-mode 80386. We talked with Richard Schwartz, vice
president of software development and cofounder of Ansa, to discover how one
software firm is preparing for their move to Presentation Manager.

With all the hoopla surrounding the OS/2 systems, we don't want to forget
the importance of recent XENIX and MS-DOS developments. In particular, the
new version of XENIX for the 386 environments has generated much interest,
including questions on the portability between different versions of XENIX.
Martin Dunsmuir, who is responsible for XENIX development at Microsoft,
answers these questions and others.

We look forward to 1988, and the new developments this coming year will
bring. Look for MSJ to be there with the details.──ED.


Editor and Publisher


Technical Editor

Associate Editor

Production Editor

Staff Editor

Editorial Assistant


Art Director

Associate Art Director


Circulation Manager

Assistant to the Publisher

Administrative Assistant

Copyright(C) 1988 Microsoft Corporation. All rights reserved; reproduction
in part or in whole without permission is prohibited.

Microsoft Systems Journal is a publication of Microsoft Corporation, 16011
NE 36th Way, Box 97017, Redmond, WA 98073-9717. Officers: William H.
Gates, III, Chairman of the Board and Chief Executive Officer; Jon Shirley,
President and Chief Operating Officer; Francis J. Gaudette, Treasurer;
William Neukom, Secretary.

Microsoft Corporation assumes no liability for any damages resulting from
the use of the information contained herein.

Microsoft, MS-DOS, XENIX, CodeView and the Microsoft logo are registered
trademarks of Microsoft Corporation. PageMaker is a registered trademark of
Aldus Corporation. Paradox is a registered trademark of Ansa Software, a
Borland Company. Apple and Macintosh are registered trademarks of Apple
Computer, Inc. UNIX is a registered trademark of American Telephone and
Telegraph Company. RAMpage! is a registered trademark of AST Research, Inc.
Advantage! is a trademark of AST Research, Inc. IBM and PC/AT are registered
trademarks of International Business Machines Corporation. PS/2 and Micro
Channel are trademarks of International Business Machines Corporation. Intel
is a registered trademark of Intel Corporation. Above is a trademark of
Intel Corporation. Lotus and 1-2-3 are registered trademarks of Lotus
Development Company. Microrim and R:BASE are registered trademarks of
Microrim, Inc.


Preparing for Presentation Manager: Paradox Steps Up to Windows 2.0

Also see the related article:
  Paradox Under Microsoft(R) OS/2

Craig Stinson☼

Ansa Software, the Belmont, Calif., firm recently acquired by Borland
International, sells only one product, but that product, the relational
database manager Paradox(R), will appear in four new versions during the
coming year. A fifth new version will probably be available in early 1989.

The offspring are aimed at different operating environments:
Microsoft(R) Windows Version 2.0, OS/2 systems Version 1.0, the OS/2
Presentation Manager, UNIX(R) 5.3 and XENIX(R), and the 80386 running MS-
DOS(R) 3.x in protected mode with the help of a 386 MS-DOS compatibility
tool. Paradox 386 was shown at last November's Comdex and will probably be
available by the time you read this article; it won't require a
compatibility tool, just a 386 machine. The OS/2 1.0 version should be
released during the first quarter of 1988, the Windows version in the
second quarter, the UNIX/XENIX version in the third quarter, and the
Presentation Manager version in about a year, depending on the date of the
OS/2 Presentation Manager's arrival.

All Paradox versions, old and new, will have multiuser capability and will
be data-compatible. With versions for all the major personal computing
environments (except Macintosh(R)──Ansa is currently evaluating the
feasibility of a Mac version), Ansa will be in a position to serve the vast
majority of desktop users. And all users at various nodes of a network will
be able to access and edit a common database, regardless of the hardware or
operating systems they're running.

Sound Design Pays Off

What type of effort is involved in porting a large, complex MS-DOS
application such as Paradox to all these different operating environments?
In a recent interview with MSJ, Richard Schwartz, vice president for
software development and a cofounder of Ansa, said that the move from one
character-based system to another has been relatively straightforward,
thanks largely to the foresight of the original designers of Paradox.

"We designed Paradox from the beginning to be very clean and portable," says
Schwartz. "We tried not to play dirty tricks getting at the operating system
or creating hardware dependencies." This is not to say, of course, that the
original Paradox doesn't write directly to the screen. "We do, of course,
but we've isolated it. We have one module that will access the screen memory
map directly. And even the BIOS dependencies are all in one place."

The move from a character environment (MS-DOS 3.x) to a graphics environment
(Microsoft Windows and the OS/2 Presentation Manager) is a considerably
more ambitious undertaking. Schwartz talked about the whys and hows of
this move and about how the graphics-based Paradox will look, feel, and
perform relative to the original.

Why Windows?

The move to Windows now, according to Schwartz, will give Ansa advantages in
marketing and in implementation when the OS/2 Presentation Manager arrives.

"We think it's important to put out the Windows version, not because Windows
2.0 is going to be the primary market-certainly, Presentation Manager
market will dwarf the Windows market-but because a lot of companies will be
evaluating the Presentation Manager interface through Windows 2.0. And
applications that run under Windows early on will have an advantage, given
the long evaluation cycles that companies go through."

Second, because Windows 2.0 is here now and Presentation Manager is not,
the effort required to rethink and recode Paradox for Windows is the best
preparation Ansa can undertake for the eventual port to the Presentation
Manager-even though the API for Presentation Manager will differ
significantly from that for Windows.

The Good News

How hard was the port to Windows? According to Schwartz, all but the
underlying database engine was redesigned, borrowing pieces of the
previous architecture and source code. Nevertheless, he asserts, about 50
percent of the original source code emigrated to the new world intact.
That's an off-the-cuff, work-in-progress estimate, but even if it is too
optimistic, it is still good news for Windows developers: not all programs
will have to be rewritten from the ground up to run in the new graphics

Moreover, in Ansa's case the conversion will probably take about two person-
years of work. That's not bad, considering that the original Paradox was a
14-person-year effort. And the company has done it without hiring a lot of
specialized expertise. That's also good news, considering the current
scarcity and high market value of Macintosh and Windows programmers.

"We tried to get some people with product-level graphics experience. We
used a headhunter for a while, but we were very unsuccessful," says
Schwartz. "And it turned out that we did very well taking good software
engineers who have general computer science sophistication and having them
come up to speed under Windows. We found that general computer science
background was the most important factor."

A Natural Fit

Making Ansa's transition from characters to pixels easier was the fact that
Paradox, despite its early origins (development for the first version
began in the summer of 1981), has always been a highly visual, interactive
program. From an interface design standpoint, therefore, the transfer to
Windows entailed an expansion of ideas already in place rather than a
wholesale reconceptualization.

"We've always had a very visual orientation, an object orientation," says
Schwartz. "And it was just sort of a natural fit."

Data tables in the character-based Paradox, for example, are presented to
the user in the form of visual constructs that look much like windows,
except that the borders are made of text-graphic characters and don't go all
the way around (see Figure 1☼). Like a frame in Framework or a screenful of a
spreadsheet, these objects act as portholes onto larger expanses of data,
which Paradox calls images.

Paradox presents the user with a workspace, much as Windows does, on which
he can keep several images visible at once. And though it doesn't support a
mouse, the character-based product lets the user manipulate the sizes and
locations of images, and the width of columns within them, by positioning
a highlight in the right hot spot and pressing cursor keys. This ability to
interact immediately with a data construct, instead of having to issue a
menu command and describe what one wants, is what Schwartz calls "direct
operability," an important component of the original Paradox interface.

Visual Interaction

"The idea was──as much as possible, anywhere we could──to allow visual
interaction, rather than descriptive interaction," says Schwartz. "Instead
of asking the user to explain or describe something, we asked him to show it
to us. That's very compatible with moving to the Windows environment."

Paradox under Windows will look quite similar at first glance to the
original version (see Figure 2☼), except that the Lotus-style menu gives way
to drop-down menus, the text comes up black-on-white, and the images are
fully formed document windows, with title bars, scroll bars, control menus,
and sizing icons. The direct operability will still be there, but there'll
be more of it. And, of course, there will be mouse support.

One new area of direct operability in Windows Paradox is in the
specification of sorts. In the character-based version, the user indicated
which fields he wanted to be sort keys and in which direction he wanted a
field sorted by typing a field number followed by an "a" for ascending or a
"d" for descending (see Figure 3☼).

The Windows version (see Figure 4☼) simplifies this process and makes it less
analytical by letting the user double-click directly on the name of each
sort key field. An arrow indicating sort direction (ascending, by default)
will appear to the left of the chosen field name; to switch directions, the
user merely toggles the arrow by double-clicking on it.

The notion of clicking on a value to toggle it or to select from available
values appears elsewhere in Windows Paradox. In creating or restructuring a
table, for example, where the user is asked for a data type, he can click
the mouse to get a menu showing available types. Similarly, in the
create/restructure subsystem, a double-click on a field name will summon
a dialog box, allowing the user to inspect current validity-check
definitions or create new ones.

Implementation Advantages

Describing the conversion effort, Schwartz downplays the fearsome Windows
learning curve, emphasizing instead the implementation advantages
conferred by the graphics environment. In particular, he cites the greater
degree of independence among the various objects with which the user

In the Windows environment, every table in the workspace, as well as every
form, query, and report specification, lives in its own window and has its
own message handler. That message handler is responsible for knowing only
about itself, how to repaint itself and how to update itself. In the
character-based version, more attention had to be paid to the "global
state," that is, to the ways in which changes in a table would affect
everything else in the workspace.

"At the implementation level," says Schwartz, "that means that your code
can have an architecture such that everything is purely local to each
image. So you can have a local handler that knows how to resize a window or
how much of it to repaint. If anything in the size of that window is
affected, Windows takes care of sending messages to the other windows to let
them know that some other portion needs to be repainted."

Debugging Difficulties

According to Schwartz, one fairly serious hindrance to the development of
Windows Paradox was the lack of debugging tools for use in the Windows

"It's a harder problem anyway," Schwartz says, "because there's more going
behind the scenes in the Windows environment." The movability and
discardability of Windows object code complicates matters, making bugs
harder to find. Bugs tend to turn up later in the game as the result of
stress on the system because a piece of code has been moved in memory.

The current version of CodeView(R) doesn't work in the Windows environment,
though Microsoft is working on a Windows-specific version. Symdeb is
provided, but Ansa didn't use it extensively, preferring instead to rely on
algorithmic-level debugging by embedding printf statements to test values
of variables at various points in the program.

"In our design environment, we have a monochrome display hooked up as well
as an EGA. We've hooked things up so that we can put printfs in the code and
have them scroll out on the monochrome display. You get the effect of
traditional debugging preserved in the Windows environment."

Making debugging even more difficult was the fact that Ansa was working with
early prerelease versions of Windows and the Windows development tools (the
project began last April). Programmers were sometimes hard pressed to tell
which problems they should attribute to their own code, which to the
compiler, and which to bugs in the underlying environment.

User Advantages

The independence of screen objects provides several benefits for users.
Most importantly, it means that users can have more things going on at
once and still keep track of everything. Instead of being limited to the
number of images that will fit one after the other on screen, the user can
load up the screen with overlapping windows, thereby achieving whatever
level of information density he finds most appropriate.

This facilitates such activities as moving from one table to another,
copying data or structures from one table to another, discovering
correlations between one set of data and another, and so on. It also means
that Windows Paradox will appear somewhat less "modal" to users than its
text-based ancestor. There are places in the current program where a user
has to drop one context, if only briefly, to work in another. The Windows
version will minimize the effects of such breaks in continuity.

For example, in the character-based version, if the user wants to modify a
data-entry form on the fly while entering data, he issues a command to
summon the forms subsystem. The data and form he was working on disappear
at that point, becoming visible again only after he's finished changing
the form spec. In the Windows version, the form subsystem will appear in a
child window, with the data remaining visible in the parent window.

Other benefits the Windows Paradox user will find include the following:

  ■  The Windows version will automatically support any high-resolution
     large-screen displays supported by Windows itself.

  ■  The report subsystem will offer enhanced font support and will accept
     bit-mapped images by way of the Windows clipboard. Users can dress up
     Paradox reports with company logos, signatures, clip art, and the
     like. Windows Paradox will not accept bit-mapped images as data
     types, although Schwartz concedes that that's "a natural direction"
     for the product to evolve in.

  ■  Windows Paradox can exchange data with other Windows applications by
     way of dynamic data exchange (DDE). At the Microsoft Excel
     announcement, Ansa showed an early version of Paradox running as a
     client of Microsoft Excel, with Paradox started up by means of the
     macro language and transferring data to Microsoft Excel via DDE (see
     Figure 5☼). The finished Windows version of Paradox will allow a
     reversal of this scenario: you can start up and control other
     applications by means of scripts written in the Paradox application
     language (PAL).

Performance Hits

The picture sounds rosy, but Schwartz concedes that the advantages of the
Windows environment will exact a few performance penalties. The hits, he
suggests, will come less from Windows' need to represent text in graphics
mode than from its propensity to swap program modules in and out of memory.

Because the program was still under development at this writing, Ansa was
not prepared to supply benchmark data. However, Schwartz maintains the
company is "quite happy with the Paradox performance on 10-MHz 286s and
above." And, he says, the SMARTDrive program, a Microsoft-supplied disk-
caching utility tailored to enhance the performance of Windows, speeds
things up significantly on all machines.

Fears Allayed

What messages would Ansa's Schwartz pass along to another Windows
application developer? The most obvious one is that the redesign of an MS-
DOS program to run under Windows is not the horrendous task it's commonly
thought to be, particularly if the program was well designed in the first
place. A second notion involves the maintenance of individuality under
conditions that seem to impose similarity.

"Some people say that all products are going to look alike, that all the
creativity has been taken away from the designer in the Windows
environment," says Schwartz. "So much has been standardized that
everything's going to look the same and work the same. That's certainly
true at some level. Still, there is a very diverse set of ways in which one
can take advantage of the environment to present a particular application.

"You need to concern yourself first with the core model of how an
application is to be constructed-with the user-level concepts and how you
want to present them. And only after that you look at what the Windows tools
are and how you'll use them. The greatest effect on the user is the
underlying model, and that is not affected at all by Windows. Windows is
just a way to present that, to communicate it more effectively to the

Paradox Under Microsoft(R) OS/2

Paradox was concieved in the days of very limited resources. Development
began in the first half of 1981, months before the announcement of the
IBM(R) PC, when 64Kb was as much memory as anyone could expect to have.

Over the next several years, as larger machines grew commonplace, Paradox
grew in scope and ambition, so that by the time the the product was
announced in the Fall of 1885, it required 512Kb to run──practically
speaking, that meant a 640Kb computer.

However, like a depression era parent, Paradox never forgat what it was like
to live in lean times. From the beggining, the product incorporated a
virtual memory manager that swapped data as necessary to the hard disk. One
of the programs selling points, moreover, was the heuristic approach to the
processing of relational queries. An important aspect of this process was
machine reasoning about what data was currently in memory and what was
swapped out somewhere.

Therefore, the large address space afforded by the 80286 and 80386 chips
running in protected mode represents a liberation for Paradox, demonstrated
by the preliminary statistics shown in the accompanying charts.

Judging by the numbers, Paradox running under the OS/2 systems in protected
mode should take care of complex queries and large scale sorts much more
quickly than the MS-DOS 3.3 version. The 80386 version will do queries about
as efficiently as the OS/2 versions and will be even more nimble at sorting.

Perhaps the most surprosing information revealed by these early numbers is
that Paradox 2.0 running in real mode under OS/2 (in the OS/2 compatibility
box) outperforms the same product running on the same hardware under MS-DOS
3.3. According to Microsoft, most MS-DOS programs will run 5 percent slower
in the compatibility box. Ansa's Richard Schwartz attributes the improvement
to the fact that Paradox is doing a lot of random file access and is
therefore able to profit significantly from OS/'s disk caching.

Performance statistics for the Microsoft Windows version of Paradox were not
yet available as of this writing.

    ║   ║100─┐      93
    ║   ║    │      ▄▄▄                     ▒▒ Query ░░ Sort
    ║   ║    │     ░░░█
    ║   ║ 80─┤  73 ░░░█            71
    ║ S ║    │  ▄▄▄░░░█            ▄▄▄
    ║ E ║    │ ▒▒▒█░░░█        59 ░░░█            60
    ║ C ║ 60─┤ ▒▒▒█░░░█        ▄▄▄░░░█        46  ▄▄▄        48
    ║ O ║    │ ▒▒▒█░░░█       ▒▒▒█░░░█        ▄▄▄░░░█        ▄▄▄ 44
    ║ N ║    │ ▒▒▒█░░░█       ▒▒▒█░░░█       ▒▒▒█░░░█       ▒▒▒█ ▄▄▄
    ║ D ║ 40─┤ ▒▒▒█░░░█       ▒▒▒█░░░█       ▒▒▒█░░░█       ▒▒▒█░░░█
    ║ S ║    │ ▒▒▒█░░░█       ▒▒▒█░░░█       ▒▒▒█░░░█       ▒▒▒█░░░█
    ║   ║    │ ▒▒▒█░░░█       ▒▒▒█░░░█       ▒▒▒█░░░█       ▒▒▒█░░░█
    ║   ║ 20─┤ ▒▒▒█░░░█       ▒▒▒█░░░█       ▒▒▒█░░░█       ▒▒▒█░░░█
    ║   ║    │ ▒▒▒█░░░█       ▒▒▒█░░░█       ▒▒▒█░░░█       ▒▒▒█░░░█
    ║   ║    │ ▒▒▒█░░░█       ▒▒▒█░░░█       ▒▒▒█░░░█       ▒▒▒█░░░█
    ╚═══╝  0─┼─▒▒▒▀░░░▀───┬───▒▒▒▀░░░▀───┬───▒▒▒▀░░░▀───┬───▒▒▒▀░░░▀─┐
             Paradox 2.0    Paradox 2.0     Paradox OS/2   Paradox 386
            under DOS 3.3   in the OS/2     in protected   under DOS 3.3
                          Compatability Box     mode

    ════════════════════ ELAPSED TIME IN SECONDS ═══════════════════════

Preliminary performance statistics for three new versions of Paradox, as
measured on a PS/2 Model 80. The query involved a join of a 5000-record
table and a 10,000-record table; the sort was for the 500-record table. The
figures for Paradox OS/2 and Paradox 386 may differ when those products are


   ║ E ║    60─┐                                              53%
   ║ R ║       │     ▒▒ Query                                 ▄▄▄▄
   ║ F ║       │                                             ░░░░█
   ║ O ║    50─┤     ░░ Sort                                 ░░░░█
   ║ R ║       │                                             ░░░░█
   ║ M ║       │                      37%                    ░░░░█
   ║ A ║    40─┤                      ▄▄▄▄ 35%           34% ░░░░█
   ║ N ║       │                     ▒▒▒▒█ ▄▄▄▄          ▄▄▄▄░░░░█
   ║ C ║       │                     ▒▒▒▒█░░░░█         ▒▒▒▒█░░░░█
   ║ E ║    30─┤        24%          ▒▒▒▒█░░░░█         ▒▒▒▒█░░░░█
   ║   ║       │        ▄▄▄▄         ▒▒▒▒█░░░░█         ▒▒▒▒█░░░░█
   ║ I ║       │   19% ░░░░█         ▒▒▒▒█░░░░█         ▒▒▒▒█░░░░█
   ║ M ║    20─┤   ▄▄▄▄░░░░█         ▒▒▒▒█░░░░█         ▒▒▒▒█░░░░█
   ║ P ║       │  ▒▒▒▒█░░░░█         ▒▒▒▒█░░░░█         ▒▒▒▒█░░░░█
   ║ R ║       │  ▒▒▒▒█░░░░█         ▒▒▒▒█░░░░█         ▒▒▒▒█░░░░█
   ║ O ║    10─┤  ▒▒▒▒█░░░░█         ▒▒▒▒█░░░░█         ▒▒▒▒█░░░░█
   ║ V ║       │  ▒▒▒▒█░░░░█         ▒▒▒▒█░░░░█         ▒▒▒▒█░░░░█
   ║ E ║       │  ▒▒▒▒█░░░░█         ▒▒▒▒█░░░░█         ▒▒▒▒█░░░░█
   ║ M ║     0─┴──▒▒▒▒▀░░░░▀────┬────▒▒▒▒▀░░░░▀────┬────▒▒▒▒▀░░░░▀──┐
   ╚═T═╝      Paradox 2.0 in the   Paradox OS/2 in    Paradox 386 under
              OS/2 compatability   protected mode     DOS 3.3

Performance improvements of three new versions of Paradox over Paradox 2.0
running in MS-DOS 3.3. These figures are preliminary and may not reflect
actual performance when the products are shipped.


Converting Windows Applications For Microsoft(R) OS/2 Presentation Manager

Micheal Geary☼

The Microsoft(R) OS/2 Presentation Manager represents the marriage of two
technologies: the Windows operating environment and the protected-mode OS/2
operating systems. In any wedding, it's traditional to have something old,
something new, something borrowed, and something blue, and Presentation
Manager is no exception.

Something old is Microsoft Windows. In a sense, Presentation Manager is
really Windows. Although there have been a number of changes from the real-
mode (MS-DOS(R)) version of Windows, nearly all Windows programming concepts
carry over directly to the new environment: message-driven programming,
window functions, resources, and so forth. If you know how to program
Windows, Presentation Manager will be the icing on the wedding cake.

Something new is the protected-mode hardware and OS/2 operating systems. It
would not be far from the truth to say this is the environment Windows was
really meant for. OS/2 offers true preemptive multitasking, unlike the
"round-robin" multitasking of real-mode Windows. Applications no longer have
to wait for each other to yield control. Within a single application, you
can make good use of OS/2's multiple threads of execution, assigning to
different windows their own threads. The protected-mode hardware greatly
expands memory addressing capability and eliminates the need for some of the
software memory management done by real-mode Windows.

Something borrowed is the new Graphics Programming Interface (GPI). Adapted
from the mainframe graphics GDDM (Graphics Data Display Manager) and GCP
(Graphics Control Program) standards, GPI replaces Windows' Graphics Device
Interface (GDI) with a richer, more versatile set of graphics functions.

Something blue, as you've already guessed, is IBM. Presentation Manager
plays a crucial role in IBM's Systems Application Architecture (SAA) as the
user interface portion of SAA, at least for machines with graphics
capabilities. IBM has an ambitious goal for SAA: a common user interface and
programming interface on a wide range of machines, from PCs to mainframes.
Eventually you should be able to take a Presentation Manager application and
compile it to run on any machine. For machines without graphics, a subset of
Presentation Manager functions will be provided, giving a similar user
interface, but running in a character mode environment.

Presentation Manager Programming

To explore the differences and the similarities between a Presentation
Manager application and a Windows application, let's look at a sample
program called SayWhat, which is my entry in the "silliest Windows
application" contest. I started out with the Hello program that comes with
the Windows Software Development Kit and doctored it up a bit. Instead of
just displaying the "hello" message in one place, the text wanders around
the window randomly, changing its color as it moves. If you make SayWhat
into an icon, it spells out the message one letter at a time, with the
letter wandering around the icon. A control panel, a modeless dialog box,
lets you change the displayed text and adjust how fast it moves and how
often it selects a new random direction to move in.

The source code for SayWhat is found at the end of the text

SayWhat illustrates several programming techniques that are useful for
Windows and Presentation Manager. It also points out an irritating flaw in
the way many Windows applications do their screen painting and how to
improve it. A Windows version and a Presentation Manager version of SayWhat
are listed side by side for comparison.

When comparing the listings, it is immediately apparent that the overall
structure of the two programs is identical. The most important aspects of
Windows apply equally to Presentation Manager: messages, window functions,
painting, and so on. However, at the detailed coding level we will see many

Getting Started

At the beginning, both programs have a main function, but in the
Presentation Manager application it's called main( ) instead of WinMain( )
as in the Windows application. That's because a Presentation Manager
application starts out as an ordinary OS/2 application; what makes it
different is its use of the Presentation Manager API.

The parameters passed to main are different from those that are passed to
WinMain in the Windows version. The lpszCmdLine parameter is replaced by the
more conventional, and convenient, argc and argv parameters. There is no
nCmdShow parameter. If you create your main application window as an
ordinary visible window by using the WinCreateStdWindow function, this is
taken care of for you along with the window size. If you require more direct
control over your window placement, create your main window as invisible and
then set the size and position yourself. Finally, the hPrevInstance and
hInstance parameters are missing: hPrevInstance isn't used at all and there
is nothing in Presentation Manager corresponding to the GetInstanceData
function. Under OS/2, there is no such thing as an instance in the same
sense as there is in Windows. There are only OS/2 processes, and each
process initializes its own data. Subsequent instances of an application
have exactly the same initialization as the first instance.

Message Loop

The main message loop of a Presentation Manager program is nearly identical
to the main loop of a Windows program. Other than some name changes and the
use of the hAB parameter, the only difference is that the TranslateMessage
function is not used. Since this function was always required anyway, it has
been merged with WinGetMsg. Actually, the keyboard messages under
Presentation Manager have been substantially simplified. The messages
been removed, and the information that they provided is incorporated into
WM_CHAR. The WM_CHAR message now provides the "raw" keyboard information
that the WM_KEYDOWN/UP messages used to give, as well as the "translated"
ASCII character code always provided by WM_CHAR.


Before the message loop starts up, we have to initialize the application and
create our main window, which is done in the SayInitApp function. The
Presentation Manager version begins with two new calls: first, it calls
WinInitialize which initializes the program for Presentation Manager and
returns a handle to an anchor block, called hAB in our code. The anchor
block isn't really used by Presentation Manager, but it's there for
compatibility with future SAA environments. However, the hAB parameter is
required on several of the Presentation Manager function calls.

WinInitialize doesn't actually make the application become a Presentation
Manager application. In fact, kernel applications can call WinInitialize if
they want to use the local memory management or atom functions provided with
Presentation Manager. Then it calls WinCreateMsgQueue, which allocates its
message queue. Unlike Windows, in Presentation Manager you have to do this
yourself. This gives you more flexibility; you can specify the size of the
message queue or just take the default by specifying a size of 0, as SayWhat

WinCreateMsgQueue is that special call that says "yes, this is a
Presentation Manager application." It establishes this thread of execution
as a message queue thread, and if you happen to start up a Presentation
Manager application on a system that has the regular kernel shell running
instead of the Presentation Manager shell, this is the call that puts you
into graphics mode. If you have multiple threads, some of the threads may
have message queues and some may not, but any thread that wants to create
windows must call WinCreateMsgQueue since threads cannot share a queue.

Both applications then load some string resources. This is nearly identical
in the two versions, except for the hAB parameter used in the Presentation
Manager version. Next we pick up display information: the number of colors
available, the system font size, and the screen size. In the Windows
version, you create an information context which is like a device context,
but is used only for gathering information about the device. Then you pick
up information on the system font with GetTextMetrics, and get the number of
display planes with a GetDeviceCaps call.

The Presentation Manager version is a little different. Instead of an
information context or device context, it gets a presentation space with the
WinGetPS call. Although Presentation Manager also has device contexts, most
graphics functions use a presentation space instead, which, in a simple
application like SayWhat, can be used much as a device context would be in
the Windows version. However, the presentation space is much more powerful
and versatile; for example, it can record and later play back graphics
calls, much like a metafile. You could avoid having to process WM_PAINT
messages this way. (This approach does not enhance performance, but it can
be a programming convenience for some applications.) The details of
presentation spaces are not discussed here, since SayWhat doesn't do
anything fancy with them.

Having obtained the presentation space handle, the Presentation Manager
version then calls GpiQueryCharBox to get the system font character size and
GpiQueryColorData to pick up the number of colors. Note that we're using the
total number of distinct solid colors here, not the number of display planes
as in the Windows version.

Creating a Window

Now you can register your window class and create the main window. It is
here that you encounter a major difference between Windows and Presentation
Manager-the architecture of a standard application window.

Under Windows, a standard application window has two main components: the
client area, which your code normally deals with, and the nonclient area,
which includes all the controls around the edge of the window; title bar,
sizing border, menu bar, and so forth. Your window function receives
messages for the client and nonclient areas, but you usually pass the
WM_NCxxxxx nonclient messages through to DefWindowProc. Although this works
for most applications, this architecture has some problems. The behavior of
all of the nonclient items is hard-coded into Windows. It is possible to
override some of this behavior by intercepting the WM_NCxxxxx messages, but
this is fairly cumbersome and very limited. There are also some strange
inconsistencies, like the fact that standard scroll bars are part of the
special nonclient area, but any non-standard scroll bars are created as
child windows and are treated differently.

When I was coding the window editor in one of my applications, this
client/nonclient architecture was a serious obstacle. I needed complete
control over all aspects of a window's behavior, because my application lets
the user build and edit window characteristics on the fly. In design mode, I
didn't want the nonclient items to behave normally, since they were objects
being edited at that point. I finally got things to work reasonably well,
but not quite the way I wanted. For example, if the user wished to remove
the Maximize icon from a window, I would have to destroy and recreate the
entire window.

Under Presentation Manager, this client/nonclient architecture has been
eliminated. The functionality is still there, but child windows and some new
predefined window classes implement it. All of the items that were part of
the nonclient area are now child windows, and you can do special things with
them simply by subclassing them as you would any other window. There is a
new window class, the frame window, which acts as a container for all these
child windows and has the smarts to position them appropriately according to
the window size. What used to be the client area under Windows is now itself
a child window of the frame window.

Now, for many applications, all of this won't make much difference. You can
use the WinCreateStdWindow function to set up a standard frame window with
the desired controls around the border, and things will work pretty much as
before. The only significant difference is that some operations that were
performed with Windows function calls are now done by sending messages to
the various controls.

However, in an application where the window frame controls are treated
specially, this new architecture makes life much easier. Subclassing a child
window is a lot more convenient than trying to fool all of the WM_NCxxxxx
messages; it's a simpler, more consistent architecture. It is even
reasonable to add more special controls to the window frame, placed anywhere
you want.

There's another, more subtle area where the window architecture has been
cleaned up. Under Windows, every window logically has a parent window and an
owner window. The parent determines how screen space is used-a child window
is contained within its parent and clipped at the edges of the parent. The
owner determines two things; control windows (edit controls, for example)
send notification messages to their owner, and pop-up windows disappear when
their owner is minimized. These are two different steps, although Windows
combines them into a single "parent" window handle and interprets this
handle according to the window style bits. For a WS_CHILD window, the
"parent" is actually both the parent and the owner. For a WS_POPUP, the
actual parent is NULL, and the "parent" is really the owner.

Presentation Manager separates the parent and owner window handles, letting
you specify them individually. This removes the need for the WS_CHILD,
WS_POPUP, and WS_OVERLAPPED window styles; instead, a window is just a
window. Furthermore, every window has a parent, because there is one new
window, called the desktop window, which occupies the entire screen. Every
top level window is actually a child of the desktop window.

With all this in mind, let's look at the WinRegisterClass and
WinCreateStdWindow calls in SayWhat. You'll see that there isn't much to the
WinRegisterClass call. The WNDCLASS structure from the Windows version is
gone, because most of what it specified has migrated to the frame window
class and also to WinCreateStdWindow. One interesting new option in
WinRegisterClass is the CS_SYNCPAINT style bit. If this bit is set, then
WM_PAINT messages for windows of this class are not deferred in the
customary fashion. Instead, WM_PAINT is sent immediately whenever the window
becomes invalidated. This is how the window frame controls get repainted
immediately; they have the CS_SYNCPAINT style. In the Windows version, this
is handled specially by WM_PAINT, but under Presentation Manager, this
feature is available to any window. Since SayWhat's window painting
procedure is very simple and quite fast, CS_SYNCPAINT makes for a nice clean
display. A window class that takes longer to paint should omit this style
bit, and its windows will receive deferred WM_PAINT messages just as they do
under Windows.

The WinCreateStdWindow call creates SayWhat's frame window, along with its
client window and all the frame controls. The frame window handle and the
client window handle are returned by this call, the latter through the
pointer in the last parameter. The window style bits look pretty familiar,
but many of them have FS_ names instead of WS_ names because they are really
just indicators to the frame window telling it which child windows to
create. Also, there's no size or position information in this call. The
trick is that if you give the window the WS_VISIBLE style, it will
automatically be assigned the appropriate default size and position.
Alternatively, you can create it as invisible and place it where you want
(and then make it visible) explicitly with the call WinSetWindowPos.

WinCreateStdWindow is just a convenience for creating standard frame
windows. There is also a generic WinCreateWindow that can create any kind of
window, giving you total control over how the window is set up. You would
use this function to create additional child windows or any other kind of
special window.

Window Functions

Now that our window has been created, it's time to look at the window
function, SayWhatWndProc. You'll notice that the window functions in the two
versions are very similar. As I mentioned before, all of the keyboard input
is done under a WM_CHAR message, not under WM_KEYDOWN as in the Windows
version, but otherwise it's essentially the same. Mouse input is nearly
identical also, except that the message names have been changed to avoid the
inconsistency of the right button sending a message called WM_LBUTTONUP/DOWN
when the buttons have been swapped-the main button is instead WM_BUTTON1.

The other messages processed by SayWhatWndProc are also extremely similar;
apparent difference is that there is no corresponding wParem = = SIZEICONIC,
but there is a new WM_MINMAX message that provides the same information.
WM_MINMAX is thus intercepted and processed to find out when the window is
being minimized or restored to the normal state.

There are some differences in the message parameters for all messages,
though. Instead of a 16-bit parameter and a 32-bit parameter, wParam and
lParam, there are two 32-bit parameters; lParam1 and lParam2. This allows a
little more information to be passed along with messages, but the real
reason for this change was for those messages that pass an additional window
handle in wParam, such as WM_VSCROLLCLIPBOARD. Under Presentation Manager,
window handles and most other handles are 32 bits, not 16 bits, so the
wParam had to be expanded to permit this. The hWnd parameter to the window
function is also a 32-bit parameter. Watch out for this one if you have any
code that assumes handles are 16 bits.

Window Painting

The SayWhatPaint function handles window painting in both versions. It is
rather similar in the two versions, although the Presentation Manager
version has no PAINTSTRUCT. Instead, the WinBeginPaint function returns a
presentation space handle and gives you the update rectangle as a separate
parameter. In addition, it makes use of GpiSetColor instead of using
SetTextColor, and uses GpiCharStringAt instead of TextOut. The same work is
being performed in both cases.

There's a little twist in SayWhat's painting logic in both versions. When
you run SayWhat, you will notice that the text wanders around the window
cleanly. There is no flicker, even though the text is repainted from scratch
each time. Many Windows applications have a little bit of flicker when they
paint into their window, which is caused by the usual practice of erasing
the background completely and then painting the nonblank portions. That's
fine when you paint a window for the first time, but if you want to repaint
a portion of the window, there is a split second where the background is
blanked out.

You've probably seen this effect running Notepad or Write. If you type
quickly in either program, the line you are on will flicker a little instead
of painting cleanly. In SayWhat, you can see the same problem by selecting
the Flicker option on the control dialog box. It's even worse here because
the window gets repainted so frequently.

How does SayWhat avoid this flicker in its normal mode? Simple: it doesn't
erase the background underneath the text it's about to paint. It does erase
any other portions of the background if needed, but where the text itself
will go, it just writes the text without erasing first. This works because
text is normally written opaquely; it completely covers whatever is
underneath it.

There are two different painting routines to illustrate this in
SayWhatPaint. If the bCleanPaint variable is FALSE, it uses the common,
flickery painting method, that is, it first erases the entire background and
then draws the text. If bCleanPaint is TRUE, it draws the text and erases
only the remaining portions of the background.

It may seem silly to get worked up about this, but fixing it dramatically
improves the appearance of an application. I know this from my own
experience: the outline editor in my own application originally had a lot of
irritating flicker, which went away when I converted it to use the clean
painting method. And, even though it took a little extra code in the paint
function, it actually reduced the overall amount of code and complexity. The
flicker had been so annoying that I was ready to put in all kinds of special
cases to make sure I never invalidated more of the window than was
necessary, but with the clean painting method, it no longer matters.
Painting is totally invisible to the user now, so if I invalidate a little
too much, no one is the wiser.

Dialog Boxes

SayWhat has two dialog boxes: a modal About box and a modeless control
panel, which are both created under the WM_COMMAND case in SayWhatWndProc.
The code in both versions is similar except that MakeProcInstance is not
necessary in the protected-mode environment. OS/2 and the memory management
hardware set up the proper data segment for "call-back" functions such as
dialog functions. This is a blessing for anyone who has struggled with
mysterious system crashes caused by leaving out a MakeProcInstance under
Windows. However, you still need to remember to EXPORT your call back
functions and window functions, as was the case before.

There is one important difference in the implementation of the dialog box
functions themselves. Under Windows, a dialog box function is not coded like
a normal window function. In fact, the actual window function for all dialog
boxes is a private function inside Windows called DlgWndProc, which calls
your dialog function before doing its own message switch statement and if it
returns nonzero, it skips its own message processing. One problem with this
approach is that it's a little confusing to have dialog functions work
differently from normal window functions. Another problem is that it is
impossible for a dialog function to return a value back to a SendMessage
call. The return value you give isn't returned back through SendMessage;
DlgWndProc simply returns zero. (Odd as it may seem, DlgWndProc treats the
WM_CTLCOLOR message as a special case and does return the value you

Under Presentation Manager, a dialog function operates like any other window
function. Its return value is the real return value, and there is a default
message function, WinDefDlgProc, that you call for messages that you don't
process yourself. SayAboutDlgProc illustrates this difference and, like most
About boxes, doesn't do much else except close itself when you hit the OK

The other dialog box, SayWhat's control panel, is a little more interesting.
This dialog box has no owner window (parent window under Windows). Usually,
you specify an owner window when you create a dialog box, which causes three
things to happen: the box is automatically positioned relative to the
parent, it is always on top of the parent, and it disappears if the parent
is made into an icon. However, in SayWhat, I didn't want the latter two
actions; I wanted the box to remain visible if SayWhat's window itself is
made into an icon so that you could fiddle with the parameters while the
icon is displayed. I also wanted to be able to put SayWhat's main window on
top of the dialog box.

Creating the dialog box with a NULL owner takes care of this; however, I
still wanted the box to be positioned relative to the main window instead of
being relative to the screen. So, the WM_INITDIALOG code does this
positioning explicitly, using ClientToScreen calls in the Windows version
and a WinMapWindowPoints call in the Presentation Manager version.
WinMapWindowPoints is a handy function, which replaces ClientToScreen and
ScreenToClient, since you can specify a source window, a destination window,
or both. It will also map as many points as you want in one call. In this
program it maps two points, that is, one rectangle.

After the mapping, you have to make sure that you haven't moved the dialog
box partially offscreen, so you check it against the screen boundaries and
adjust it back if necessary. Then, you call SetWindowPos or WinSetWindowPos
to set the new position. This function works a little differently under
Presentation Manager. If you have used SetWindowPos in Windows 2.0, you know
it can do a number of things at once, such as move the window, resize it, or
change the window ordering. The default is for it to do all of these, and in
the last parameter you tell it what you do not want it to do.
WinSetWindowPos does the same, except the last parameter tells it what you
do want it to do-just the opposite.

Note that the y parameter of WinSetWindowPos takes the window bottom, not
the window top. This shows one subtle difference between Windows and
Presentation Manager: vertical coordinates run from bottom to top, not top
to bottom, that is, position (0,0) is the lower-left corner of a window or
the screen, not the upper-left corner. To be consistent with this, a
rectangle structure is now stored in the order (left, bottom, right, top)
instead of (left, top, right, bottom). If you're familiar with the various
GDI mapping modes available in Windows, you have probably noticed that they
all run from bottom to top except for MM_TEXT, which runs from top to
bottom. This change in Presentation Manager removes that inconsistency──all
coordinates normally run the same direction now──but it obviously requires
some conversion effort. You can instruct Presentation Manager to use the
top-to-bottom coordinates within a window, so that could help in converting
existing applications.

After positioning the dialog box, the WM_INITDIALOG code initializes its
control windows. One item of interest here is the scroll bar initialization
in SayInitBar. The Windows version calls two scroll bar functions,
SetScrollRange and SetScrollPos, to initialize the scroll bar. Presentation
Manager deals with these two at the same time, with a single
SBM_SETSCROLLBAR message that sets the position and range. Also, you will
find throughout Presentation Manager that this is performed with a message
instead of a special function; many of the special-purpose functions for
control windows have been removed in favor of messages. If you prefer the
approach of calling functions, you can always write your own equivalent
functions that send the appropriate messages.

The other interesting messages in SayWhatDlgProc are WM_COMMAND and
WM_HSCROLL. The latter message gets sent for activity in either of the
dialog box's scroll bars, and it calls SayDoBarMsg to set the new scroll bar
position and to set the corresponding edit control value. However, in
Presentation Manager this message passes you the child window ID of the
scroll bar control instead of the window handle. This is convenient, since
you need the window ID to tell which scroll bar you're dealing with. The
WM_COMMAND message is sent when the user clicks any of the pushbuttons or
radio buttons in the dialog box. (It also gets sent when you type in one of
the edit controls, but SayWhat ignores it.) The WM_COMMAND processing is
nearly identical in the two versions, except that the IsDlgButtonChecked
function call is replaced with a BM_QUERYCHECK message in Presentation


As you can see, converting a Windows application to Presentation Manager
isn't trivial, but it is pretty straightforward. The similarities between
the two systems far outweigh the differences. If you're fluent in Windows
programming, you've already done the hard part, and you know how different a
Windows application is from a conventional MS-DOS or OS/2 program. If you
haven't gotten started with Windows yet, well.... What's that I hear in the
distance? Yes, it's wedding bells. Don't be late!

Source code in this article is referenced W for Windows programs and PM for
Presentation Manager programs.

Figure 1W:  SAYWHAT is the MAKE File for SAYWHAT

#  MAKE file for SAYWHAT (Windows version)

sw.obj:  sw.c  sw.h
    cl -c -AS -DLINT_ARGS -Gcsw -Oas -W3 -Zdp sw.c

sw.res:  sw.rc  sw.h
    rc -r sw.rc

saywhat.exe:  sw.obj  sw.res  sw.def
    link4 sw, saywhat/align:16, saywhat/map/line, slibw, sw.def
    mapsym saywhat
    rc sw.res saywhat.exe

Figure 1PM:  SAYWHAT is the MAKE File for SAYWHAT

# MAKE file for SAYWHAT (Presentation Manager version)

swp.obj:    swp.c  swp.h
    cl -c -AS -DLINT_ARGS -G2csw -Oat -W3 -Zp swp.c

swp.res:    swp.rc  swp.h
    rc -r swp.rc

saywhatp.exe:  swp.obj  swp.res  swp.def
    link @swp.lnk
    mapsym saywhat
    rc swp.res saywhat.exe

Figure 2W:

Note that there is no Windows equivalent for SAYWHATP.LNK, the LINK file
for the PM version of SAYWHAT.

Figure 2PM:  SWP.LNK is the Link File for SAYWHAT


wincalls doscalls mlibc286/NOD

Figure 3W:  SW.DEF is the Module Definition File for SAYWHAT

; SW.DEF - Module definition file for SAYWHAT (Windows version)

NAME    SayWhat





    SayAboutDlgProc     @1
    SayWhatDlgProc      @2
    SayWhatWndProc      @3

Figure 3PM:  SWP.DEF is the Module Definition File for SAYWHAT

; SWP.DEF - Module definition file for SAYWHAT (PM version)

NAME    SayWhat





    SayAboutDlgProc     @1
    SayWhatDlgProc      @2
    SayWhatWndProc      @3

Figure 4W:  SW.RC is the Resource File for SAYWHAT


#include <style.h>
#include "sw.h"

    STR_NAME,    "SayWhat!"
    STR_TITLE,   "Say What!"
    STR_WHAT,    "Hello Windows!"

SayWhat!    MENU

  POPUP "&Say"
    MENUITEM "&What..."                 , CMD_WHAT
    MENUITEM "E&xit"                    , CMD_EXIT
    MENUITEM "A&bout SayWhat!..."       , CMD_ABOUT


DLG_ABOUT DIALOG 19, 17, 130, 83
  CTEXT "Microsoft Windows",   -1,  0,  8, 127,  8
  CTEXT "Say What!",           -1,  0, 18, 127,  8
  CTEXT "Version 1.00",        -1,  0, 30, 127,  8
  CTEXT "By Michael Geary",    -1,  0, 44, 129,  8
  DEFPUSHBUTTON "Ok",        IDOK, 48, 62,  32, 14

DLG_WHAT DIALOG 49, 41, 177, 103
CAPTION "Say What!"
 CONTROL "Say &What:"   -1,            "Static",    SS_LEFT  | WS_GROUP,
 CONTROL ""             ITEM_WHAT,     "Edit",      ES_LEFT  | ES_AUTOHSCROLL
 CONTROL "&Time Delay:" -1,            "Static",    SS_LEFT,
 CONTROL ""             ITEM_INTBAR,   "ScrollBar", SBS_HORZ | WS_TABSTOP,
 CONTROL ""             ITEM_INTERVAL, "Edit",      ES_LEFT  | WS_BORDER | WS
 CONTROL "&Stability:"  -1,            "Static",    SS_LEFT,
 CONTROL ""             ITEM_DISTBAR,  "ScrollBar", SBS_HORZ | WS_TABSTOP,
 CONTROL ""             ITEM_DISTANCE, "Edit",      ES_LEFT | WS_BORDER | WS_
 CONTROL "Painting:"    -1,            "Static",    SS_LEFT,
 CONTROL "&Clean"       ITEM_CLEAN,    "Button",    BS_AUTORADIOBUTTON | WS_G
 CONTROL "Enter"        IDOK,          "Button",    BS_DEFPUSHBUTTON | WS_GRO
 CONTROL "Esc=Close"    IDCANCEL,      "Button",    BS_PUSHBUTTON | WS_TABSTO

Figure 4PM:  SWP.RC is the Resource File for SAYWHAT


#include <style.h>
#include "sw.h"

  STR_NAME,    "SayWhat!"
  STR_TITLE,   "Say What!"
  STR_WHAT,    "Hello Windows!"

MENU SayWhat!
  SUBMENU "~Say",                  -1
    MENUITEM "~What...",           CMD_WHAT
    MENUITEM "E~xit",              CMD_EXIT
    MENUITEM "A~bout SayWhat!...", CMD_ABOUT

  DIALOG "", DLG_ABOUT, 19, 17, 130, 83, FS_DLGBORDER |
    CTEXT "Microsoft Windows",   -1,  0,  8, 127,  8
    CTEXT "Say What!",           -1,  0, 18, 127,  8
    CTEXT "Version 1.00",        -1,  0, 30, 127,  8
    CTEXT "By Michael Geary",    -1,  0, 44, 129,  8
    DEFPUSHBUTTON "Ok",        IDOK, 48, 62,  32, 14

  DIALOG "Say What!", DLG_WHAT, 49, 41, 177, 103,
  CONTROL "Say ~What:"   -1,              8, 10,  40,  8, WC_STATIC,    SS_LE
  CONTROL ""             ITEM_WHAT,      56,  8, 112, 12, WC_EDIT,      ES_LE
  CONTROL "~Time Delay:" -1,              8, 28,  53,  9, WC_STATIC,    SS_LE
  CONTROL ""             ITEM_INTBAR,    56, 28,  88,  8, WC_SCROLLBAR, SBS_H
  CONTROL ""             ITEM_INTERVAL, 152, 26,  16, 12, WC_EDIT,      ES_LE
  CONTROL "~Stability:"  -1,              8, 46,  56,  9, WC_STATIC,    SS_LE
  CONTROL ""             ITEM_DISTBAR,   56, 46,  88,  8, WC_SCROLLBAR, SBS_H
  CONTROL ""             ITEM_DISTANCE, 152, 44,  16, 12, WC_EDIT,      ES_LE
  CONTROL "Painting:"    -1,              8, 64,  40,  8, WC_STATIC,    SS_LE
  CONTROL "~Clean"       ITEM_CLEAN,     56, 62,  36, 12, WC_BUTTON,    BS_AU
  CONTROL "~Flicker"     ITEM_FLICKER,   96, 62,  42, 12, WC_BUTTON,    BS_AU
  CONTROL "Enter"        IDOK,           24, 82,  48, 14, WC_BUTTON,    BS_DE
  CONTROL "Esc=Close"    IDCANCEL,      104, 82,  48, 14, WC_BUTTON,    BS_PU

Figure 5W:  SW.H is the C Header file for SAYWHAT

/* SW.H */
/* SW.H - C header file for SAYWHAT (Windows version) */

/* String table constants */

#define STR_NAME        101
#define STR_TITLE       102
#define STR_WHAT        103

/* Menu command IDs */

#define CMD_ABOUT       201
#define CMD_EXIT        202
#define CMD_WHAT        203

/* Dialog box resource IDs */

#define DLG_ABOUT       301
#define DLG_WHAT        302

/* 'What...' dialog box item IDs */

#define ITEM_WHAT       401
#define ITEM_INTBAR     402
#define ITEM_INTERVAL   403
#define ITEM_DISTBAR    404
#define ITEM_DISTANCE   405
#define ITEM_CLEAN      406
#define ITEM_FLICKER    407

/* Timer IDs */

#define TIMER_MOVE      501
#define TIMER_CHAR      502

Figure 5PM:  SWP.H is the C Header File for SAYWHAT

/* SWP.H */
/* SWP.H - C header file for SAYWHAT (PM version) */

/* String table constants */

#define STR_NAME        101
#define STR_TITLE       102
#define STR_WHAT        103

/* Menu command IDs */

#define CMD_ABOUT       201
#define CMD_EXIT        202
#define CMD_WHAT        203

/* Dialog box resource IDs */

#define DLG_ABOUT       301
#define DLG_WHAT        302

/* 'What...' dialog box item IDs */

#define ITEM_WHAT       401
#define ITEM_INTBAR     402
#define ITEM_INTERVAL   403

#define ITEM_DISTBAR    404
#define ITEM_DISTANCE   405
#define ITEM_CLEAN      406
#define ITEM_FLICKER    407

/* Timer IDs */

#define TIMER_MOVE      501
#define TIMER_CHAR      502

/* Menu resource IDs */

#define MENU_WHAT       601

Figure 6W:  SW.C is the C Source Code Listing for SAYWHAT

/* SW.C
 * SW.C - C code for SayWhat - Windows version

/* Note - this code *must* be compiled with the -Gc switch */

#ifndef LINT_ARGS
#define LINT_ARGS  /* turn on argument checking for C runtime */

#include <windows.h>
#include <limits.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "sw.h"

#define MIN_INTERVAL    1       /* limits for nInterval */
#define MAX_INTERVAL    999

#define MIN_DISTANCE    1       /* limits for nDistance */
#define MAX_DISTANCE    99

/*  Static variables  */

HANDLE  hInstance;              /* application instance handle */

HWND    hWndWhat;               /* main window handle */
HWND    hWndPanel;              /* contral panel dialog handle */

FARPROC lpfnDlgProc;            /* ProcInstance for dialog */

char    szAppName[10];          /* window class name */
char    szTitle[15];            /* main window title */

char    szText[40];             /* current "what" text */
NPSTR   pText = szText;         /* ptr into szText for icon mode */
char    cBlank = ' ';           /* a blank we can point to */

RECT    rcText;                 /* current text rectangle */
POINT   ptAdvance;              /* increments for SayAdvanceText */
POINT   ptCharSize;             /* X and Y size of a character */
POINT   ptScreenSize;           /* X and Y size of the screen */

int     nDisplayPlanes;         /* number of display planes */
DWORD   rgbTextColor;           /* current text color */
HBRUSH  hbrBkgd;                /* brush for erasing background */

int     nInterval = 40;         /* current "Interval" setting */
int     nDistance = 30;         /* current "Distance" setting */
int     nDistLeft = 0;          /* change direction when hits 0 */
BOOL    bCleanPaint = TRUE;     /* clean or flickery painting? */
BOOL    bMouseDown = FALSE;     /* is mouse down? */
BOOL    bIconic = FALSE;        /* is main window iconic? */

/*  Full prototypes for our functions to get type checking  */

BOOL FAR SayAboutDlgProc( HWND, unsigned, WORD, LONG );
void     SayAdvanceTextChar( HWND );
void     SayAdvanceTextPos( HWND );
void     SayChangeColor( HWND );
void     SayDoBarMsg( HWND, HWND, WORD, int );
void     SayFillRect( HDC, int, int, int, int );
void     SayInitBar( HWND, int, int, int, int );
BOOL     SayInitApp( HANDLE, int );
void     SayInvalidateText( HWND );
void     SayLimitTextPos( HWND );
void     SayMoveText( HWND, POINT );
void     SaySetBar( HWND, int *, int );
void     SayExitApp( int );
BOOL FAR SayWhatDlgProc( HWND, unsigned, WORD, LONG );
void     SayWhatPaint( HWND );
LONG FAR SayWhatWndProc( HWND, unsigned, WORD, LONG );
void     WinMain( HANDLE, HANDLE, LPSTR, int );

/*  Dialog function for the "About" box  */

BOOL FAR SayAboutDlgProc( hWndDlg, wMsg, wParam, lParam )
    HWND        hWndDlg;
    unsigned    wMsg;
    WORD        wParam;
    LONG        lParam;
    switch( wMsg )
      case WM_COMMAND:
        switch( wParam )
          case IDOK:
            EndDialog( hWndDlg, TRUE );
            return TRUE;
    return FALSE;

/*  Advances to next display character in iconic mode.
 *  Forces in a blank when it reaches the end of string.

void SayAdvanceTextChar( hWnd )
    HWND        hWnd;
    if( ! bIconic )

    if( pText = = &cBlank )
        pText = szText;
    else if( ! *(++pText) )
        pText = &cBlank;

    SayChangeColor( hWnd );
    SayInvalidateText( hWnd );

/*  Advances text position according to ptAdvance.  Decrements
 *  nDistLeft first, and when it reaches zero, sets a new
 *  randomized ptAdvance and nDistLeft, also changes color.
 *  Does nothing if mouse is down, so text will track mouse.

void SayAdvanceTextPos( hWnd )
    HWND        hWnd;
    int         i;

    if( bMouseDown )

    SayInvalidateText( hWnd );

    if( nDistLeft- < 0 ) {
        nDistLeft = rand() % nDistance + 1;
        do {
            i = rand();
            ptAdvance.x = (
                i < SHRT_MAX/3   ? -1
              : i < SHRT_MAX/3*2 ?  0
              :                     1
            i = rand();
            ptAdvance.y = (
                i < SHRT_MAX/3   ? -1
              : i < SHRT_MAX/3*2 ?  0
              :                     1
        } while( ptAdvance.x = = 0  &&  ptAdvance.y = = 0 );

        if( ! bIconic )
            SayChangeColor( hWnd );
    } else {
        rcText.left   += ptAdvance.x;
        rcText.right  += ptAdvance.x;
        rcText.top    += ptAdvance.y;
        rcText.bottom += ptAdvance.y;

    SayInvalidateText( hWnd );

/*  Changes color to a random selection, if color is available.
 *  Forces a color change - if the random selection is the same
 *  as the old one, it tries again.

void SayChangeColor( hWnd )
    HWND        hWnd;
    HDC         hDC;
    DWORD       rgbNew;
    DWORD       rgbWindow;

    if( nDisplayPlanes <= 1 ) {
        rgbTextColor = GetSysColor(COLOR_WINDOWTEXT);
    } else {
        rgbWindow = GetSysColor(COLOR_WINDOW);
        hDC = GetDC( hWnd );
        do {
            rgbNew = GetNearestColor(
                MAKELONG( rand(), rand() ) & 0x00FFFFFFL
        } while( rgbNew = = rgbWindow || rgbNew = = rgbTextColor );
        rgbTextColor = rgbNew;
        ReleaseDC( hWnd, hDC );

/*  Handles scroll bar messages from the control dialog box.
 *  Adjusts scroll bar position, taking its limits into account,
 *  copies the scroll bar value into the adjacent edit control,
 *  then sets the nDistance or nInterval variable appropriately.

void SayDoBarMsg( hWndDlg, hWndBar, wCode, nThumb )
    HWND        hWndDlg;
    HWND        hWndBar;
    WORD        wCode;
    int         nThumb;
    int         nPos;
    int         nOldPos;
    int         nMin;
    int         nMax;
    int         idBar;

    idBar = GetWindowWord( hWndBar, GWW_ID );

    nOldPos = nPos = GetScrollPos( hWndBar, SB_CTL );
    GetScrollRange( hWndBar, SB_CTL, &nMin, &nMax );

    switch( wCode )
        case SB_LINEUP:         -nPos;              break;

        case SB_LINEDOWN:       ++nPos;             break;

        case SB_PAGEUP:         nPos -= 10;         break;

        case SB_PAGEDOWN:       nPos += 10;         break;

        case SB_THUMBPOSITION:
        case SB_THUMBTRACK:     nPos = nThumb;      break;

        case SB_TOP:            nPos = nMin;        break;

        case SB_BOTTOM:         nPos = nMax;        break;

    if( nPos < nMin )
        nPos = nMin;

    if( nPos > nMax )
        nPos = nMax;

    if( nPos = = nOldPos )

    SetScrollPos( hWndBar, SB_CTL, nPos, TRUE );

    SetDlgItemInt( hWndDlg, idBar+1, nPos, FALSE );

    switch( idBar )
      case ITEM_DISTBAR:
        nDistance = nPos;

      case ITEM_INTBAR:
        KillTimer( hWndWhat, TIMER_MOVE );
        nInterval = nPos;
        SetTimer( hWndWhat, TIMER_MOVE, nInterval, NULL );
        InvalidateRect( hWndWhat, NULL, FALSE );

/*  Terminates the application, freeing up allocated resources.
 *  Note that this function does NOT return to the caller, but
 *  exits the program.

void SayExitApp( nRet )
    int         nRet;
    if( GetModuleUsage(hInstance) = = 1 ) {
        DeleteObject( hbrBkgd );

    exit( nRet );

/*  Fills a specified rectangle with the background color.
 *  Checks that the rectangle is non-empty first.

void SayFillRect( hDC, nLeft, nTop, nRight, nBottom )
    HDC         hDC;
    int         nLeft;
    int         nTop;
    int         nRight;
    int         nBottom;

    RECT        rcFill;

    if( nLeft >= nRight  ||  nTop >= nBottom )

    SetRect( &rcFill, nLeft, nTop, nRight, nBottom );

    FillRect( hDC, &rcFill, hbrBkgd );

/*  Initializes the application.  */

BOOL SayInitApp( hPrevInstance, nCmdShow )

    HANDLE      hPrevInstance;
    int         nCmdShow;
    WNDCLASS    Class;
    HDC         hDC;
    TEXTMETRIC  Metrics;

        hInstance, STR_NAME,  szAppName, sizeof(szAppName)
        hInstance, STR_TITLE, szTitle,   sizeof(szTitle)
        hInstance, STR_WHAT,  szText,    sizeof(szText)

    hDC = CreateIC( "DISPLAY", NULL, NULL, NULL );
    GetTextMetrics( hDC, &Metrics );
    nDisplayPlanes = GetDeviceCaps( hDC, PLANES );
    DeleteDC( hDC );

    ptCharSize.x = Metrics.tmMaxCharWidth;
    ptCharSize.y = Metrics.tmHeight;

    ptScreenSize.x = GetSystemMetrics(SM_CXSCREEN);
    ptScreenSize.y = GetSystemMetrics(SM_CYSCREEN);

    if( ! hPrevInstance ) {

        hbrBkgd = CreateSolidBrush( GetSysColor(COLOR_WINDOW) );

        Class.style         = 0; /* CS_HREDRAW | CS_VREDRAW; */
        Class.lpfnWndProc   = SayWhatWndProc;
        Class.cbClsExtra    = 0;
        Class.cbWndExtra    = 0;
        Class.hInstance     = hInstance;
        Class.hIcon         = NULL;
        Class.hCursor       = LoadCursor( NULL, IDC_ARROW );
        Class.hbrBackground = COLOR_WINDOW + 1;
        Class.lpszMenuName  = szAppName;
        Class.lpszClassName = szAppName;

        if( ! RegisterClass( &Class ) )
            return FALSE;

    } else {

            hPrevInstance, (NPSTR)&hbrBkgd, sizeof(hbrBkgd)

    hWndWhat = CreateWindow(
        CW_USEDEFAULT, 0,
        CW_USEDEFAULT, 0,

    if( ! hWndWhat )
        return FALSE;

    ShowWindow( hWndWhat, nCmdShow );
    UpdateWindow( hWndWhat );

    return TRUE;

/*  Initializes one scroll bar in the control dialog.  */

void SayInitBar( hWndDlg, idBar, nValue, nMin, nMax )
    HWND        hWndDlg;
    int         idBar;
    int         nValue;

    int         nMin;
    int         nMax;
    HWND        hWndBar;

    hWndBar = GetDlgItem( hWndDlg, idBar );

    SetScrollRange( hWndBar, SB_CTL, nMin, nMax, FALSE );
    SetScrollPos( hWndBar, SB_CTL, nValue, FALSE );

    SetDlgItemInt( hWndDlg, idBar+1, nValue, FALSE );

/*  Invalidates the text within the main window, adjusting the
 *  text rectangle if it's gone out of bounds.

void SayInvalidateText( hWnd )
    HWND        hWnd;
    SayLimitTextPos( hWnd );
    InvalidateRect( hWnd, &rcText, FALSE );

/*  Checks the text position against the window client area
 *  rectangle.  If it's moved off the window in any direction,
 *  forces it back inside, and also reverses the ptAdvance value
 *  for that direction so it will "bounce" off the edge.  Handles
 *  both the iconic and open window cases.

void SayLimitTextPos( hWnd )
    HWND        hWnd;
    RECT        rcClient;
    POINT       ptTextSize;

    ptTextSize = ptCharSize;

    if( ! bIconic ) {
        pText = szText;
        ptTextSize.x *= strlen(szText);

    GetClientRect( hWndWhat, &rcClient );

    if( rcText.left > rcClient.right - ptTextSize.x ) {
        rcText.left = rcClient.right - ptTextSize.x;
        ptAdvance.x = -ptAdvance.x;

    if( rcText.left < rcClient.left ) {
        rcText.left = rcClient.left;
        ptAdvance.x = -ptAdvance.x;

    if( rcText.top > rcClient.bottom - ptTextSize.y ) {
        rcText.top = rcClient.bottom - ptTextSize.y;
        ptAdvance.y = -ptAdvance.y;

    if( rcText.top < rcClient.top ) {
        rcText.top = rcClient.top;
        ptAdvance.y = -ptAdvance.y;

    rcText.right  = rcText.left + ptTextSize.x;
    rcText.bottom = rcText.top  + ptTextSize.y;

/*  Moves the text within the window, by invalidating the old
 *  position, adjusting rcText according to ptMove, and then
 *  invalidating the new position.

void SayMoveText( hWnd, ptMove )
    HWND        hWnd;
    POINT       ptMove;
    SayInvalidateText( hWnd );
    rcText.left = ptMove.x - (rcText.right  - rcText.left >> 1);
    rcText.top  = ptMove.y - (rcText.bottom - rcText.top  >> 1);
    SayInvalidateText( hWnd );

/*  Sets one of the dialog scroll bars to *pnValue.  If that
 *  value is out of range, limits it to the proper range and
 *  forces *pnValue to be within the range as well.

void SaySetBar( hWndDlg, pnValue, idBar )
    HWND        hWndDlg;
    int *       pnValue;
    int         idBar;
    HWND        hWndBar;
    int         nMin;
    int         nMax;
    int         nValue;
    BOOL        bOK;

    hWndBar = GetDlgItem( hWndDlg, idBar );

    GetScrollRange( hWndBar, SB_CTL, &nMin, &nMax );

    nValue = GetDlgItemInt( hWndDlg, idBar+1, &bOK, FALSE );

    if( bOK  &&  nValue >= nMin  &&  nValue <= nMax ) {
        *pnValue = nValue;
        SetScrollPos( hWndBar, SB_CTL, nValue, TRUE );
    } else {
            GetScrollPos( hWndBar, SB_CTL ),

/*  Dialog function for the control panel dialog box.  */

BOOL FAR SayWhatDlgProc( hWndDlg, wMsg, wParam, lParam )
    HWND        hWndDlg;
    unsigned    wMsg;
    WORD        wParam;
    LONG        lParam;
    HWND        hWndBar;
    RECT        rcWin;
    int         n;

    switch( wMsg )
      case WM_COMMAND:
        switch( wParam )
          case IDOK:
            KillTimer( hWndWhat, TIMER_MOVE );
                hWndDlg, ITEM_WHAT, szText, sizeof(szText)
            if( strlen(szText) = = 0 )
                    hInstance, STR_WHAT, szText, sizeof(szText)
            pText = szText;
            SaySetBar( hWndDlg, &nInterval, ITEM_INTBAR );
            SaySetBar( hWndDlg, &nDistance, ITEM_DISTBAR );
            SetTimer( hWndWhat, TIMER_MOVE, nInterval, NULL );
            InvalidateRect( hWndWhat, NULL, FALSE );
            return TRUE;

          case IDCANCEL:
            DestroyWindow( hWndDlg );
            return TRUE;

          case ITEM_CLEAN:
          case ITEM_FLICKER:
            bCleanPaint =
                IsDlgButtonChecked( hWndDlg, ITEM_CLEAN );
            return TRUE;
        return FALSE;

      case WM_HSCROLL:
        if( HIWORD(lParam) )
                hWndDlg, HIWORD(lParam), wParam, LOWORD(lParam)
        return TRUE;

      case WM_INITDIALOG:
        GetWindowRect( hWndDlg, &rcWin );
        ClientToScreen( hWndWhat, (LPPOINT)&rcWin.left );
        ClientToScreen( hWndWhat, (LPPOINT)&rcWin.right );
        n = rcWin.right - ptScreenSize.x + ptCharSize.x;
        if( n > 0 )
            rcWin.left -= n;
        rcWin.left &= ~7;  /* byte align */
        n = rcWin.bottom - ptScreenSize.y + ptCharSize.y;
        if( n > 0 )
            rcWin.top -= n;
            0, 0,
        SetDlgItemText( hWndDlg, ITEM_WHAT, szText );
            hWndDlg, ITEM_WHAT,
            EM_LIMITTEXT, sizeof(szText)-1, 0L
            hWndDlg, ITEM_INTBAR,
            nInterval, MIN_INTERVAL, MAX_INTERVAL

            hWndDlg, ITEM_DISTBAR,
            nDistance, MIN_DISTANCE, MAX_DISTANCE
        CheckDlgButton( hWndDlg, ITEM_CLEAN, TRUE );
        return TRUE;

      case WM_NCDESTROY:
        FreeProcInstance( lpfnDlgProc );
        hWndPanel = NULL;
        return FALSE;
    return FALSE;

/*  Painting procedure for the main window.  Handles both the
 *  clean and flickery painting methods for demonstration
 *  purposes.

void SayWhatPaint( hWnd )
    HWND        hWnd;

    BeginPaint( hWnd, &ps );

    SetTextColor( ps.hdc, rgbTextColor );

    SayLimitTextPos( hWnd );

    if( bCleanPaint ) {

        /* Clean painting, avoid redundant erasing */

            bIconic ? 1 : strlen(szText)





    } else {

        /* Flickery painting, erase background and paint traditionally */

        FillRect( ps.hdc, &ps.rcPaint, hbrBkgd );

            bIconic ? 1 : strlen(szText)


    EndPaint( hWnd, &ps );

    if( ! nInterval )
        SayAdvanceTextPos( hWnd );
/*  Window function for the main window.  */

LONG FAR SayWhatWndProc( hWnd, wMsg, wParam, lParam )
    HWND        hWnd;
    unsigned    wMsg;
    WORD        wParam;
    LONG        lParam;
    FARPROC     lpfnAbout;

    switch( wMsg )
      case WM_COMMAND:
        switch( wParam )
          case CMD_ABOUT:
            lpfnAbout =
                MakeProcInstance( SayAboutDlgProc, hInstance );
                hInstance, MAKEINTRESOURCE(DLG_ABOUT),
                hWnd, lpfnAbout
            FreeProcInstance( lpfnAbout );
            return 0L;

          case CMD_EXIT:
            DestroyWindow( hWndWhat );
            return 0L;

          case CMD_WHAT:
            if( hWndPanel ) {
                BringWindowToTop( hWndPanel );
            } else {
                lpfnDlgProc =
                    MakeProcInstance( SayWhatDlgProc, hInstance );
                if( ! lpfnDlgProc )
                    return 0L;
                hWndPanel = CreateDialog(
                if( ! hWndPanel )
                    FreeProcInstance( lpfnDlgProc );

      case WM_CREATE:
        srand( (int)time(NULL) );
        SetTimer( hWnd, TIMER_MOVE, nInterval, NULL );
        return 0L;

      case WM_DESTROY:
        if( hWndPanel )
            DestroyWindow( hWndPanel );
        PostQuitMessage( 0 );
        return 0L;

      case WM_KEYDOWN:
        SayInvalidateText( hWnd );
        switch( wParam )
          case VK_LEFT:
            rcText.left -= ptCharSize.x;
            ptAdvance.x  = -1;
            ptAdvance.y  = 0;

          case VK_RIGHT:
            rcText.left += ptCharSize.x;
            ptAdvance.x  = 1;
            ptAdvance.y  = 0;

          case VK_UP:
            rcText.top  -= ptCharSize.y >> 1;
            ptAdvance.x  = 0;
            ptAdvance.y  = -1;

          case VK_DOWN:
            rcText.top  += ptCharSize.y >> 1;
            ptAdvance.x  = 0;
            ptAdvance.y  = 1;

            return 0L;
        SayInvalidateText( hWnd );
        nDistLeft = nDistance;
        return 0L;

      case WM_LBUTTONDOWN:
        if( bMouseDown )
        KillTimer( hWnd, TIMER_MOVE );
        bMouseDown = TRUE;
        SetCapture( hWnd );
        SayMoveText( hWnd, MAKEPOINT(lParam) );

      case WM_LBUTTONUP:
        if( ! bMouseDown )
        bMouseDown = FALSE;
        SayMoveText( hWnd, MAKEPOINT(lParam) );
        SetTimer( hWnd, TIMER_MOVE, nInterval, NULL );

      case WM_MOUSEMOVE:
        if( bMouseDown )
            SayMoveText( hWnd, MAKEPOINT(lParam) );

      case WM_PAINT:
        SayWhatPaint( hWnd );
        return 0L;

      case WM_SIZE:
        if( wParam = = SIZEICONIC ) {
            if( ! bIconic )
                SetTimer( hWnd, TIMER_CHAR, 1000, NULL );
            bIconic = TRUE;
        } else {
            if( bIconic )
                KillTimer( hWnd, TIMER_CHAR );
            bIconic = FALSE;
        SayInvalidateText( hWnd );
        nDistLeft = 0;
        SayAdvanceTextPos( hWnd );
        return 0L;

      case WM_TIMER:
        switch( wParam )
          case TIMER_MOVE:
            SayAdvanceTextPos( hWnd );

          case TIMER_CHAR:
            SayAdvanceTextChar( hWnd );
        return 0L;
    return DefWindowProc( hWnd, wMsg, wParam, lParam );

/*  Main function for the application.  */

void WinMain( hInst, hPrevInst, lpszCmdLine, nCmdShow )
    HANDLE      hInst;
    HANDLE      hPrevInst;
    LPSTR       lpszCmdLine;
    int         nCmdShow;
    MSG         msg;

    hInstance = hInst;

    if( ! SayInitApp( hPrevInst, nCmdShow ) )
        SayExitApp( 1 );

    while( GetMessage( &msg, NULL, 0, 0 ) ) {

        if( hWndPanel  &&  IsDialogMessage( hWndPanel, &msg ) )

        TranslateMessage( &msg );
        DispatchMessage( &msg );

    SayExitApp( msg.wParam );

Figure 6PM:  SWP.C is the C Source Code Listing for SAYWHAT

 *  SWP.C - C code for SayWhat - Presentation Manager version

#ifndef LINT_ARGS
#define LINT_ARGS  /* turn on argument checking for C runtime */

#include <os2pm.h>
#include <limits.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include ╙swp.h╙

#define MIN_INTERVAL    1       /* limits for nInterval */
#define MAX_INTERVAL    999

#define MIN_DISTANCE    1       /* limits for nDistance */
#define MAX_DISTANCE    99

#define COLORDATAMAX    5

/*  Static variables  */

HAB     hAB = NULL;             /* anchor block handle */
HMQ     hMsgQ = NULL;           /* message queue handle */

HWND    hWndWhatFrame;          /* frame window handle */
HWND    hWndWhat;               /* client window handle */
HWND    hWndPanel;              /* control panel dialog handle */

CHAR    szAppName[10];          /* window class name */
CHAR    szTitle[15];            /* main window title */

CHAR    szText[40];             /* current 'what' text */
PSZ     npszText = szText;      /* ptr into szText for icon mode */
CHAR    cBlank = ╒ ╒;           /* a blank we can point to */

RECT    rcText;                 /* current text rectangle */
POINT   ptAdvance;              /* increments for SayAdvanceText */
POINT   ptCharSize;             /* X and Y size of a character */
POINT   ptScreenSize;           /* X and Y size of the screen */

LONG    lColorMax;              /* number of available colors */
LONG    lColor;                 /* current text color index */

SHORT   nInterval = 40;         /* current 'Interval' setting */
SHORT   nDistance = 30;         /* current 'Distance' setting */
SHORT   nDistLeft = 0;          /* change direction when hits 0 */
BOOL    bCleanPaint = TRUE;     /* clean or flickery painting? */
BOOL    bMouseDown = FALSE;     /* is mouse down? */
BOOL    bIconic = FALSE;        /* is main window iconic? */

/*  Full prototypes for our functions to get type checking  */

VOID             SayAdvanceTextChar( HWND );
VOID             SayAdvanceTextPos( HWND );
VOID             SayChangeColor( HWND );
VOID             SayDoBarMsg( HWND, USHORT, USHORT, SHORT );
VOID             SayExitApp( INT );
VOID             SayFillRect( HPS, SHORT, SHORT, SHORT, SHORT );
VOID             SayInitBar( HWND, SHORT, SHORT, SHORT, SHORT );
BOOL             SayInitApp( VOID );
VOID             SayInvalidateText( HWND );
VOID             SayLimitTextPos( HWND );
VOID             SayMoveText( HWND, POINT );
VOID             SaySetBar( HWND, SHORT *, SHORT );
VOID             SayWhatPaint( HWND );
void  cdecl      main( INT, PSZ );

/*  Dialog function for the 'About' box  */

ULONG FAR PASCAL SayAboutDlgProc( hWndDlg, wMsg, lParam1, lParam2 )
    HWND        hWndDlg;
    USHORT      wMsg;
    ULONG       lParam1;
    ULONG       lParam2;
    switch( wMsg )
      case WM_COMMAND:
        switch( LOUSHORT(lParam1) )
          case IDOK:
            WinDismissDlg( hWndDlg, TRUE );
    return WinDefDlgProc( hWndDlg, wMsg, lParam1, lParam2 );

/*  Advances to next display character in iconic mode.
 *  Forces in a blank when it reaches the end of string.

VOID SayAdvanceTextChar( hWnd )
    HWND        hWnd;
    if( ! bIconic )

    if( npszText = = &cBlank )
        npszText = szText;
    else if( ! *(++npszText) )
        npszText = &cBlank;

    SayChangeColor( hWnd );
    SayInvalidateText( hWnd );

/*  Advances text position according to ptAdvance. Decrements
 *  nDistLeft first, and when it reaches zero, sets a new
 *  randomized ptAdvance and nDistLeft, also changes color.
 *  Does nothing if mouse is down, so text will track mouse.

VOID SayAdvanceTextPos( hWnd )
    HWND        hWnd;
    SHORT       i;

    if( bMouseDown )

    SayInvalidateText( hWnd );

    if( nDistLeft- < 0 ) {
        nDistLeft = rand() % nDistance + 1;
        do {
            i = rand();
            ptAdvance.x = (
                i < SHRT_MAX/3   ? -1

              : i < SHRT_MAX/3*2 ?  0
              :                     1
            i = rand();
            ptAdvance.y = (
                i < SHRT_MAX/3   ? -1
              : i < SHRT_MAX/3*2 ?  0
              :                     1
        } while( ptAdvance.x = = 0  &&  ptAdvance.y = = 0 );
        if( ! bIconic )
            SayChangeColor( hWnd );
    } else {
        rcText.xLeft   += ptAdvance.x;
        rcText.xRight  += ptAdvance.x;
        rcText.yTop    += ptAdvance.y;
        rcText.yBottom += ptAdvance.y;

    SayInvalidateText( hWnd );

/*  Changes color to a random selection, if color is available.
 *  Forces a color change - if the random selection is the same
 *  as the old one, it tries again.

VOID SayChangeColor( hWnd )
    HWND        hWnd;
    HPS         hPS;
    LONG        lWindow;
    LONG        lNew;

    hPS = WinGetPS( hWnd );

    if( lColorMax <= 2 ) {
        lColor = GpiQueryColorIndex(
            WinQuerySysColor( hAB, SCLR_WINDOWTEXT ),

    } else {
        lWindow = GpiQueryColorIndex(
            WinQuerySysColor( hAB, SCLR_WINDOW ),
        do {
            lNew = rand() % lColorMax;
        } while( lNew = = lWindow || lNew = = lColor );
        lColor = lNew;

    WinReleasePS( hPS );


/*  Handles scroll bar messages from the control dialog box.
 *  Adjusts scroll bar position, taking its limits into account,
 *  copies the scroll bar value into the adjacent edit control,
 *  then sets the nDistance or nInterval variable appropriately.

VOID SayDoBarMsg( hWndDlg, idBar, wCode, nThumb )
    HWND        hWndDlg;
    USHORT      idBar;
    USHORT      wCode;
    SHORT       nThumb;
    SHORT       nPos;
    SHORT       nOldPos;
    ULONG       lRange;

    nOldPos = nPos =
            hWndDlg, idBar, SBM_QUERYPOS, 0L, 0L

    lRange =
            hWndDlg, idBar, SBM_QUERYRANGE, 0L, 0L

    switch( wCode ) {

        case SB_LINEUP:         -nPos;                    break;

        case SB_LINEDOWN:       ++nPos;                   break;

        case SB_PAGEUP:         nPos -= 10;               break;

        case SB_PAGEDOWN:       nPos += 10;               break;

        case SB_THUMBPOSITION:
        case SB_THUMBTRACK:     nPos = nThumb;            break;

        case SB_TOP:            nPos = LOUSHORT(lRange);  break;

        case SB_BOTTOM:         nPos = HIUSHORT(lRange);  break;


    if( nPos < LOUSHORT(lRange) )
        nPos = LOUSHORT(lRange);

    if( nPos > HIUSHORT(lRange) )
        nPos = HIUSHORT(lRange);

    if( nPos = = nOldPos )

        hWndDlg, idBar, SBM_SETPOS, (LONG)nPos, 0L

    WinSetDlgItemShort( hWndDlg, idBar+1, nPos, FALSE );

    switch( idBar )
      case ITEM_DISTBAR:
        nDistance = nPos;

      case ITEM_INTBAR:
        WinStopTimer( hAB, hWndWhat, TIMER_MOVE );
        nInterval = nPos;
        WinStartTimer( hAB, hWndWhat, TIMER_MOVE, nInterval );

        WinInvalidateRect( hWndWhat, NULL );

/*  Terminates the application, freeing up allocated resources.
 *  Note that this function does NOT return to the caller, but
 *  exits the program.

VOID SayExitApp( nRet )
    INT         nRet;
    if( hWndWhatFrame )
        WinDestroyWindow( hWndWhatFrame );

    if( hMsgQ )
        WinDestroyMsgQueue( hMsgQ );

    if( hAB )

        WinTerminate( hAB );

    exit( nRet );

/*  Fills a specified rectangle with the background color.
 *  Checks that the rectangle is non-empty first.

VOID SayFillRect( hPS, xLeft, xBottom, xRight, xTop )
    HPS         hPS;
    SHORT       xLeft;
    SHORT       xBottom;
    SHORT       xRight;
    SHORT       xTop;

    RECT        rcFill;

    if( xLeft >= xRight  ||  xBottom >= xTop )


    WinSetRect( hAB, &rcFill, xLeft, xBottom, xRight, xTop );

        WinQuerySysColor( hAB, SCLR_WINDOW )

/*  Initializes the application.  */

BOOL SayInitApp()
    HPS         hPS;
    BOOL        bOK;

    hAB = WinInitialize();
    if( ! hAB )

        return FALSE;

    hMsgQ = WinCreateMsgQueue( hAB, 0 );
    if( ! hMsgQ )
        return FALSE;

        hAB, NULL, STR_NAME,  szAppName, sizeof(szAppName)
        hAB, NULL, STR_TITLE, szTitle,   sizeof(szTitle)
        hAB, NULL, STR_WHAT,  szText,    sizeof(szText)

    bOK = WinRegisterClass(

    if( ! bOK )
        return FALSE;

    hWndWhatFrame = WinCreateStdWindow(

    if( ! hWndWhatFrame )
        return FALSE;

    WinShowWindow( hWndWhat, TRUE );

    return TRUE;

/*  Initializes one scroll bar in the control dialog.  */

VOID SayInitBar( hWndDlg, idBar, nValue, nMin, nMax )
    HWND        hWndDlg;
    SHORT       idBar;
    SHORT       nValue;
    SHORT       nMin;

    SHORT       nMax;
    HWND        hWndBar;

    hWndBar = WinWindowFromID( hWndDlg, idBar );


        MAKELONG( nMin, nMax )

    WinSetDlgItemShort( hWndDlg, idBar+1, nValue, FALSE );

/*  Invalidates the text within the main window, adjusting the
 *  text rectangle if it's gone out of bounds.

VOID SayInvalidateText( hWnd )
    HWND        hWnd;
    SayLimitTextPos( hWnd );
    WinInvalidateRect( hWnd, &rcText );

/*  Checks the text position against the window client area
 *  rectangle. If it's moved off the window in any direction,
 *  forces it back inside, and also reverses the ptAdvance value
 *  for that direction so it will 'bounce' off the edge. Handles
 *  both the iconic and open window cases.

VOID SayLimitTextPos( hWnd )
    HWND        hWnd;
    RECT        rcClient;
    POINT       ptTextSize;

    ptTextSize = ptCharSize;

    if( ! bIconic ) {
        npszText = szText;
        ptTextSize.x *= strlen(szText);

    WinQueryWindowRect( hWndWhat, &rcClient );

    if( rcText.xLeft > rcClient.xRight - ptTextSize.x ) {
        rcText.xLeft = rcClient.xRight - ptTextSize.x;
        ptAdvance.x = -ptAdvance.x;

    if( rcText.xLeft < rcClient.xLeft ) {
        rcText.xLeft = rcClient.xLeft;
        ptAdvance.x = -ptAdvance.x;

    if( rcText.yBottom < rcClient.yBottom ) {
        rcText.yBottom = rcClient.yBottom;
        ptAdvance.y = -ptAdvance.y;

    if( rcText.yBottom > rcClient.yTop - ptTextSize.y ) {
        rcText.yBottom = rcClient.yTop - ptTextSize.y;
        ptAdvance.y = -ptAdvance.y;

    rcText.xRight = rcText.xLeft   + ptTextSize.x;
    rcText.yTop   = rcText.yBottom + ptTextSize.y;


/*  Moves the text within the window, by invalidating the old
 *  position, adjusting rcText according to ptMove, and then
 *  invalidating the new position.

VOID SayMoveText( hWnd, ptMove )
    HWND        hWnd;
    POINT       ptMove;
    SayInvalidateText( hWnd );
    rcText.xLeft   =
        ptMove.x - (rcText.xRight - rcText.xLeft    >> 1);
    rcText.yBottom =
        ptMove.y - (rcText.yTop   - rcText.yBottom  >> 1);
    SayInvalidateText( hWnd );

/*  Sets one of the dialog scroll bars to *pnValue.  If that value
 *  is out of range, limits it to the proper range and forces
 *  *pnValue to be within the range as well.

VOID SaySetBar( hWndDlg, pnValue, idBar )
    HWND        hWndDlg;
    SHORT *     pnValue;
    SHORT       idBar;
    ULONG       lRange;
    SHORT       nValue;
    BOOL        bOK;

    lRange =
            hWndDlg, idBar, SBM_QUERYRANGE, 0L, 0L

    nValue =
        WinQueryDlgItemShort( hWndDlg, idBar+1, &bOK, FALSE );

        bOK  &&
        nValue >= LOUSHORT(lRange)  &&
        nValue <= HIUSHORT(lRange)
    ) {
        *pnValue = nValue;
            hWndDlg, idBar, SBM_SETPOS, (LONG)nValue, (LONG)TRUE
    } else {
            idBar + 1,
                hWndDlg, idBar, SBM_QUERYPOS, 0L, 0L

/*  Dialog function for the control panel dialog box.  */

ULONG FAR PASCAL SayWhatDlgProc( hWndDlg, wMsg, lParam1, lParam2 )
    HWND        hWndDlg;
    USHORT      wMsg;
    ULONG       lParam1;
    ULONG       lParam2;
    HWND        hWndBar;
    RECT        rcWin;
    SHORT       n;

    switch( wMsg )
    case WM_COMMAND:
        switch( LOUSHORT(lParam1) )
          case IDOK:
            WinStopTimer( hAB, hWndWhat, TIMER_MOVE );

                WinWindowFromID( hWndDlg, ITEM_WHAT ),
            if( strlen(szText) = = 0 )
                    hAB, NULL, STR_WHAT, szText, sizeof(szText)
            npszText = szText;
            SaySetBar( hWndDlg, &nInterval, ITEM_INTBAR );
            SaySetBar( hWndDlg, &nDistance, ITEM_DISTBAR );
            WinStartTimer( hAB, hWndWhat, TIMER_MOVE, nInterval );
            WinInvalidateRect( hWndWhat, NULL );

          case IDCANCEL:
            WinDestroyWindow( hWndDlg );

          case ITEM_CLEAN:
          case ITEM_FLICKER:
            bCleanPaint = (BOOL)WinSendDlgItemMsg(

                hWndDlg, ITEM_CLEAN,
                BM_QUERYCHECK, 0L, 0L

    case WM_DESTROY:
        hWndPanel = NULL;

    case WM_HSCROLL:
            hWndDlg, LOUSHORT(lParam1),
            HIUSHORT(lParam2), LOUSHORT(lParam2)

    case WM_INITDLG:
        WinQueryWindowRect( hWndDlg, &rcWin );
            hWndWhat, (HWND)NULL, (LPPOINT)&rcWin.xLeft, 2

        n = rcWin.xRight - ptScreenSize.x + ptCharSize.x;
        if( n > 0 )
            rcWin.xLeft -= n;
        rcWin.xLeft &= ~7;  /* byte align */
        n = rcWin.yTop - ptScreenSize.y + ptCharSize.y;
        if( n > 0 )
            rcWin.yBottom -= n;
            0, 0,
            WinWindowFromID( hWndDlg, ITEM_WHAT ), szText
            hWndDlg, ITEM_WHAT, EM_SETTEXTLIMIT,
            (LONG)sizeof(szText)-1, 0L
            hWndDlg, ITEM_INTBAR,  nInterval,
            hWndDlg, ITEM_DISTBAR, nDistance,
            hWndDlg, ITEM_CLEAN, BM_SETCHECK, (LONG)TRUE, 0L
    return WinDefDlgProc( hWndDlg, wMsg, lParam1, lParam2 );

/*  Painting procedure for the main window. Handles both the
 *  clean and flickery painting methods for demonstration
 *  purposes.

VOID SayWhatPaint( hWnd )
    HWND        hWnd;
    HPS         hPS;
    RECT        rcPaint;
    GPOINT      gptText;

    hPS = WinBeginPaint( hWnd, (HPS)NULL, &rcPaint );

    GpiSetColor( hPS, lColor );

    SayLimitTextPos( hWnd );

    gptText.x = (LONG)rcText.xLeft;
    gptText.y = (LONG)rcText.yBottom;

    if( bCleanPaint ) {

        /* Clean painting, avoid redundant erasing */

        GpiSetBackMix( hPS, MIX_OVERPAINT );

            (LONG)( bIconic ? 1 : strlen(szText) ),





     } else {

        /* Flickery painting, erase background and paint traditionally */

            WinQuerySysColor( hAB, SCLR_WINDOW )

            (LONG)( bIconic ? 1 : strlen(szText) ),

     WinEndPaint( hPS );

     if( ! nInterval )
         SayInvalidateText( hWnd );

/*  Window function for the main window.  */

ULONG FAR PASCAL SayWhatWndProc( hWnd, wMsg, lParam1, lParam2 )
    HWND        hWnd;
    USHORT      wMsg;
    ULONG       lParam1;
    ULONG       lParam2;
    POINT       ptMouse;
    GSIZE       gsChar;
    HPS         hPS;
    LONG        ColorData[COLORDATAMAX];
    BOOL        bNowIconic;

    switch( wMsg )
      case WM_BUTTON1DOWN:
        if( bMouseDown )
        WinStopTimer( hAB, hWnd, TIMER_MOVE );

        bMouseDown = TRUE;
        WinSetCapture( hAB, hWnd );
        ptMouse.x = LOUSHORT(lParam1);
        ptMouse.y = HIUSHORT(lParam1);
        SayMoveText( hWnd, ptMouse );
        return 0L;

      case WM_BUTTON1UP:
        if( ! bMouseDown )
        bMouseDown = FALSE;
        WinSetCapture( hAB, (HWND)NULL );
        ptMouse.x = LOUSHORT(lParam1);
        ptMouse.y = HIUSHORT(lParam1);
        SayMoveText( hWnd, ptMouse );
        WinStartTimer( hAB, hWnd, TIMER_MOVE, nInterval );
        return 0L;

      case WM_CHAR:
            ( LOUSHORT(lParam1) & KC_KEYUP )  ||
            ! ( LOUSHORT(lParam1) & KC_VIRTUALKEY )

        ) {
        SayInvalidateText( hWnd );
        switch( HIUSHORT(lParam2) )
          case VK_LEFT:
            rcText.xLeft -= ptCharSize.x;
            ptAdvance.x   = -1;
            ptAdvance.y   = 0;
          case VK_RIGHT:
            rcText.xLeft += ptCharSize.x;
            ptAdvance.x   = 1;
            ptAdvance.y   = 0;
          case VK_UP:
            rcText.yBottom -= ptCharSize.y >> 1;
            ptAdvance.x     = 0;
            ptAdvance.y     = -1;
          case VK_DOWN:

            rcText.yBottom += ptCharSize.y >> 1;
            ptAdvance.x     = 0;
            ptAdvance.y     = 1;
            return 0L;
        SayInvalidateText( hWnd );
        nDistLeft = nDistance;
        return 0L;

      case WM_COMMAND:
        switch( LOUSHORT(lParam1) )
          case CMD_ABOUT:
                (HWND)NULL, hWnd, (LPFNWP)SayAboutDlgProc,
                NULL, DLG_ABOUT, NULL
            return 0L;

          case CMD_EXIT:

            WinDestroyWindow( hWndWhatFrame );
            return 0L;

          case CMD_WHAT:
            if( hWndPanel ) {
                    0, 0, 0, 0,
                    SWP_ZORDER | SWP_ACTIVATE
            } else {
                hWndPanel = WinLoadDlg(

        return 0L;

      case WM_CREATE:
        /* find out character/screen sizes, number of colors */
        hPS = WinGetPS( hWnd );
        GpiQueryCharBox( hPS, &gsChar );
        GpiQueryColorData( hPS, (LONG)COLORDATAMAX, ColorData );
        WinReleasePS( hPS );
        lColorMax = ColorData[3];
        ptCharSize.x = gsChar.width;
        ptCharSize.y = gsChar.height;
        ptScreenSize.x =
          WinQuerySysValue( (HWND)NULL, SV_CXSCREEN );
        ptScreenSize.y =
          WinQuerySysValue( (HWND)NULL, SV_CYSCREEN );
        /* initialize timer */
        srand( (INT)time(NULL) );
        WinStartTimer( hAB, hWnd, TIMER_MOVE, nInterval );
        return 0L;

      case WM_DESTROY:
        if( hWndPanel )

            WinDestroyWindow( hWndPanel );
        WinPostQueueMsg( hMsgQ, WM_QUIT, 0L, 0L );
        return 0L;

        return 1L;  /* don't erase */

      case WM_MINMAX:
        bNowIconic = ( LOUSHORT(lParam1) = = SWP_MINIMIZE );
        if( bIconic != bNowIconic ) {
            bIconic = bNowIconic;
            if( bIconic )
                WinStopTimer( hAB, hWnd, TIMER_CHAR );
                WinStartTimer( hAB, hWnd, TIMER_CHAR, 1000 );
        return 1L;

      case WM_MOUSEMOVE:
        if( bMouseDown ) {
            ptMouse.x = LOUSHORT(lParam1);
            ptMouse.y = HIUSHORT(lParam1);

            SayMoveText( hWnd, ptMouse );
        return 0L;

      case WM_PAINT:
        SayWhatPaint( hWnd );
        return 0L;

      case WM_SIZE:
        SayInvalidateText( hWnd );
        nDistLeft = 0;
        SayAdvanceTextPos( hWnd );
        return 0L;

      case WM_TIMER:
        switch( LOUSHORT(lParam1) ) {
            case TIMER_MOVE:
                SayAdvanceTextPos( hWnd );
            case TIMER_CHAR:
                SayAdvanceTextChar( hWnd );

        return 0L;
    return WinDefWindowProc( hWnd, wMsg, lParam1, lParam2 );

/*  Main function for the application.  */

void cdecl main( nArgs, pArgs )
    INT         nArgs;
    PSZ         pArgs;
    QMSG        qMsg;

    if( ! SayInitApp() )
        SayExitApp( 1 );

    while( WinGetMsg( hAB, &qMsg, (HWND)NULL, 0, 0 ) ) {

        if( hWndPanel  &&  WinProcessDlgMsg( hWndPanel, &qMsg ) )


        WinDispatchMsg( hAB, &qMsg );

    SayExitApp( 0 );


Programming Considerations in Porting to Microsoft XENIX System V/386

Also see the related article:
  Demand Paging and Virtual Memory

Martin Dunsmuir

XENIX(R) System V/386 is the Microsoft version of the UNIX(R) Operating
System ported to the Intel(R) 386 microprocessor. The product, which was
released to OEMs in the summer of 1987, is the first from Microsoft to take
full advantage of the features of the Intel 386 microprocessor. In
particular, XENIX allows 32-bit applications to be written for the first

Microsoft has been active in the UNIX business since its inception, and in
fact, XENIX predates even MS-DOS(R) as a Microsoft product. Since 1983, the
XENIX development effort has concentrated on the Intel microprocessors──the
286 introduced by Intel in 1983 and more recently the 386. By concentrating
on one instruction set and chip architecture, Microsoft has been able to
develop the world's largest installed base of binary-compatible UNIX
systems. Currently, XENIX accounts for about 70 percent of all UNIX
licenses sold, or roughly 250,000 units, and more than 1000 different
applications are available. Although these numbers may sound small when
compared with the numbers quoted for MS-DOS, the majority of these systems
are used in multiuser configurations supporting between 2 and 16 users.

Most of the current XENIX installed base is on 286-based PCs such as the
IBM(R) PC AT(R). XENIX is used primarily as a cost-effective solution for
small businesses that require multiuser access; it has sold particularly
well in vertical markets that lend themselves to a customized "systems
solution." Two such markets are dentistry and accounting.

There was great excitement when Intel informed Microsoft that it was
planning the 386 chip-and Microsoft set out to find a way to take advantage
of the chip's features within the XENIX environment. In particular,
Microsoft wanted to give developers the ability to create 32-bit
applications and provide support within the operating system for virtual
memory and demand paging──features that can greatly increase the throughput
of a computer system.

Another major design goal was to be sure that existing XENIX 286 users were
not prevented from moving up to the 386. Microsoft wanted its installed base
to be able to take advantage of the increased performance without having to
buy new versions of their applications. Since the 386 chip supports both 16-
and 32-bit segments in its architecture, Microsoft has been able to create
an environment in which both 16- and 32-bit programs can be executed
simultaneously. The features of demand paging and virtual memory are
available transparently to both 16- and 32-bit applications.

Implementing support for segments independently of the paging subsystem
provides full performance for old applications without slowing down the
execution of the new 32-bit programs (see Figure 1).

Support for 32-bit applications is the key to the continued success of
XENIX. It opens the door to the creation of much more powerful programs, as
well as making it easier for developers to move existing UNIX applications
onto XENIX. UNIX 32-bit programs being ported to XENIX 286 often needed
extensive rewriting to make them work well in a 16-bit environment. This
article outlines the considerations that will help programmers in choosing
between 16- and 32-bit program models when developing applications or
migrating to XENIX System V/386.

Small-Model Programs

In 16-bit mode, as used on XENIX 286, a program is composed of two segments
up to 64Kb in size. One segment contains program code; the other segment
contains data (the stack is of fixed size and resides in the data segment).
An example of a small-model program is shown in Figure 2.

The program in Figure 2 has only one data and one code segment, so it is
not necessary to change the contents of the segment registers while the
program is running. At load time, the operating system initializes them
to point to the memory in which the program executes (via the LDT). In
particular, the compiler or assembler programmer does not have to take
account of changing segment registers during the program execution.

Also of note is the fact that in the small model, both integers and pointers
to data objects are 16-bit quantities and therefore interchangeable. This is
important because many programs implicitly make this assumption.
Unfortunately, although many commands and utilities are less than 64Kb in
size, most third-party applications are larger than this; they require
multiple code and/or data segment support. Figure 3 shows a multisegment

Large- and Middle-Model

Programs can overflow the 64Kb limit with their code or their data, or both
at the same time. Programs that exceed 64Kb of code but still have less than
64Kb of data are called middle-model programs. Large-model programs exceed
64Kb in both their code and data segments.

Large Code

The program is broken down by the linker into pieces that will fit into
64Kb. If a program's code exceeds 64Kb, then it is necessary to place
different parts of it in different segments. (Because the breaks occur on
function boundaries, each piece can be less than 64Kb in size.) The compiler
must also gener-ate code that automatically reloads the CS register when
the thread of execution moves from one segment to another. This results in a
program that is slightly slower than the equivalent small-model image. The
effect is not too drastic because within each subroutine CS remains constant
so the frequency of segment register reloads is relatively low in
comparison with the number of instructions executed.

Large Data

Data structures are spread among a number of different segments when data
exceeds 64Kb. Again, the linker fills each segment with data structures as
fully as possible. The performance penalty to be paid for going to large
data is much larger than in the case of code because the frequency of
intersegment data accesses is generally much greater than that of
intersegment branches. The C compiler does not know which segment a
particular data structure resides in at compile time; this causes a large
number of unnecessary intersegment calls to be generated. Another problem
in moving to large model is the fact that the size of data pointers
increases to 32 bits. This means that the size of an integer is no longer
equal to the size of a pointer, and programs that rely on this equality,
either implicitly or explicitly, break. This is one of the primary problems
developers experience when porting existing 32-bit UNIX programs to XENIX
286. A summary of the different models can be seen in Figure 4.

Hybrid Model

Using the 286, where 16-bit segments are the norm but most useful
applications exceed 64Kb in size, it is important for programmers to
understand how to design their programs to reduce the effect of the
multiple segment accesses. One way of doing this is to select specific data
structures to be placed in separate far segments, while keeping
indiscriminately accessed data structures (and the stack if possible) in a
single near segment. The code generated by the Microsoft(R) C compiler in
this case is much more efficient because the far keyword, used to mark
specific data structures, gives the compiler a hint as to when it should
reload segment registers.

Use of a hybrid model with carefully designed programs comes close to the
performance of small-model programs, even though they exceed 64Kb in size.
However, there is a down side to this approach──it makes programs inherently
less portable between XENIX 286 and other UNIX environments. Also,
converting an existing 32-bit UNIX application to a hybrid model is
complicated by the differences in pointer and integer size that make large-
model ports such a problem.

32-Bit Programming

In contrast to the complexity of a multisegment 286 program, the native 386
program structure is very simple (see Figure 5). Each program consists of
one code segment along with one data segment, and each segment can be very
large in size. (The exact limit depends on the availability of real
memory and swap space and is typically a few megabytes).

Because the address space is large, it is not necessary to support multiple
segments in 32-bit mode, either in the operating system or in the C
compiler. When a program is loaded, all the segment registers are
initialized to static values and remain unchanged while the program is
executing. In 32-bit mode, the stack lives in the data segment and grows
down to lower addresses, while the data segment extends upward to higher

XENIX 386 programs are truly 32-bit; they support 32-bit integers and all
pointers are 32 bits in length. This eases the problems of porting existing
32-bit applications to XENIX 386 in 32-bit mode.

Other advantages offered by the 32-bit mode of the 386 are more orthogonal
registers and addressing modes, which allow better code generation and more
register variables, plus extra instructions that improve in-line code
generation. Thirty-two-bit programs generally exhibit a significant
performance advantage over the 16-bit versions.

New XENIX Applications

The introduction of XENIX 386 no longer constrains the developer to the 16-
bit architecture. If he chooses, he can develop his application in 32-bit
mode. However, the choice between a 16- and a 32-bit architecture for a
new application is not as simple as it appears at first glance. 32-bit
programs will only execute on XENIX 386, whereas 16-bit applications will
execute on both XENIX 286 and XENIX 386. The installed base of XENIX 386 is
still small, but it is almost certain to exceed that of XENIX 286 in time. A
16-bit application may be a better choice for developers who want to address
the largest possible installed base. Let's look at the trade-offs that must
be considered when making the choice between 16 and 32 bits.

The developer should ask himself the following questions:

  ■  What is the size of the application, both code and data?

  ■  Is the application an existing UNIX program being ported to XENIX?

  ■  Is the application an existing MS-DOS program or aimed at the MS-DOS,
     OS/2, and UNIX markets?

  ■  For new applications, what is the target market for the application?
     Is it limited to XENIX or does it have wider appeal in other UNIX or
     DOS markets?

  ■  What are the application's performance requirements?

Application Size

In many ways size is the most important consideration; unfortunately for
new applications it is most likely the hardest to answer. For a simple
application it is probably wise to build the application first as a 32-
bit program and then see if it will fit into 16 bits. At this point it
should be remembered that large data is a much more serious performance
limitation to 16-bit programs-programs with more than 16 bits of code but
less than 16 bits of data can be built as 16-bit middle-model programs
without serious performance degradation.

Another approach that can be used to fit a more complex program into the
64Kb address space is to break it down into a number of separate,
communicating processes, each of which fits into the smaller address
space. Not all programs are amenable to such an architecture. Breaking an
application into pieces can also limit portability into the MS-DOS


Many developers of UNIX applications for UNIX systems other than XENIX 286
have programs that are designed implicitly for the 32-bit world. This is
because XENIX 286 is one of the few UNIX systems to run on a 16-bit
processor. Even if size is not a consideration, the work required to port
UNIX applications from 32 to 16 bits has often deterred developers from
doing the port.

Such developers are best advised to build their applications for XENIX
386 only. Debugging those problems summarized earlier in this article is
often too great an effort to warrant porting a program to XENIX 286. This
extra development effort can be considered for a later release if market
pressure is felt.

When porting existing MS-DOS applications to UNIX, it is usually more
feasible to build an application in 16 bits. This is certainly the best and
easiest option if the application contains a significant amount of
assembler code. Since the XENIX and MS-DOS macro assemblers accept the same
source syntax in 16-bit mode, assembly code that is not environment-specific
should port directly to XENIX.

Traditionally, UNIX and MS-DOS applications markets have been separated by a
wide gap in complexity. This is because the architecture of real-mode MS-
DOS programs is very different from UNIX. With the advent of OS/2, the
underlying support provided by the two operating systems is now comparable,
so it may make sense for new applications to be developed that can easily
be hosted in both XENIX and OS/2 environments. If this is the case, it makes
more sense to build the application for the 16-bit environment common to
both XENIX and OS/2 and to delay the development of a 32-bit application
until a 32-bit version of OS/2 becomes available.

Another consideration for simpler applications is the use of the C library
calls supported by the Microsoft C Compiler under MS-DOS. These calls, which
embody a subset of the UNIX C library calls, can make it relatively easy to
build a program that can be simply rehosted in both XENIX and MS-DOS
environments. A good example of this approach would be the Microsoft 286 C
compiler itself, which is recompiled and linked with different run-time
libraries for execution on MS-DOS or XENIX 286. The task of creating a
common source code for both MS-DOS and XENIX versions of the compiler is
greatly facilitated by the fact that the XENIX and MS-DOS linkers both
accept the same relocatable format as input (although they generate a
different executable file format).

XENIX and UNIX Markets

Building applications that port easily between XENIX 286 and other UNIX
platforms has generally been difficult. It is prudent──if a source portable
application is desired──to remain within the 32-bit world. The 32-bit XENIX
386 environment is completely compatible with the System V Interface
Definition (SVID), and thus there should be very little difficulty in moving
a carefully designed program from XENIX 386 to other UNIX platforms.


Although performance is a combination of many factors, it is most strongly
linked to the architecture of the program and to the inherent speed of the
host computer. All architectural considerations being equal, a 32-bit
program will execute faster than a 16-bit program on the same 386 CPU.
Applications that are being ported from the earlier 286 or 8086 worlds onto
the 386 will experience an increase in raw 16-bit performance, simply by
running the code on a 386, that more than offsets the need to consider
rehosting into 32-bit mode.

For new XENIX applications, especially those being ported from other 32-bit
processors, where a 16-bit port is a serious possibility, it is important to
understand the performance degradation seen on the 386 between 16-bit and
32-bit code. The operating system itself runs in 32-bit mode, and some part
of a program's execution time is spent in this code. The decrease in speed
when moving to 16 bits is not as great as a simple comparison of CPU-bound
16 and 32 performance might indicate. Figure 8 shows the relative
execution times of two small C programs, "Cpubound" and "IObound," built
as small-16, middle-16, large-16, and small-32 programs on XENIX 386.
Figures 6 and 7 show the source code of these programs .

An analysis of Figure 8 shows that the 32-bit architecture offers a
significant performance advantage for CPU-bound programs that do a mix of
arithmetic, pointer processing, and function calls. There is no
performance difference among the various 16- and 32-bit models chosen for
I/O-bound activities where the processing is all within the kernel.
Although the performance of a 16-bit application on XENIX 386 falls short
of the 32-bit performance, it is still between two and three times greater
than the performance when that program is run on an 8-Mhz 286. The
difference in performance between the 386 host and the 286 target must be
factored in when measuring 16-bit performance on XENIX 386.


When designing an application for the XENIX 386 environment, the developer
must weigh a number of conflicting criteria. The foremost problem is
whether to build the program in 16- or 32-bit mode. Further questions must
address the intended market as well as the performance and portability
required of the completed product. Lastly, it is important to consider
future compatibility requirements.

Microsoft and AT&T are currently working together to merge XENIX 386 and
AT&T's UNIX System V/386 Release 3.2 into a single UNIX system that will be
marketed jointly by the two companies. This Merged Product (MP) will
support all the existing 286 and 386 executable formats common to UNIX and
XENIX on the 386, thereby allowing all existing applications to run.

The emphasis for developers using the Merged Product will be to establish
the UNIX/386, 32-bit mode program interface as the standard for new
applications. This standard will be a superset of the current XENIX System
V/386 program interface, without the support for XENIX-specific system call
extensions. This means that in the long run there will be one binary
standard, developed and supported by Microsoft and AT&T, which will run on
all 386 machines running UNIX, thereby stabilizing the market.

Developers who would like their programs to be source compatible with the
new binary standard may want to avoid the use of XENIX system call
extensions before the Merged Product becomes available in mid-1988. This
applies particularly to the use of 32-bit applications (see Figure 9).
Although kernel support is provided for XENIX extensions in the MP, minimal
development tools will be provided. Debugging support will be limited to
the UNIX System V/386 Release 3.2 binary standard. Without exception, the
functionality of the XENIX call extensions is supported within the
framework of the UNIX program interface.

Figure 1:  Page table entries are maintained in groups of 16. This allows a
           286 segment to expand to 64Kb without existing table entries.

    ░░░░░░░░░░░░░░░░Mapping 286 Programs under XENIX 386░░░░░░░░░░░░░░░░░░

                                          Data Page Table
          Selector                  ║  Available for Expansion ║
            LDT                     ╟──────────────────────────╢
     ╔════════════════╗             ╟──────────────────────────╢
 5FH ║     DS #2      ║             ║           32Kb           ║- 8 pages
     ╟────────────────╢             ╟──────────────────────────╢      mapped
 57H ║     DS #1      ║────────────►║           64Kb           ║-16 pages
     ╟────────────────╢             ╚══════════════════════════╝      mapped
 4FH ║     TS #3      ║                   Text Page Table
     ╟────────────────╢             ╔══════════════════════════╗
 47H ║     TS #2      ║─────┐       ║          Unused          ║
     ╟────────────────╢     │       ╟──────────────────────────╢
 3FH ║     TS #1      ║──┐  │       ║          Unused          ║
     ╚════════════════╝  │  │       ╟──────────────────────────╢
                         │  │       ║            8Kb           ║- 2 pages
                         │  │       ╟──────────────────────────╢      mapped
                         │  └──────►║            64Kb          ║-16 pages
                         │          ╟──────────────────────────╢      mapped
                         └─────────►║            64Kb          ║-16 pages
                                    ╚══════════════════════════╝      mapped

Figure 2:  A Small-Model 286 Program

      │        Text Segment               Data Segment         │█
      │  ╔═════════════════════╗64Kb╔═════════════════════╗64Kb│█
      │  ║                     ║    ║                     ║    │█
      │  ║      Unused         ║    ║    Available heap   ║    │█
      │  ║                     ║    ║                     ║    │█
      │  ╟─────────────────────╢    ╟─────────────────────╢    │█
      │  ║                     ║    ║                     ║    │█
      │  ║                     ║    ║      heap (BSS)     ║    │█
      │  ║                     ║    ║                     ║    │█
      │  ║       Text          ║    ╟─────────────────────╢    │█
      │  ║    (fixed size)     ║    ║                     ║    │█
      │  ║                     ║    ║     Fixed stack     ║    │█
      │  ║                     ║    ║                     ║    │█
      │  ║                     ║    ╟─────────────────────╢    │█
      │  ║                     ║    ║                     ║    │█
      │  ║                     ║    ║  Initialized data   ║    │█
      │  ║                     ║    ║                     ║    │█
      │  ║                     ║    ║                     ║    │█
      │  ║                     ║    ║                     ║    │█
      │  ╚═════════════════════╝0   ╚═════════════════════╝0   │█
      │       Selector 3FH                Selector 47H         │█

Figure 3:  Large-Model 286 Program Layout

          ╔════════════════╗            ╔════════════════╗
          ║    Unused      ║            ║                ║
          ╟────────────────╢            ║   1st segment  ║
          ║     TS #3      ║            ║  available for ║
          ║      8Kb       ║            ║      heap      ║
       4FH╚════════════════╝         67H╚════════════════╝

          ╔════════════════╗            ╔════════════════╗
          ║                ║            ║                ║
          ║     TS #2      ║            ║     DS #2      ║
          ║      64Kb      ║            ║    far data    ║
          ║                ║            ║                ║
       47H╚════════════════╝         5FH╚════════════════╝

          ╔════════════════╗            ╔════════════════╗
          ║                ║            ║                ║
          ║     TS #1      ║            ║      DS #1     ║
          ║      64Kb      ║            ║  stack & data  ║
          ║                ║            ║                ║
       3FH╚════════════════╝         57H╚════════════════╝

Figure 4:  Camparison of 286 Program Models

Name of
Model      Max. Text  Max. Data  Stack          Heap             Performance

Small      <=64Kb     <=64Kb     Fixed (<64Kb)  In Data (<64Kb)  Best

Middle     >64Kb      <=64Kb     Fixed          In Data (<64Kb)  Good

Compact    <=64Kb     >64Kb      <=64Kb         >64Kb            Poor

Large      >64Kb      >64Kb      <=64Kb         >64Kb            Poorest

Data       <=64Kb     >64Kb      Fixed (<64Kb)  <64Kb            Good

Figure 5:  Program Layout in 386 Mode

                    │    Text Segment            Data Segment             │█
          The stack │ ╔═════════════════╗4Gb ╔═════════════════╗4Gb       │█
         grows down │ ║                 ║    ║     Video RAM   ║virtual   │█
      to 0 virtual, │ ║                 ║    ╟─────────────────╢          │█
     while the heap │ ║                 ║    ║      Unused     ║          │█
      grows up. The │ ║                 ║    ╟─────────────────╢          │█
         sum of the │ ║                 ║    ║   Shared Data   ║          │█
       mapped text, │ ║                 ║    ╟─────────────────║6400000H  │█
          data, and │ ║                 ║    ║    Available    ║          │█
       stack cannot │ ║     Unused      ║    ║    for heap     ║          │█
         exceed the │ ║                 ║    ║    expansion    ║          │█
       installation │ ║                 ║    ╟─────────────────║          │█
    dependent limit │ ║                 ║    ║      Heap       ║          │█
     (typically the │ ║                 ║    ╟─────────────────╢          │█
   sum of installed │ ║                 ║    ║ Initialized Data║          │█
       RAM plus the │ ║                 ║    ╟─────────────────╢          │█
            size of │ ╟─────────────────╢    ║     Stack       ║          │█
         the paging │ ║                 ║    ╟─────────────────╢1880000H  │█
            area on │ ║      Text       ║    ║▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒║          │█
         the disk). │ ║                 ║    ║▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒║          │█
                    │ ╚═════════════════╝0   ╚═════════════════╝0         │█

Figure 7:  IObound.c

 * IObound.c

#define IMAX 100
#define JMAX 100

#define BFS 2
char buffer[BFS];

 int i, j;
 int fd;

 /* Create a Disk File */
 fd = creat("scratch", 0600);

 for(i=0; i<IMAX; i++){
    for(j=0; j<JMAX; j++){
       write(fd, buffer, BFS);

 /* Return to Beginning of the File */
 lseek(fd, 0, 0);

 for(i=0; i<IMAX; i++){
    for(j=0; j<JMAX; j++){
       read(fd, buffer, BFS);

 /* Remove the File */

Figure 6:  Cpubound.c

 *      Cpubound.c
#define IMAX 100
#define JMAX 1000

int id[IMAX];
int jd[JMAX];

 int i, j;

 for(i=0; i<IMAX; i++){
    id[i] = i;
    for(j=0; j<JMAX; j++){
       jd[i] = j; calli(id, jd, j);
 calli(i, j, c)
 int *i, *j;
 int c;

  int t;
  int ti = i[c];
  int tj = j[c];

  while(ti- -)
     t += (*(i++))+(*(j++))+(tj-);


Figure 8:  Performance Table

CPU Bound Performance (normalized)

                    Real time        User time     System time

Small-Model  286    32.4 (0.59)      32.2          0.0
Middle-Model 286    40.6 (0.47)      40.5          0.0
Large-Model  286    57.5 (0.33)      57.4          0.0
Small-Model  386    19.0 (1.00)      18.9          0.0

I/O Bound Performace (normalized)

                    Real time        User time     System time

Small-Model  286    38.5 (1.00)      0.3           13.3
Middle-Model 286    38.6 (1.00)      0.7           12.8
Large-Model  286    42.4 (0.91)      0.4           14.8
Small-Model  386    38.4 (1.00)      0.2           12.4

Figure 9:  XENIX System Call Extensions to be avoided for portability.

Entry Point    Function

chsize         adjust file size

creatsem       semaphore operations
nbwaitsem      semaphore operations
Entry Point    Function
nbwaitsem      semaphore operations
opensem        semaphore operations
sigsem         semaphore operations
waitsem        semaphore operations

execseg        execute data
unexecseg      execute data

ftime          obsolete UNIX time function

locking        XENIX file locking

nap            sleep for a short time

proctl         process specific control function

rdchk          check for input without reading

sdenter        XENIX shared data extension
sdfree         XENIX shared data extension
Entry Point    Function
sdfree         XENIX shared data extension
sdget          XENIX shared data extension
sdgetv         XENIX shared data extension
sdleave        XENIX shared data extension
sdwaitv        XENIX shared data extension

shutdn         shutdown system

swapon         control paging devices

Demand Paging and Virtual Memory

Demand paging is a feature of the XENIX 386 operating system, built on top
of the 386 chip architecture that allows a program to run even though all
of its pages have not been loaded into memory. Instead, only those pages
from the executable image stored on disk that are actually referenced by the
program as it runs are loaded into memory.

Whenever the program references a page that is not in memory, it causes a
"page fault." The XENIX kernel acts in response by loading the requested
page from the disk and restarting the faulting program. The effect of demand
paging is to reduce the memory usage of a given program to those pages that
it actually references during a particular invocation. This set of pages is
called the "working set" and is usually smaller than the full size of the
program, especially if that program is large.

Demand paging occurs without any knowledge on the part of the application.
For example, the second pass of the Microsoft 386 C compiler is
approximately 300Kb; however its working set on a typical program is
closer to 80Kb, depending on which Microsoft C language features are

The effect of demand paging is to improve the throughput of the system on
smaller memory configurations, and because it is not necessary to load
programs into memory before they start execution, the latency of command
execution can be greatly reduced.

Virtual memory allows the real memory associated with a program's heap
(allocated via malloc or sbrk calls) to be allocated on demand rather than
at the time of the malloc or sbrk function call. A program can be assigned a
large address space without incurring additional overhead for pages that
would remain unused.

When a program makes a memory allocation call, the kernel recognizes the
change in the end of the virtual heap. However, it allocates no memory
between the end of the old heap and the new location. It is only when a
program accesses pages in the new heap region and causes a page fault that
the kernel allocates empty pages of real memory. This feature is also
called "zero-fill on demand."


HEXCALC: An Instructive Pop-Up Calculator for Microsoft Windows

Charles Petzold☼

When you design a dialog box for either a Windows program or an OS/2
Presentation Manager program, you have available a whole array of "control
windows" that take the form of push buttons, check boxes, scroll bars, list
boxes, edit fields, and text items. Simply list these controls in a dialog
box template and they magically appear in the dialog box window when the
dialog box is invoked.

A little further into Windows programming, you realize that these control
windows can also be plastered right onto the surface of your window's client
area. All it takes is a call to CreateWindow specifying the window class and
the style of the control. A demonstration of this technique was shown in my
COLORSCR program. (See "A Simple Windows Application for Custom Color
Mixing," MSJ, Vol. 2, No. 2.)

Then you reach a frustrating impasse. What you really want to do is create a
simple dialog box template that describes the size and position of the
various controls, and use that template to put the controls on your window's
client area. At first there doesn't appear to be any way to do this. The
only Windows functions that use the dialog template are CreateDialog and
DialogBox. But you want to have these controls on your main window, not on a
dialog box.

HEXCALC (see Figure 1☼) shows how this is done. HEXCALC is a pop-up window
with 29 child window push-button controls. It does not contain even one
CreateWindow call. Instead, it uses CreateDialog to create the program's
main window. This window and all the child window push-button controls are
defined in a dialog box template in HEXCALC's resource script. This article
will take a close look at HEXCALC, then compares HEXCALCW, a version for
Windows 2.0, with HEXCALCP, which runs under Presentation Manager in the
OS/2 systems.


HEXCALC is a ten-function infix-notation hexadecimal calculator with a
full keyboard and mouse interface. It works with 32-bit unsigned long
integers and does addition, subtraction, multiplication, division,
remainders, bitwise AND, OR, and Exclusive-OR, as well as left and right
bit shifts. The buttons for the operations use C notation, except for the
bit shift buttons, which display one angle bracket rather than two.

To do a calculation, type or click in the first number (up to eight
hexadecimal digits) followed by the operation and then the second number.
You can show the result by clicking the Equals button or by pressing either
the keyboard Equals key or Enter key. To correct your entries, you can use
either the BackSpace key, the left cursor movement key, or the Back button.
Pressing Escape or clicking the result box clears the current entry.

Results are always truncated to 32 bits, just as if you were performing the
operation on two unsigned long integers in a C program. The only special
handling is a check for division by zero before doing division or a
remainder operation. In this case, HEXCALC sets the result to FFFFFFFF.

Inspired by PIFEDIT

I don't know how many Windows programmers have taken a close look at
PIFEDIT, but it does some very interesting things. PIFEDIT's main window
looks a lot like a full-screen dialog box, and in fact it is.

All those static text fields, edit fields, check boxes, and radio buttons on
the surface of PIFEDIT's main window are defined in a dialog box template.
Like HEXCALC, PIFEDIT doesn't make any CreateWindow calls. PIFEDIT's main
window is created instead with a call to CreateDialog. The messages to this
window are processed by a window function in the PIFEDIT program.

The technique demonstrated in PIFEDIT and HEXCALC can be used for any
program that would otherwise call CreateWindow to create child window
controls on the surface of its main window. These controls can include
buttons, scroll bars, static text fields, edit fields, list boxes, and even
controls that you design yourself. The technique works best when these
child window controls are of a fixed size and position relative to the
size of a system font character, because that's how they are defined in the
dialog box template.

This is not the technique that the CALC.EXE program uses, however. CALC
draws its own buttons rather than using child window button controls. This
results in a faster display, but CALC is then forced to do hit testing on
the mouse-click messages. Because push-button controls do their own hit
testing, HEXCALC can ignore all mouse messages.

The HEXCALC Program

The various parts of the Windows 2.0 version of HEXCALC are shown in
Figures 2W-6W.

With an installed version of the Microsoft C Compiler Version 4.0 or 5.0
and the Windows Software Development Kit Version 2.0, you can create
HEXCALCW.EXE by running


Let's begin by taking a look at the HEXCALCW.RC resource script. This
contains the dialog box template that defines HEXCALC's main window and all
the push-button controls.

Resource Script

The dialog box template shown in the HEXCALCW.RC resource script describes
the size and appearance of HEXCALC's window. The style bits are
WS_MINIMIZEBOX, these are normal for a modeless dialog box. The text
appearing in the caption bar is "Hex Calculator".

As is the case with all dialog box templates, the values in the DIALOG
statement that specify the window's initial position and size are in units
of 1/4 the width and 1/8 the height of a system font character. It is 102
units (or 25 1/2 characters) wide and 122 units (or 15 1/4 characters)

The 29 buttons displayed in the calculator are defined with PUSHBUTTON
statements. The string in double quotes is the text that appears inside the
button. The number that follows is the control ID. This is followed by the
horizontal and vertical position of the control relative to the upper-left-
hand corner of the dialog box's client area. The final two numbers indicate
the horizontal and vertical size of the control. All the push buttons are
a standard height: 14 units, or 1 3/4 characters.

Control IDs

The control ID is an integer that the child window uses to identify itself
to its parent. The control ID also allows the parent window to send messages
to the child window control (using SendDlgItemMessage) without knowing the
window handle of the control. A parent can obtain the window handle of a
control from its ID using GetDlgItem.

Normally Windows programmers use identifiers defined in a header file for
these control IDs. The header file is then included in both the program and
the resource script using a #include statement.

The control IDs I've chosen for these push buttons may appear to be random,
but there really is a method to the madness. The control IDs have been set
to the ASCII codes of the corresponding number, letter, or symbol that
appears inside the push button.

As will be seen in the HEXCALC.C listing, this is both a cheap and easy
method to add a keyboard interface to the calculator. Clicking on a child
window button with the mouse, the child window sends to its parent window
(which is HEXCALC's main window) a WM_COMMAND message with wParam equal to
the control ID. When the user presses a character key on the keyboard,
HEXCALC's main window receives a WM_CHAR message with wParam equal to the
ASCII code of the character. This means that the same logic can be used for
both key presses and mouse clicks.

The CLASS Statement

The big difference between HEXCALC's dialog box template and most other
dialog box templates is the presence of the CLASS statement. You can read in
the Windows Programmer's Utility Guide, that (I quote it in its entirety)
"This optional statement defines the class of the dialog box" and the
parameter to CLASS "is an integer or a string (enclosed in double quotes)
that identifies the dialog box class." At this point, perhaps some
additional explanation is required.

All windows created in Microsoft(R) Windows are based on a particular window
class. Among other things, this class specifies a function (called the
window function) that processes messages to the window. Normally in a
Windows program you first register a window class and then create a window
based on that class by calling CreateWindow.

When you create a dialog box by calling CreateDialog or DialogBox, Windows
normally uses a default window class that Windows itself defines. The
function specified in this window class is internal to Windows. It is this
window function that processes the messages to the dialog box. This
function will pass many of the messages to a "dialog function" located in
your program. The address of this dialog function is specified as a
parameter to the CreateDialog or DialogBox function. That's the normal case.
However, what we're doing in HEXCALC is somewhat different.

By specifying a window class in the dialog box template, you're telling
Windows not to use its own window class for dialog boxes created using this
template. You're telling Windows to use a different window class,
specifically the one indicated in the CLASS statement. This provides
Windows with the address of a window function. It is this function that
will receive messages for the dialog box. The window function, called
WndProc, is part of HEXCALC.

For an ordinary dialog box you wouldn't want to do this, because then you
would be faced with the problem of duplicating all the logic required for
changing the input focus among child window controls when the user presses
the Tab key or cursor movement keys. But for a program like HEXCALC, this
technique is ideal.

Creating the Window

Like most Windows programs, HEXCALC begins by registering a window class
(HexCalcW) that among other things specifies the window function (WndProc)
that will process messages to windows based on that class. But then, rather
than call CreateWindow, HEXCALC calls CreateDialog, a function that usually
creates a modeless dialog box.

The second parameter to the CreateDialog function is the name of the dialog
box template, which is also named HexCalcW. The third parameter to
CreateDialog is normally the window handle of the dialog box's parent.
Because you're using this dialog box as the main window in HEXCALC, this is
set to 0.

The fourth parameter to CreateDialog is normally a far pointer to the
program's dialog function for the dialog box. This is the function to which
some (but not all) messages processed by the actual dialog box window
function are passed. Usually that window function is internal to Windows,
but not when a class for the dialog box is specified in the template.
Because the WndProc window function itself is part of HEXCALC, this dialog
function is not needed. The fourth parameter is set to NULL.

Now it's time to ask: What does Windows do during the CreateDialog call?
Consider that Windows has access to the instance handle of the program and
the name of the dialog box template. It gets this from the CreateDialog

  hWnd = CreateDialog (hInstance, szAppName, 0, NULL) ;

The szAppName variable is a character array that contains the name HexCalcW,
the name of the dialog box template. This means that Windows can load the
dialog box template into memory. The template (in its ASCII form) begins
like this:

  HexCalcW DIALOG 32768, 0, 102, 122
    CLASS "HexCalcW"
    CAPTION "Hex Calculator"

Using the parameters to the CreateDialog function along with the top of the
dialog box template, Windows is able to call CreateWindow with the
following parameters:

  hWnd = CreateWindow ("HexCalcW", "Hex Calculator",
    32768,0, 102 * 4 / xChar, 122 * 8 / yChar, NULL, NULL, hInstance, NULL) ;

This creates HEXCALC's top-level window. Assigning a value of 32768 to the
initial x position specifies that Windows use the default position (when
using a CreateWindow call in a program, CW_USEDEFAULT is the identifier).
The xChar and yChar values shown here would be determined by Windows. They
are the width and height of a system font character and are used to
translate the dialog box coordinate and size units into pixels. The hWnd
parameter returned from this call is eventually passed back to the program
as the return value from CreateDialog.

Still processing the CreateDialog call, Windows will then create child
windows based on the PUSHBUTTON statements in the dialog box template. This
is the first PUSHBUTTON statement:

  PUSHBUTTON "D", 68, 8, 24, 14, 14

This becomes another CreateWindow call:

  hCtl = CreateWindow ("button", "D", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON |
    8 * 4 / xChar, 24 * 8 / yChar, 14 * 4 / xChar,
    14 * 8 / yChar, hWnd, 68, hInstance, NULL) ;

Windows will call CreateWindow 29 times for each of the push buttons. You
should note that the eighth parameter (the parent window) is set to hWnd.
This will permit the child window controls to send WM_COMMAND messages to
their parent window. The ninth parameter will be set to the control ID.

Because the dialog box template specifies a window class, Windows does
almost nothing else during the CreateDialog function except to call
CreateWindow multiple times based on the template. When CreateDialog
returns, all the windows have been created. The program proceeds normally
just as if it had done all that work itself. HEXCALC has no other odd code.
It has a normal message loop and a normal window function.

So, in effect, HEXCALC is not very different from my earlier COLORSCR
program. Both programs create a main window and several child window
controls. The difference is that COLORSCR does it with explicit CreateWindow
calls. HEXCALC simply gives Windows a dialog box template and says, "Here,
you do this stuff. Tell me when you're finished."

Window Function

I'm not going to explain the actual calculator logic in HEXCALC because
that's really not the point of this exercise. However, let's take a look at
some of the code near the top of WndProc.

Because the control IDs of the push buttons are set to the ASCII code of the
push-button text, the keyboard interface is very simple. On a WM_CHAR
message, the wParam parameter (the ASCII code of the key) is first converted
to upper case and the Enter key is translated to an equals sign.

HEXCALC determines if the key corresponds to a button by calling GetDlgItem.
This function translates a control ID of a child window into the child
window handle:

  hButton = GetDlgItem (hWnd, wParam)

If hButton is 0, then the key does not correspond to a button, and HEXCALC
beeps. Otherwise, HEXCALC flashes the button by sending it a couple of
BM_SETSTATE messages:

  SendMessage (hButton, BM_SETSTATE, 1, 0L) ;
  SendMessage (hButton, BM_SETSTATE, 0, 0L) ;

This adds a nice touch to the keyboard interface of HEXCALC and-as you can
see-it's almost trivial.

When WndProc processes WM_COMMAND messages, it always sets the input focus
to the parent window:

  case WM_COMMAND: SetFocus (hWnd) ;

Otherwise, the input focus would be shifted to one of the buttons whenever
it was clicked with the mouse. This would alter the appearance of the button
and also prevent WndProc from seeing subsequent WM_CHAR messages.

The Presentation Manager Version

The conversion of HEXCALC to the OS/2 Presentation Manager is relatively
straightforward. The result──called HEXCALCP.EXE──is shown in Figures

From a distance of about six feet, a Presentation Manager program looks the
same as a Windows program. Structurally, the application program interfaces
in the two environments are quite similar. However, on closer
examination, lots of little changes are required. Some of these changes
are described by Michael Geary in "Converting Windows Applications for
Microsoft(R) OS/2 Presentation Manager," in this issue.

The format of the dialog box template in HEXCALCP.RC is slightly different
because it reflects the structure of "standard windows" in the
Presentation Manager. The main window is a "frame" window, the client
window is a child of the frame, and the push buttons are children of the
client window. This parent-child relationship is described by the window
definitions within nested brackets in the template. Messages to the client
window in HEXCALCP are handled by the ClientWndProc window function.

You will remember that HEXCALC must make a call to SetFocus whenever a
button is clicked to prevent the button from keeping the keyboard input
focus. Using the Presentation Manager, buttons can have a BS_NOMOUSEFOCUS
style, which means that they will not receive the input focus when clicked
with the mouse. The SetFocus call is made unnecessary by specifying the
BS_NOMOUSEFOCUS style for the push buttons in the HEXCALCP.RC resource

The keyboard interface in HEXCALCP is somewhat cleaner in the Presentation
Manager version. First, all keyboard messages are WM_CHAR messages so
HEXCALCP doesn't have to process WM_KEYDOWN messages. Secondly, if the key
pressed corresponds to one of the buttons, then HEXCALCP sends the button a
single BM_CLICK message:

  WinSendMsg (hWndButton, BM_CLICK, 0L, 0L) ;

This message not only causes the button to flash, but also causes the button
to send its owner a WM_COMMAND message. In the Windows version, the
keyboard message falls through to be processed within the WM_COMMAND case.

Termination works a little differently in the two programs. In Windows, the
Close option on the system menu generates a WM_CLOSE message. If the message
is passed on to DefWindowProc, then Windows destroys the window by calling
DestroyWindow and the window function receives a WM_DESTROY message. The
window function responds by posting a WM_QUIT message to itself with
PostQuitMessage. This message causes the GetMessage function in WinMain to
return 0, and the program terminates.

In the Presentation Manager, the WM_CLOSE message is still generated by the
system menu. If the program passes this message to WinDefWindowProc, then
the Presentation Manager posts a WM_QUIT message to the message queue. As in
Windows, this message causes the WinGetMsg call in main to return 0.
However, the window still exists, so the program must explicitly destroy the
window in main using a call to WinDestroyWindow. This function causes the
Presentation Manager to destroy the frame window as well as all the
children and other descendants of the frame.

Figure 2W:  HEXCALCW Make File

hexcalcw.obj: hexcalcw.c
     cl -c -D LINT_ARGS -Gsw -Os -W2 -Zp hexcalcw.c

hexcalcw.res : hexcalcw.rc hexcalcw.ico
     rc -r hexcalcw.rc

hexcalcw.exe: hexcalcw.obj hexcalcw.def hexcalcw.res
     link4 hexcalcw, /align:16, /map, slibw, hexcalcw
     rc hexcalcw.res

Figure 2PM:  HEXCALCP Make File

hexcalcp.obj: hexcalcp.c hexcalcp.h
     cl -c -D LINT_ARGS -G2sw -W2 -Zp hexcalcp.c

hexcalcp.res : hexcalcp.rc hexcalcp.h
     rc -r hexcalcp.rc

hexcalcp.exe: hexcalcp.obj hexcalcp.def hexcalcp.res
     link hexcalcp, /align:16, /map, slibc5 os2, hexcalcp
     rc hexcalcp.res

Figure 3W:  HEXCALCW.RC Resource Script

#include <windows.h>

HexCalcW ICON hexcalcw.ico

HexCalcW DIALOG 32768, 0, 102, 122
     CLASS "HexCalcW"
     CAPTION "Hex Calculator"
          PUSHBUTTON "D",       68,  8,  24, 14, 14
          PUSHBUTTON "A",       65,  8,  40, 14, 14
          PUSHBUTTON "7",       55,  8,  56, 14, 14
          PUSHBUTTON "4",       52,  8,  72, 14, 14
          PUSHBUTTON "1",       49,  8,  88, 14, 14
          PUSHBUTTON "0",       48,  8, 104, 14, 14
          PUSHBUTTON "0",       27, 26,   4, 50, 14
          PUSHBUTTON "E",       69, 26,  24, 14, 14
          PUSHBUTTON "B",       66, 26,  40, 14, 14
          PUSHBUTTON "8",       56, 26,  56, 14, 14
          PUSHBUTTON "5",       53, 26,  72, 14, 14
          PUSHBUTTON "2",       50, 26,  88, 14, 14
          PUSHBUTTON "Back",     8, 26, 104, 32, 14
          PUSHBUTTON "C",       67, 44,  40, 14, 14
          PUSHBUTTON "F",       70, 44,  24, 14, 14
          PUSHBUTTON "9",       57, 44,  56, 14, 14
          PUSHBUTTON "6",       54, 44,  72, 14, 14
          PUSHBUTTON "3",       51, 44,  88, 14, 14
          PUSHBUTTON "+",       43, 62,  24, 14, 14
          PUSHBUTTON "-",       45, 62,  40, 14, 14
          PUSHBUTTON "*",       42, 62,  56, 14, 14
          PUSHBUTTON "/",       47, 62,  72, 14, 14
          PUSHBUTTON "%",       37, 62,  88, 14, 14
          PUSHBUTTON "Equals",  61, 62, 104, 32, 14
          PUSHBUTTON "&&",      38, 80,  24, 14, 14
          PUSHBUTTON "|",      124, 80,  40, 14, 14
          PUSHBUTTON "^",       94, 80,  56, 14, 14
          PUSHBUTTON "<",       60, 80,  72, 14, 14
          PUSHBUTTON ">",       62, 80,  88, 14, 14

Figure 3PM:  HEXCALCP.RC Resource Script

#include <os2.h>
#include "hexcalcp.h"

    FRAME "Hex Calculator", 1, 110, 40, 102, 122, WS_VISIBLE |
        WINDOW "", FID_CLIENT, 0, 0, 102, 122, "HexCalcP", WS_VISIBLE
            PUSHBUTTON "D",       68,  8,  88, 14, 14, BS_NOMOUSEFOCUS
            PUSHBUTTON "A",       65,  8,  72, 14, 14, BS_NOMOUSEFOCUS
            PUSHBUTTON "7",       55,  8,  56, 14, 14, BS_NOMOUSEFOCUS
            PUSHBUTTON "4",       52,  8,  40, 14, 14, BS_NOMOUSEFOCUS
            PUSHBUTTON "1",       49,  8,  24, 14, 14, BS_NOMOUSEFOCUS
            PUSHBUTTON "0",       48,  8,   4, 14, 14, BS_NOMOUSEFOCUS
            PUSHBUTTON "0",       27, 26, 104, 50, 14, BS_NOMOUSEFOCUS
            PUSHBUTTON "E",       69, 26,  88, 14, 14, BS_NOMOUSEFOCUS
            PUSHBUTTON "B",       66, 26,  72, 14, 14, BS_NOMOUSEFOCUS
            PUSHBUTTON "8",       56, 26,  56, 14, 14, BS_NOMOUSEFOCUS
            PUSHBUTTON "5",       53, 26,  40, 14, 14, BS_NOMOUSEFOCUS
            PUSHBUTTON "2",       50, 26,  24, 14, 14, BS_NOMOUSEFOCUS
            PUSHBUTTON "Back",     8, 26,   4, 32, 14, BS_NOMOUSEFOCUS
            PUSHBUTTON "F",       70, 44,  88, 14, 14, BS_NOMOUSEFOCUS
            PUSHBUTTON "C",       67, 44,  72, 14, 14, BS_NOMOUSEFOCUS
            PUSHBUTTON "9",       57, 44,  56, 14, 14, BS_NOMOUSEFOCUS
            PUSHBUTTON "6",       54, 44,  40, 14, 14, BS_NOMOUSEFOCUS
            PUSHBUTTON "3",       51, 44,  24, 14, 14, BS_NOMOUSEFOCUS
            PUSHBUTTON "+",       43, 62,  88, 14, 14, BS_NOMOUSEFOCUS
            PUSHBUTTON "-",       45, 62,  72, 14, 14, BS_NOMOUSEFOCUS
            PUSHBUTTON "*",       42, 62,  56, 14, 14, BS_NOMOUSEFOCUS
            PUSHBUTTON "/",       47, 62,  40, 14, 14, BS_NOMOUSEFOCUS
            PUSHBUTTON "%",       37, 62,  24, 14, 14, BS_NOMOUSEFOCUS
            PUSHBUTTON "Equals",  61, 62,   4, 32, 14, BS_NOMOUSEFOCUS
            PUSHBUTTON "&",       38, 80,  88, 14, 14, BS_NOMOUSEFOCUS
            PUSHBUTTON "|",      124, 80,  72, 14, 14, BS_NOMOUSEFOCUS
            PUSHBUTTON "^",       94, 80,  56, 14, 14, BS_NOMOUSEFOCUS
            PUSHBUTTON "<",       60, 80,  40, 14, 14, BS_NOMOUSEFOCUS
            PUSHBUTTON ">",       62, 80,  24, 14, 14, BS_NOMOUSEFOCUS

Figure 4W:  HEXCALCW.DEF Module Definition File

NAME           HexCalcW
DESCRIPTION    'Hexadecimal Calculator(C) Charles Petzold 1987'
STUB           'WINSTUB.EXE'
HEAPSIZE       1024
STACKSIZE      4096
EXPORTS        WndProc

Figure 4PM:  HEXCALCP.DEF Module Definition File

DESCRIPTION    'Hexadecimal Calculator(C) Charles Petzold
STUB           'OS2STUB.EXE'
HEAPSIZE       1024
STACKSIZE      4096
EXPORTS        ClientWndProc

Figure 5W:

There is no special header file equivalent
for the Windows version.

Figure 5PM:  HEXCALCP.H Header File

#define ID_HEXCALC 1

Figure 6W:  HEXCALCW.C is the Source Code Listing

#include <windows.h>
#include <limits.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

long FAR PASCAL WndProc (HWND, unsigned, WORD, LONG) ;

int PASCAL WinMain (hInstance, hPrevInstance, lpszCmdLine, nCmdShow)
     HANDLE      hInstance, hPrevInstance;
     LPSTR       lpszCmdLine;
     int         nCmdShow;
     static char szAppName [] = "HexCalcW" ;
     HWND        hWnd ;
     MSG         msg;
     WNDCLASS    wndclass ;

     if (!hPrevInstance)
          wndclass.style          = CS_HREDRAW | CS_VREDRAW;
          wndclass.lpfnWndProc    = WndProc ;
          wndclass.cbClsExtra     = 0 ;
          wndclass.cbWndExtra     = 0 ;
          wndclass.hInstance      = hInstance ;
          wndclass.hIcon          = LoadIcon (hInstance, szAppName) ;
          wndclass.hCursor        = LoadCursor (NULL, IDC_ARROW) ;
          wndclass.hbrBackground  = COLOR_WINDOW + 1 ;
          wndclass.lpszMenuName   = NULL ;
          wndclass.lpszClassName  = szAppName ;

          if (!RegisterClass (&wndclass))
               return FALSE ;

     hWnd = CreateDialog (hInstance, szAppName, 0, NULL) ;

     ShowWindow (hWnd, nCmdShow) ;

     while (GetMessage (&msg, NULL, 0, 0))
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
     return msg.wParam ;

void ShowNumber (hWnd, dwNumber)
     HWND  hWnd ;
     DWORD dwNumber ;
     char  szBuffer [20] ;

     SetDlgItemText (hWnd, VK_ESCAPE,
                         strupr (ltoa (dwNumber, szBuffer, 16))) ;

DWORD CalcIt (dwFirstNum, nOperation, dwNum)
     DWORD dwFirstNum, dwNum ;
     short nOperation ;
     switch (nOperation)
          case '=' : return dwNum ;
          case '+' : return dwFirstNum +  dwNum ;
          case '-' : return dwFirstNum -  dwNum ;
          case '*' : return dwFirstNum *  dwNum ;
          case '&' : return dwFirstNum &  dwNum ;
          case '|' : return dwFirstNum |  dwNum ;
          case '^' : return dwFirstNum ^  dwNum ;
          case '<' : return dwFirstNum << dwNum ;
          case '>' : return dwFirstNum >> dwNum ;
          case '/' : return dwNum ? dwFirstNum / dwNum : ULONG_MAX ;
          case '%' : return dwNum ? dwFirstNum % dwNum : ULONG_MAX ;
          default  : return 0L ;

long FAR PASCAL WndProc (hWnd, iMessage, wParam, lParam)
     HWND         hWnd;
     unsigned     iMessage;
     WORD         wParam;
     LONG         lParam;
     static BOOL  bNewNumber = TRUE ;
     static DWORD dwNumber, dwFirstNum ;
     static short nOperation = '=' ;
     HWND         hButton ;

     switch (iMessage)
          case WM_KEYDOWN:             /* left arrow -> backspace */
               if (wParam != VK_LEFT)
                    break ;
               wParam = VK_BACK ;
                                             /* fall through */
          case WM_CHAR:
               if ((wParam = toupper (wParam)) == VK_RETURN)
                    wParam = '=' ;

               if (hButton = GetDlgItem (hWnd, wParam))
                    SendMessage (hButton, BM_SETSTATE, 1, 0L) ;
                    SendMessage (hButton, BM_SETSTATE, 0, 0L) ;
                    MessageBeep (0) ;
                    break ;
                                             /* fall through */
          case WM_COMMAND:
               SetFocus (hWnd) ;

               if (wParam == VK_BACK)                  /* backspace */
                    ShowNumber (hWnd, dwNumber /= 16) ;

               else if (wParam == VK_ESCAPE)           /* escape */
                    ShowNumber (hWnd, dwNumber = 0L) ;

               else if (isxdigit (wParam))             /* hex digit */
                    if (bNewNumber)
                         dwFirstNum = dwNumber ;
                         dwNumber = 0L ;
                    bNewNumber = FALSE ;

                    if (dwNumber <= ULONG_MAX >> 4)
                         ShowNumber (hWnd, dwNumber =
                              16 * dwNumber + wParam -
                              (isdigit (wParam) ? '0' : 'A' - 10)) ;
                         MessageBeep (0) ;

               else                                    /* operation */
                    if (!bNewNumber)
                         ShowNumber (hWnd, dwNumber =
                              CalcIt (dwFirstNum, nOperation,
                                      dwNumber)) ;
                    bNewNumber = TRUE ;
                    nOperation = wParam ;
               break ;

          case WM_DESTROY:
               PostQuitMessage (0) ;
               break ;

          default :
               return DefWindowProc (hWnd, iMessage, wParam, lParam) ;
     return 0L ;
#include <windows.h>
#include <limits.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

long FAR PASCAL WndProc (HWND, unsigned, WORD, LONG) ;

int PASCAL WinMain (hInstance, hPrevInstance, lpszCmdLine, nCmdShow)
     HANDLE      hInstance, hPrevInstance;
     LPSTR       lpszCmdLine;
     int         nCmdShow;
     static char szAppName [] = "HexCalcW" ;
     HWND        hWnd ;
     MSG         msg;
     WNDCLASS    wndclass ;

     if (!hPrevInstance)
          wndclass.style          = CS_HREDRAW | CS_VREDRAW;
          wndclass.lpfnWndProc    = WndProc ;
          wndclass.cbClsExtra     = 0 ;
          wndclass.cbWndExtra     = 0 ;
          wndclass.hInstance      = hInstance ;
          wndclass.hIcon          = LoadIcon (hInstance, szAppName) ;
          wndclass.hCursor        = LoadCursor (NULL, IDC_ARROW) ;
          wndclass.hbrBackground  = COLOR_WINDOW + 1 ;
          wndclass.lpszMenuName   = NULL ;
          wndclass.lpszClassName  = szAppName ;

          if (!RegisterClass (&wndclass))
               return FALSE ;

     hWnd = CreateDialog (hInstance, szAppName, 0, NULL) ;

     ShowWindow (hWnd, nCmdShow) ;

     while (GetMessage (&msg, NULL, 0, 0))
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
     return msg.wParam ;

void ShowNumber (hWnd, dwNumber)
     HWND  hWnd ;
     DWORD dwNumber ;
     char  szBuffer [20] ;

     SetDlgItemText (hWnd, VK_ESCAPE,
                         strupr (ltoa (dwNumber, szBuffer, 16))) ;

DWORD CalcIt (dwFirstNum, nOperation, dwNum)
     DWORD dwFirstNum, dwNum ;
     short nOperation ;
     switch (nOperation)
          case '=' : return dwNum ;
          case '+' : return dwFirstNum +  dwNum ;
          case '-' : return dwFirstNum -  dwNum ;
          case '*' : return dwFirstNum *  dwNum ;
          case '&' : return dwFirstNum &  dwNum ;
          case '|' : return dwFirstNum |  dwNum ;
          case '^' : return dwFirstNum ^  dwNum ;
          case '<' : return dwFirstNum << dwNum ;
          case '>' : return dwFirstNum >> dwNum ;
          case '/' : return dwNum ? dwFirstNum / dwNum : ULONG_MAX ;
          case '%' : return dwNum ? dwFirstNum % dwNum : ULONG_MAX ;
          default  : return 0L ;

long FAR PASCAL WndProc (hWnd, iMessage, wParam, lParam)
     HWND         hWnd;
     unsigned     iMessage;
     WORD         wParam;
     LONG         lParam;
     static BOOL  bNewNumber = TRUE ;
     static DWORD dwNumber, dwFirstNum ;
     static short nOperation = '=' ;
     HWND         hButton ;

     switch (iMessage)
          case WM_KEYDOWN:             /* left arrow -> backspace */
               if (wParam != VK_LEFT)
                    break ;
               wParam = VK_BACK ;
                                             /* fall through */
          case WM_CHAR:
               if ((wParam = toupper (wParam)) == VK_RETURN)
                    wParam = '=' ;

               if (hButton = GetDlgItem (hWnd, wParam))
                    SendMessage (hButton, BM_SETSTATE, 1, 0L) ;
                    SendMessage (hButton, BM_SETSTATE, 0, 0L) ;
                    MessageBeep (0) ;
                    break ;
                                             /* fall through */
          case WM_COMMAND:
               SetFocus (hWnd) ;

               if (wParam == VK_BACK)                  /* backspace */
                    ShowNumber (hWnd, dwNumber /= 16) ;

               else if (wParam == VK_ESCAPE)           /* escape */
                    ShowNumber (hWnd, dwNumber = 0L) ;

               else if (isxdigit (wParam))             /* hex digit */
                    if (bNewNumber)
                         dwFirstNum = dwNumber ;
                         dwNumber = 0L ;
                    bNewNumber = FALSE ;

                    if (dwNumber <= ULONG_MAX >> 4)
                         ShowNumber (hWnd, dwNumber =
                              16 * dwNumber + wParam -
                              (isdigit (wParam) ? '0' : 'A' - 10)) ;
                         MessageBeep (0) ;

               else                                    /* operation */
                    if (!bNewNumber)
                         ShowNumber (hWnd, dwNumber =
                              CalcIt (dwFirstNum, nOperation,
                                      dwNumber)) ;
                    bNewNumber = TRUE ;
                    nOperation = wParam ;
               break ;

          case WM_DESTROY:
               PostQuitMessage (0) ;
               break ;

          default :
               return DefWindowProc (hWnd, iMessage, wParam, lParam) ;
     return 0L ;

Figure 6PM:  HEXCALCP.C is the Source Code Listing

#include <os2.h>
#include <limits.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include "hexcalcp.h"


main ()

      static CHAR szClassName [] = "HexCalcP" ;
      HAB         hAB ;
      HMQ         hMQ ;
      HWND        hWnd ;
      QMSG        qmsg ;

      hAB = WinInitialize () ;
      hMQ = WinCreateMsgQueue (hAB, 0) ;

      WinRegisterClass (hAB, szClassName, ClientWndProc, 0L, 0, NULL) ;

             ID_HEXCALC, NULL) ;

      WinSetFocus (HWND_DESKTOP, WinWindowFromID (hWnd, FID_CLIENT)) ;
      while (WinGetMsg (hAB, &qmsg, NULL, 0, 0)) WinDispatchMsg (hAB,&qmsg);

      WinDestroyWindow (hWnd) ;
      WinDestroyMsgQueue (hMQ) ;
      WinTerminate (hAB) ;

      return 0 ;

 void ShowNumber (hWnd, lNumber)
      HWND  hWnd ;
      ULONG lNumber ;
      CHAR  szBuffer [20] ;
      HWND  hWndResult ;

      hWndResult = WinWindowFromID (hWnd, VK_ESCAPE) ;

      WinSetWindowText (hWndResult, strupr (ltoa (lNumber, szBuffer, 16))) ;

 ULONG CalcIt (lFirstNum, iOperation, lNum)
      ULONG lFirstNum, lNum ;
      SHORT iOperation ;
      switch (iOperation)
           case '=' : return lNum ;
           case '+' : return lFirstNum +  lNum ;
           case '-' : return lFirstNum -  lNum ;
           case '*' : return lFirstNum *  lNum ;
           case '&' : return lFirstNum &  lNum ;
           case '|' : return lFirstNum |  lNum ;
           case '^' : return lFirstNum ^  lNum ;
           case '<' : return lFirstNum << lNum ;
           case '>' : return lFirstNum >> lNum ;
           case '/' : return lNum ? lFirstNum / lNum :
           case '%' : return lNum ? lFirstNum % lNum :
           default  : return 0L ;

 ULONG EXPENTRY ClientWndProc (hWnd, nMessage, lParam1, lParam2)
      HWND         hWnd ;
      USHORT       nMessage ;
      ULONG        lParam1 ;
      ULONG        lParam2 ;
      static BOOL  bNewNumber = TRUE ;
      static ULONG lNumber, lFirstNum ;
      static SHORT iOperation = '=' ;
      HWND         hWndButton ;
      SHORT        idButton ;

      switch (nMessage)
           case WM_CHAR:
                if (lParam1 & KC_KEYUP)
                     break ;

                if (HIUSHORT (lParam2) == VK_LEFT)   /* left arrow to */
                     LOUSHORT (lParam2) = VK_BACK ;  /* backspace   */
                if (HIUSHORT (lParam2) == VK_RETURN) /*  return to     */
                     LOUSHORT (lParam2) = '=' ;      /* equals      */
                if (LOUSHORT (lParam2) == 0)
                     break ;

                LOUSHORT (lParam2) = toupper (LOUSHORT (lParam2)) ;
                if (hWndButton = WinWindowFromID (hWnd, LOUSHORT (lParam2)))
                     WinSendMsg (hWndButton, BM_CLICK, 0L, 0L) ;
                     WinAlarm (HWND_DESKTOP, WA_ERROR) ;
                return 1L ;

           case WM_COMMAND:
                idButton = LOUSHORT (lParam1) ;
                if (idButton == VK_BACK)                /* backspace */
                     ShowNumber (hWnd, lNumber /= 16) ;
                else if (idButton == VK_ESCAPE)         /* escape    */
                     ShowNumber (hWnd, lNumber = 0L) ;
                else if (isxdigit (idButton))           /* hex digit */
                     if (bNewNumber)
                          lFirstNum = lNumber ;
                          lNumber = 0L ;
                     bNewNumber = FALSE ;
                     if (lNumber <= ULONG_MAX >> 4)
                          ShowNumber (hWnd, lNumber =
                               16 * lNumber + idButton -
                               (isdigit (idButton) ? '0' : 'A' - 10)) ;
                          WinAlarm (HWND_DESKTOP, WA_ERROR) ;

                else                                    /* operation */
                     if (!bNewNumber)
                          ShowNumber (hWnd, lNumber =
                               CalcIt (lFirstNum, iOperation, lNumber)) ;
                     bNewNumber = TRUE ;
                     iOperation = idButton ;
                break ;
           default :
                return WinDefWindowProc (hWnd, nMessage, lParam1, lParam2) ;
      return 0L ;



Effectively Using Far and Huge Data Pointers In Your Microsoft C Programs

Kaare Christian☼

Program pointers are one of the most important features of the C language,
because they are often the only way to express your thoughts clearly and
precisely. Addresses are critical in all programming languages, but most
languages try to take care of addressing details for you, thereby hiding
crucial information about what happens when your program executes. Only
the C language brings these details to the fore, by allowing you to express
pointer operations in the text of the program.

In the early, heady days of C, pointers were 16-bit quantities. Newcomers to
C had to learn pointer arithmetic as well as C's somewhat difficult notation
for using pointers, but they did not have to worry much about the underlying
machine, the PDP-11. Pointer operations are beautifully supported by the
simple and consistent architecture of the PDP-11, but only within a 64Kb
data space.

The PC family of computers is based on Intel(R) processors, which use a
segmented architecture. If your data fits within one 64Kb space
(Microsoft(R) C small- or medium-model programs), then pointer operations
on the PC will work efficiently and consistently, just as on the
venerable PDP-11. (This discussion applies to the segmentation model of
the 8086, 8088, 186, 188 and the real mode on the 286 and 386. Protected
mode segmentation is different and is not discussed.) However, if you do
need the extra memory that the PC architecture offers, then you must learn
to use far and huge pointers.

Microsoft C lets you mix data types within a single program. You can create
a far pointer in a small-model program or a near pointer in a huge-model
program. My experience has been with occasional use of pointers to far or
huge data in small-model programs, because the software that I work on tends
to be small except for a few large data structures. Many of my comments will
apply to all pointers in a compact-, large-, or huge-model program, because
in those models data pointers are, by default, far or huge. All of the
sample programs in this article were compiled with Microsoft C Version 4.0,
using the small model.

Besides pointers to far and huge data, Microsoft C also has far and huge
data types. In Microsoft C there are two methods for creating a very large
array: you can declare it as a huge or far array of integers or you can
declare a pointer to a huge or far integer and then use halloc, the huge
memory allocation subroutine, to allocate the space. The second approach
is preferable because it is more flexible. Declaring a huge array forces you
to decide in advance the exact array size; using halloc to create the array
allows your program to adjust to the actual amount of memory present on the
computer when it executes. This article specifically examines pointers to
huge and far data, although some of the caveats and much of the discussion
on efficiency also applies to huge or far arrays.

As a prerequisite for grasping the following material, you must first
understand how pointers are used in C and C casts. If you really know the
basics of these two topics, then the program in Figure 1 should be a snap.
Before reading the next paragraph, look at the program and predict its

The answer: 100 and 200. The first printf statement prints the number of
integers in the interval between where ip points and where i is located.
Since ip was assigned the location 100 integers past where i is stored,
naturally the answer is 100. In the second printf statement, ip and the
location of i are cast to unsigned and then subtracted. The cast doesn't
change the bit patterns, but it does change the operation from a pointer
subtraction to an ordinary unsigned subtraction. The second answer is 200
because the two bit patterns differ by that amount. If you ask, why 200,
it's because 100 integers at 2 bytes each makes 200. The statement ip += 100
actually added 200 to ip's binary value. If you passed the quiz, then read
on. If not, then you need to learn more about ordinary pointer operations
before you can understand the extraordinary pointer operations of
Microsoft C far and huge pointers.

A pointer to far data, usually called a far pointer, is a 32-bit pointer
whose arithmetic operations are performed by using just 16 bits. (See the
summary in Figure 2.) A far pointer can point anywhere in the PC's address
space, but the 16-bit arithmetic limit means that the memory region towards
which it points must be less than 64Kb large. For instance, you could use a
far pointer to access a dynamically allocated array of 25,000 integers, but
you could not use that same far pointer to access an array of 25,000 floats
or doubles. This is because the array of integers will take up less than
64Kb, while the array of floats or doubles will take more.

A pointer to huge data, called a huge pointer, is also a 32-bit pointer, but
its arithmetic operations are performed by using all 32 bits. Thus a huge
pointer can point anywhere, at anything. The trade-off, of course, is that
huge pointers operate more slowly and use more code than far pointers.
Adding an integer to a far pointer isn't much more work than adding two
integers, but adding an integer to a huge pointer is roughly twice the work
of adding two integers.

Far and huge pointers are sometimes used in small- and medium-model programs
to access addresses outside the program's 64Kb default data space. They
might be used, for example, to access the video display buffer or to access
dynamically allocated memory outside the 64Kb data space. In compact- or
large-model programs, all data pointers are far pointers, unless you
specifically ask for a huge or near pointer. In a huge-model program, all
data pointers are huge unless you specifically request a near or far

When you are declaring an ordinary pointer, such as the pointer ip in
Figure 1, you place an asterisk in front of the name to signify that *ip is
an integer; hence ip itself must be a pointer to integer. To declare a far
or huge pointer, you must use the word far or huge with the asterisk. So
you would say:

  int far *npfi;
  static int huge *hp1, huge *hp2;

In a declaration or a cast, the word far or huge is conceptually glued to
the asterisk; that's why the word huge appears twice in the second
declaration, once for each asterisk. Note that the data type and storage
class, static integer in this example, appear only once in the second

The pointers created in the declaration above are designed to access data
anywhere in memory, but the memory space for the pointers themselves (4
bytes each) is allocated in the default data segment. The first pointer is
named npfi because it is a near pointer to a far integer; the pointer itself
is near data, but it points at far data. Nevertheless the vernacular term
for npfi is far pointer, meaning a pointer to far data. You could create a
near pointer (16-bit pointer) in the far data space, but not in the default
data segment, by using the following declaration:

  int * far fpni;

The pointer in this declaration is named fpni to remind you that it is a far
pointer to a near integer; the pointer itself (2 bytes) is stored in a far
data segment even though it can only point at integers inside the default
data segment. This declaration cannot appear inside a procedure, not even
inside main, because the declaration specifically tells the compiler to put
the pointer in the far data segment, whereas automatic variables in a
procedure are stored on the stack. However, adding the storage class static
to the declaration would enable it to be placed inside a procedure.

The words near, far, and huge are Microsoft extensions to the C language;
they help Microsoft C wring the best performance out of the difficult Intel
architecture. A declaration that looks like

  typename far * dataname

creates a pointer in the near data segment to access something stored in a
far data segment. Conversely, a declaration that looks like

  typename far dataname

creates data in the far data segment. If the word far (or near or huge) is
followed by an asterisk, the pointer is allocated locally, but is used to
access distant data. If the word far (or near or huge) is followed by the
name of a variable, then the variable itself is stored in the far (or near
or huge) data segment. The Microsoft(R) C Compiler User's Guide has a chart
with several examples of near/far/huge declarations.

The first problem that you may encounter when using far and huge pointers in
a small- or medium-model program is that the conventional pointer size for
passing data pointers to procedures in these programs is 16 bits. Thus you
can't pass a far or huge pointer to a procedure, such as strlen, that
expects an ordinary near pointer. (If necessary, you can extract a library
procedure for different models and link it in your program.) And if you
write your own procedures, be careful to declare the parameter correctly
as far or huge if you intend to pass it far or huge pointers. Far (or huge)
and near pointers as procedure parameters simply can't be mixed.

The second problem with far and huge pointers is that the C language
standard (both the old Kernighan and Ritchie definition and the emerging
ANSI standard) specifies that the difference between two pointers is an
integer, and the result of the sizeof operator is also an integer. This can
cause some problems, which the program in Figure 3 demonstrates.

When the program in Figure 3 is executed with the value 10000 on the
command line, the output is the following:

  Number of Elements: 10000

  1.10000 == (unsigned)(f2-f1)
  2.10000 == (long)(f2-f1)
  3.10000 == (unsigned long)(f2-f1)
  4.10000 == (long)(unsigned)(f2-f1)

As you can see, everything looks OK. However when you increase the size of
the allocation to 40000, which is a perfectly reasonable size for a
character array accessed by a far pointer, the result is the following.

  Number of Elements: 40000

  1. 40000 == (unsigned)(f2-f1)
  2.-25536 == (long)(f2-f1)
  3.-25536 == (unsigned long)(f2-f1)
  4. 40000 == (long)(unsigned)(f2-f1)

The problem in the second line of the output is that casting the result of
the pointer subtraction (a signed integer) to a long is done with sign
extension, thereby producing the wrong answer. The same problem could
occur with a conventional array containing more than 32,767 elements,
but conventional 32Kb element arrays are rare, whereas far and huge
pointers exist specifically to manage large arrays.

The problem in the third line is very subtle, and it took a trip or two to
the Microsoft(R) C Compiler Language Reference Manual to understand the
problem. A conversion from a signed integer (the result of subtracting f2
from f1) to an unsigned long first does a sign extension to long, then
converts that long to unsigned long. The solution is easy: simply cast the
signed integer to unsigned, and then cast that to long (the fourth line).

Figure 4 is a program for exploring the pointer arithmetic behavior of huge
pointers. Since a huge pointer points at a large amount of storage, there
are a few more cases to consider than with far pointers. When you run the
program using the value of 10000 as the number of elements, the following
is printed:

  Number of Long Elements: 10000

  1.      59152 == (unsigned)(h2-h1)
  2.      10000 == (unsigned)(long)(h2-h1)
  3.      -6384 == l1 = h2-h1
  4. 4294960912 == ul1 = h2-h1
  5.      10000 == (long)(h2-h1)
  6.      10000 == (unsigned long)(h2-h1)
  7.      59152 == (long)(unsigned)(h2-h1)

The only cases that work correctly cast the result of h2-h1 immediately into
a long or into unsigned long. The Microsoft manual warns you that without a
cast to long, subtracting two huge pointers will result in an int, with the
disastrous results shown above. In this particular case, the cast to long
has an important semantic meaning that goes beyond its usual meaning of
converting the item to the given type: the cast controls the precision of
the result when subtracting huge pointers. The rub is that it requires
Pointer differences to be an int, but an int (on a 16-bit PC) isn't large

The other distressing fact that you may have noticed is that the casts that
allow you to subtract two far pointers are not the same as those that allow
you to subtract two huge pointers. This means you must be doubly careful
when you use these seemingly similar types; watch out for subtraction.

Another difficulty with both far and huge pointers is the common assumption
that pointers are well behaved. With the Intel segmented architecture,
many different bit patterns allow a given far or huge pointer to access a
given location. Thirty-two-bit pointers have two pieces, the high 16 bits,
which are loaded into a segment register, and the low 16 bits, which are an
offset. In the Intel CPU, the segment register value, shifted left four, is
added to the ordinary register to form a 20-bit address. Thus the pointer
0100:0000H (32-bit Intel pointers are often written with a colon in the
middle for clarity) will access the location 1000H in memory. This is
because the high 16 bits (0100H) are shifted left by four (01000H) and then
added to the low 16 bits (0H) to produce the result 01000H. Figure 5 shows
two other pointer values that will access location 1000H. The moral here is
that comparing two pointers for equality may or may not work, depending on
their bit patterns.

The program in Figure 6 demonstrates the hazards of comparing far and huge
pointers. The first printf statement will print yes or no, depending on
whether the two pointers compared are equal. The second printf will print
yes if the result of subtracting the two pointers is 0; otherwise it will
print no. With ordinary pointers (PDP-11 pointers or 16-bit Intel pointers),
the two results will always be the same. However, for two huge pointers
that point to the same place, even though they have different bit patterns,
the output of the program is the following:

  0100:0000 == 0080:0800 ? no
  (long)(0100:0000 -0080:0800) == 0L ? yes

This strange result, two unequal pointers that have no difference,
actually makes perfect sense. The C compiler compares pointers by
comparing their bit patterns, but it must subtract them more laboriously.
First the compiler forms two 20-bit linear addresses just as the CPU does
in hardware, then subtracts the linear addresses and divides by the size of
whatever the pointer accesses.

If you do conventional things with your far and huge pointers, such as move
them within an allocated chunk of memory, then comparisons will work fine.
However, with anything unconventional such as hand-crafting pointers,
picking them up from hardware devices, or acquiring them from foreign
subroutine libraries, simple comparison is not wise. You can always compare
two pointers by using the expression ((long)(p2 - p1) == 0), but it takes
much longer than the typical expression p2==p1.

You must keep in mind that the bit value in a far or huge pointer is not a
byte offset from location 0. Instead it is in the Intel segment:offset
format that must be added together (see Figure 3) to form a byte offset from
location 0. Most reasons for converting a pointer to a byte offset from 0
are very bad, but if you must, see Figure 7 to do it correctly. The output
of the program in Figure 7 is shown in Figure 8.

Conventional pointers are often cast to an unsigned, or to an unsigned long,
when a byte offset from 0 is required. This will not work with a far or huge
pointer, because of the segment:offset nature of the Intel architecture.
The hptr2long macro is necessary to make sense of the address coded in the
pointer. (The FP_SEG, FP_OFF macro calls in the hptr2long macro extract the
segment and offset parts of a huge pointer. They are defined in the dos.h
include file.)

Now let's delve into the relative efficiency of far and huge pointers.
Before looking at some assembly language code generated by the compiler for
far and huge operations, let's think about what must be done in the two
cases. For a far pointer, the C compiler can access something that it
points towards by loading its high 16 bits into a segment register (usually
ES), loading its low 16 bits into an ordinary register, and then accessing
that location. For huge pointers the operation is the same.

However, pointer arithmetic with far pointers is much simpler than that for
huge pointers. For a far pointer, all arithmetic is done with just the low
16 bits. All the compiler has to do is operate on the low 16 bits, just as
if they were an ordinary near pointer. However, for a huge pointer the
compiler must first do the operation on the low 16 bits, then it must shift
the carry or borrow left 12 bits, and then it must perform the operation on
the high 16 bits. That's a lot of work, which is sometimes assisted by
subroutine calls to library routines.

The program in Figure 9 invokes the four most common pointer operations:
addition of an integer to a pointer, dereferencing a pointer, comparing
two pointers, and subtracting two pointers. The code comes from one of my
programs that manages a huge array using near and far pointers. The
#defines at the top of the program allow it to be easily compiled using
near, far, or huge pointers by defining either the word FAR or HUGE on the C
compiler command line. The code generated for each pointer type is shown in
Figure 10.

The most striking feature of Figure 10 is that the overhead for using near
and far pointers is similar; the heavy penalty does not occur until you
start to use huge pointers. The most burdensome operation on huge pointers
is subtraction; it takes nine in-line instructions, including one
subroutine call to a support routine.

In my work I needed huge pointers to manage an array of 50,000 long
variables. Unfortunately the program calculated too slowly, so I was
forced to speed things up. In my program most calculations only access a
small region of the array, small enough to be accessed with a far pointer,
although the array itself is so large that it has to be managed with huge

My solution was to create a routine that would convert a huge pointer into a
far pointer optimally. In my application, this meant that the resulting far
pointer could be incremented and/or decremented several thousand times
without having the low 16 bits overflow or underflow. Meeting that
condition implies that the low 16 bits should be neither too small (danger
of underflow) nor too large (danger of overflow).

Suppose you have the huge integer pointer 2000:0000H. If you decrement it,
the compiler will decrement the low 16 bits and then propagate the borrow
into the high bits, yielding the correct answer: 1000:FFFEH. However if
2000:000H is a far integer pointer and you decrement it, the compiler will
simply decrement the low 16 bits, yielding a wrong answer: 2000:FFFEH. Far
pointer arithmetic only breaks when there is an overflow or a borrow, so
that far pointers whose low 16 bits are far from 0H and far from FFFFH (that
is, in the neighborhood of 8000H) are quite safe.

I wrote a routine called huge2far (see Figure 11) that rearranges a huge
pointer to make its offset close to 8000H. This creates a far pointer that
can access any element within a few thousand elements of where the pointer
points initially. In other applications the optimal structuring of a near
pointer might be different. For instance, in an application where the near
pointer was only incremented (never decremented), an optional structure
would make the low 16 bits as close to 0 as possible.

Note that the huge2far routine doesn't work for all possible pointers. For
example, there is only one pointer, 0000:0000H, that will access location 0.
But in practice huge pointers always point beyond the program text and data,
and for addresses in those regions the routine works as claimed.

I use huge pointers in my program to manage the array, but whenever an
intensive, local calculation is performed I convert a huge pointer into a
far, do the calculation, and then, if the resulting pointer value must be
saved, I convert the far pointer back into a huge by using a cast. It
wouldn't be worthwhile to convert a huge to far to make a single array
access, but if you want to sum the next thousand numbers in the array, you
can convert huge to far and then use the far pointer a thousand times to
access the next thousand array members. The time saved in my program is

The code in Figure 10 gives some additional clues about writing more
efficient programs with huge pointers. Note that the most expensive
operation, subtraction, is often used in loop bounds checks. Code such as
the following is very common in C programs (p, q, head, and tail are


If these are huge pointers, each loop expends a lot of time comparing two
pointers in each iteration. A more efficient solution would avoid the
comparison on each pass through the loop, as in the following:

  cnt = q - p;
  while(p++, cnt--)
  for(cnt=tail-p, p = head;
  cnt > 0; cnt--, p++)

Of course, you have to be careful that the value of p is not changed in the
body of the loop.

Figure 1:  Understanding Pointers

    int *ip, i;

      ip = &i;
      ip += 100;
      printf("ip - &i: %d\n", ip - &i;
      printf("(unsigned)ip - (unsigned)&1: %u\n",
            (unsigned)ip - (unsigned)&i);

Figure 2:  Microsoft C Version 4. 0 Data Pointers

                       Near            Far              Huge

Size (bits)            16              32               32

Addressing             Anywhere in     Anywhere in      Anywhere
                       64Kb default    any 64Kb data
                       data segment    segment

arithmetic             16 bits         16 bits          32 bits

Microsoft C            Small           Compact          Huge
defaults               Medium          Large

Figure 3:  Pointer Subtraction

 * explore far pointers
#include <malloc.h>
#include <stdlib.h>

char *v[];
     unsigned nelems;
     char far * f1, far * f2;

     if (c != 2) {
         printf("usage: %s elements_to_allocate\n", v[0]);
     nelems = (unsigned)atol(v[1]);

     f1 = (char far *)_fmalloc(nelems * sizeof(char));
     if (f1 == (char far *)0) {
         perror("_falloc failed:");
     f2 = f1 + nelems;

     printf("Number of Elements: %u\n", nelems);
     printf("1. %6u == (unsigned)(f2-f1)\n", (unsigned)(f2-f1));
     printf("2. %6ld == (long)(f2-f1)\n", (long)(f2-f1));
     printf("3. %6ld == (unsigned long)(f2-f1)\n",
            (unsigned long)(f2-f1));
     printf("4. %6ld == (long)(unsigned)(f2-f1)\n",

Figure 4:  Pointer Arithmetic Using Huge Pointers

 * explore huge pointers
#include <malloc.h>
#include <stdlib.h>

char *v[];
     long nelems;
     long huge * h1, huge * h2;
     long l1;
     unsigned long ul1;

     if (c != 2) {
         printf("usage: %s elements_to_allocate\n", v[0]);
     nelems = atol(v[1]);

     h1 = (long huge *)halloc(nelems, sizeof(long));
     if (h1 == (long huge *)0) {
         perror("halloc failed:");

     h2 = h1 + nelems;
     printf("Number of Long Elements: %lu\n", nelems);
     printf("1. %12u == (unsigned)(h2-h1)\n", (unsigned)(h2-h1));
     printf("2. %12u == (unsigned)(long)(h2-h1)\n",
     printf("3. %12ld == l1 = h2-h1\n", l1=h2-h1);
     printf("4. %12lu == ul1 = h2-h1\n", ul1=h2-h1);
     printf("5. %12ld == (long)(h2-h1)\n", (long)(h2-h1));
     printf("6. %12lu == (unsigned long)(h2-h1)\n", (unsigned
     printf("7. %12ld == (long)(unsigned)(h2-h1)\n",

Figure 5:  Three Distinct Pointer Values Point to Location 1000H

Pointer                 0x01000000    0x00ff0010    0x00800800

High 16 bits                0x0100        0x00ff          0x80

Low 16 Bits                    0x0          0x10         0x800

Intel segmentation         0x01000       0x00ff0         0x800

Address computation           +0x0         +0x10        +0x800
                          ________      ________    __________
Result                     0x01000       0x01000        0x1000

Figure 6:  Comparing Far and Huge Pointers

 * equality of huge pointers

     int huge *h1, huge * h2;

     h1 = (int huge *)0x01000000;
     h2 = (int huge *)0x00800800;

     printf("%p == %p ? %s\n", h1, h2, h1 == h2 ? "yes" : "no");
     printf("(long)(%p - %p) == 0L ? %s\n", h1, h2,
         (long)(h1 - h2) == 0L ? "yes" : "no");

Figure 7:  Converting Pointers to a Byte Offset from 0

 * Byte offsets of huge pointers

#include <dos.h>
#include <malloc.h>

/* convert a MSC huge pointer into a byte offset from location 0 */
#define hptr2long(p) (((unsigned long)FP_SEG(p))<<4 + (unsigned

#define NELEM 5

     int huge *hptr[NELEM];
     int i;

     printf("   Huge    Offset\n");
     printf("  Pointer  from 0\n");
     for(i=0;i<NELEM;i++) {
         hptr[i] = (int huge *)halloc(32768L, sizeof(int));
         printf("%p %7ld\n", hptr[i], hptr2long(hptr[i]));

Figure 8:  The output of the program in Figure 7. The first pointer,
           209A:0000H, accesses byte 209A0H on the PC. Note that 209A0H is
           equal to 133536 decimal.

     Huge              Offset
     pointer           from 0

     209A:0000         133536
     309B:0000         199088
     409C:0000         264640
     509D:0000         330192
     609E:0000         395744

Figure 9:  Invoking Common Pointer Operations

#include <malloc.h>

if defined (HUGE)
#define PTYPE Huge
#define MALLOC halloc(NELEM, sizeof(unsighned) )
#define DTYPE long
#elif defined (FAR)
#define PTYPE far
#define MALLOC _fmalloc(NELEM * sizeof(unsigned) )
#define DTYPE unsigned
#define PTYPE
#define MALLOC malloc(NELEM * sizeof(unsighned) )
#define DTYPE unsigned

    unsigned PTYPE *head, PTYPE *aspikes, PTYPE *end;
    unsigned time;
    DTYPE diff;

    head = (unsigned PTYPE *)MALLOC;
    aspikes = head;
    end = head + NELEM;
    while ((time = *aspikes) && aspikes++ < end)
        diff = (DTYPE) (end - aspikes);

Figure 10:  Compiler Code Generation for the Program in Figure 9 for Near,
            Far, and Huge Pointers

C                    NEAR                      FAR                      HUGE

end = head + NELEM;  add ax,20000              add ax,20000             mov a
                     mov [bp-6],ax   ;end      mov [bp-8],ax  ;end      cwd
                                               mov [bp-6],dx            add a
                                                                        adc d
                                                                        mov c
C                    NEAR                      FAR                      HUGE
                                                                        mov c
                                                                        shl d
                                                                        add d
                                                                        mov [
                                                                        mov [

time = *aspikes      mov bx,[bp-8]   ;aspikes  les bx,[bp-12] ;aspikes  les b
                     mov ax,[bx]               mov ax,es:[bx]           mov a
                     mov [bp-4],ax   ;time     mov [bp-4],ax  ;time     mov [

aspikes++ < end      mov ax,[bp-8]   ;aspikes  mov ax,[bp-12] ;aspikes  mov a
                     add WORD PTR [bp-8],2     mov dx,[bp-10]           cwd
                     cmp ax,[bp-6]   ;end      add WORD PTR [bp-12],2   add a
                                               cmp ax,[bp-8]   ;end     adc d
                                                                        mov c
                                                                        shl d
                                                                        add d
                                                                        mov c
                                                                        mov b
                                                                        mov [
C                    NEAR                      FAR                      HUGE
                                                                        mov [
                                                                        mov [
                                                                        cmp c

diff = (DTYPE)(end-
        aspikes);    mov ax,[bp-6]   ;end      mov ax,[bp-8]   ;end     pushd
                     sub ax,[bp-8]   ;aspikes  sub ax,[bp-12]  ;aspikes pusha
                     sar ax,1                  sar ax,1                 pushW
                     mov [bp-2],ax   ;diff     mov [bp-2],ax   ;diff    pushW
                                                                        sar d
                                                                        rcr a
                                                                        mov [
                                                                        mov [

Figure 11:  Rearranging Huge Pointers

#include <dos.h>

char far *
char huge *n;
     register unsigned o;
     register unsigned s;
     unsigned d;
     char far *p;

     o = FP_OFF(n);
     s = FP_SEG(n);
     if (o < 0x7ff0) {
         d = (0x8000 - o) & ~0xf;
         o += d;
         s -= (d>>4);
     else if (o > 0x8000) {
         d = (o - 0x7ff0) & ~0xf;
         o -= d;
         s += (d>>4);
     FP_OFF(p) = o;
     FP_SEG(p) = s;
     return p;


EMS Support Improves Microsoft Windows 2.0 Application Performance

Also see the related article:
  Glossary of Window Memory Terms

Paul Yao☼

In 1981, IBM introduced its Personal Computer, which, among other things,
gave applications developers more memory to work with. You may recall that
the best-selling machine at that time was the Apple(R) II, which, with its
6502 processor was able to address 64Kb of RAM memory. The IBM(R) Personal
Computer was able to address a full megabyte of memory with its Intel(R)
8086 processor. Subtracting the memory reserved for the hardware, the
program address space of 640Kb was ten times the available memory on the
Apple computer. Suddenly, programmers and users found themselves with more
memory than they knew what to do with.

Eventually, users pushed the limits of 640Kb of memory: software became
larger as new features were added, spreadsheets and documents grew as users
became more sophisticated, and, with advances in chip technology, the
price of memory dropped. What once had seemed an enormous amount of memory
was no longer enough.

To meet the need for more memory, the Expanded Memory Specification (EMS)
was introduced in 1984 to provide applications with memory above the 640Kb
limit. EMS defines the software interface for a program to access expanded
memory. Using EMS, an application can store up to 8 additional megabytes of
data in RAM. AST Research Inc. developed a superset of EMS, called the
Enhanced Expanded Memory Specification (EEMS), which provided even greater
flexibility in the way that expanded memory was mapped for use by

The EMS and EEMS specifications were superseded by the Lotus/Intel/
Microsoft Expanded Memory Specification Version 4.0 (LIM EMS 4.0) in August
1987. EMS 4.0 provides up to 32Mb of expanded memory, four times the 8Mb
supported under EMS 3.2. By consolidating two similar, but distinct, memory
specifications, LIM EMS 4.0 simplifies expanded memory support for Windows
since any EMS or EEMS card can be made compatible with LIM EMS 4.0 by adding
a device driver. Besides unifying the expanded memory interface, EMS 4.0
provides multitasking support for expanded memory.

This article looks at how expanded memory works, how Windows 2.0 uses
expanded memory, how virtual memory is implemented in Windows/386, and how
expanded memory support will affect your Windows programming.

Expanded Memory

EMS 4.0 allows applications to go beyond the 640Kb limit by setting aside
part of the 8088's 1Mb address space as a porthole into expanded memory,
called a page frame. Depending on how system memory is used, page frames
range in size from 16Kb to 1024Kb.

Page frames are divided into physical pages, which are typically 16Kb.
Expanded memory is divided into logical pages, also 16Kb in size.
Applications obtain expanded memory by asking the Expanded Memory
Manager (EMM) to allocate a certain number of logical pages. The application
must ask the EMM to map a set of logical pages into the physical address
space of the hardware in order to use them. The application then accesses
expanded memory just as it would access conventional memory. To use another
set of logical pages, the application asks the EMM to map in a new set of
pages. An application cannot access a page when that page is not mapped in.
While the physical address space of the 8088 is 1Mb, expanded memory can
have up to 32Mb.

Figure 1 is a memory map showing the relationship of an MS-DOS(R)
application, such as Lotus(R) 1-2-3(R), to expanded memory logical pages. In
this example, there are two slots in the physical address space, labeled
Slot-1 and Slot-2. The two logical pages, which are labeled Page-3 and Page-
6, are currently mapped into the two slots. Note that the shaded areas are
reserved for the operating system and for hardware use (where display
memory is located). The single page frame resides above 640Kb, although EMS
4.0 allows expanded memory to be mapped into any portion of the physical
address space.

Expanded memory requires special hardware and special software. The hardware
consists of a card that contains memory chips. The software is a device
driver, the EMM, which processes the requests for expanded memory actions
and makes the expanded memory board perform its mapping magic.

Applications must allocate memory by calling the EMM, and by another call
that causes the allocated page to be made accessible by hardware mapping.
The third task that applications must perform is to free allocated expanded
memory once it is no longer needed. These three steps are analogous to the
way that memory is allocated from the Windows memory manager: memory is
allocated (either with GlobalAlloc or LocalAlloc), it is mapped into a
physical address (via GlobalLock or LocalLock), and it is released when no
longer needed (by GlobalFree or LocalFree). The fourth step, unlocking
memory (with GlobalUnlock or LocalUnlock), is analogous to the unmapping of
expanded memory pages, which in EMS is part of the mapping call.

Some EMS boards allow only a single 64Kb page frame, which must physically
appear above 640Kb. Other boards permit multiple page frames with no limit
on the size or location of the page frames. Even though an EMS 4.0 driver
can be written for either type of board, their capabilities require that
Windows deal with each a little differently. Older boards leave the memory
below 640Kb untouched and provide a relatively small page frame. Newer
boards can be configured to take over everything above 256Kb, allowing total
bank space to exceed 600Kb.

As I will discuss later, the size of the available page frame affects how
the Windows memory manager uses expanded memory. I will refer to old-style
EMS support, which allows only a single page frame, as "small frame" EMS and
to the new-style EMS support, which permits multiple page frames, as "large
frame" EMS. Note that the presence of terminate-and-stay-resident programs,
network device cards, and other dedicated memory applications, may cause
Windows to use a newer EMS card in "small frame" EMS mode.

The division between the bankable and the nonbankable portions of a given
EMS memory configuration is called the "bank line." Knowing where objects
will be allocated ("above the bank line" or "below the bank line") will not
affect you if you are writing a single, standalone Windows application.
But if you develop applications that "talk" to each other, write your own
dynamic-link library, use Windows hooks, or wish to use the clipboard or
DDE, you should familiarize yourself with the location of memory objects
relative to the EMS bank line.

The mapping and unmapping of logical expanded memory pages does not require
copying large blocks of data. Instead, it involves modifying a few mapping
registers. Once these mapping registers are changed, memory mapping
automatically occurs on the expanded memory card and is thus fast and
efficient, with little performance degradation.

Expanded Memory and Windows

Windows Version 2.0 uses expanded memory to bank Windows applications. This
expanded memory support lets multiple large Windows applications, such as
Microsoft Excel and Aldus PageMaker(R), run simultaneously and as fast as if
each were running in Windows by itself. Windows 1.x uses expanded memory to
swap typical MS-DOS applications, but not for swapping Windows applications.
Windows 2.0 will support these older applications, but with the advantage of
banking Windows applications in expanded memory.

Memory is the most severely constrained resource in Windows. There are two
reasons for this: one due to the hardware, and the other related to the
multitasking nature of Windows. The hardware constraint is simply that the
address space of the 8086 family of processors is 1Mb. The multitasking
nature of Windows means that the demand on memory is potentially
unlimited, as more and more applications are run. This problem is
partially solved in the design of Windows, which provides a type of virtual
memory support. Unnecessary code and resource segments marked
"discardable" and which are "least recently used," can be destroyed on
demand, freeing up memory. Of course, when the destroyed objects are needed
again, Windows will re-read them from the disk (code and resource
segments in memory are "read-only," which means that the disk image is
guaranteed to be correct). The memory problem is not entirely solved by
discarding. Each application has a minimum memory requirement, so that
two or more large applications may not be able to run simultaneously. This
is a problem because users will most likely want to use the large, powerful
applications together, to cut and paste data, initiate DDE conversations,
and in general to take advantage of Windows' ability to move quickly between

Windows 2.0 solves the problem with its enhanced memory manager, which will
bank each application into its own set of expanded memory pages, so that
each application is, in effect, running on the machine by itself. This works
as long as an EMS card and an EMS 4.0 driver are present. The banking of
application code and data is entirely transparent to Windows programs. The
Windows memory manager performs all required handshaking with the EMM to
allocate memory, map and unmap pages, and to free EMS memory when an
application is closed.

The memory manager could have been enhanced to page in EMS memory so as to
give a single Windows application the full 32Mb of memory that EMS 4.0
supports. The current design was chosen, however, for implementation
considerations, performance goals, and the availability of alternative

One implementation consideration in favor of this design is the need for
Windows 2.0 to be backwards compatible with Windows 1.x applications. This
means that the interface to the memory manager must be the same. To make use
of EMS memory, a new interface would be required. However, this interface
already exists──in the form of the EMS 4.0 specification. Therefore, if an
application has a need for megabytes of memory, it can talk directly to
the EMS memory manager.

The second reason for the current design is to meet the main performance
goal for supporting expanded memory: allowing several large Windows
applications to run simultaneously, with minimum overhead incurred at
context-switching time. The current implementation insists only that each
application be able to run with acceptable performance in 640Kb. After all,
not every computer will have expanded memory, and thus Windows' developers
cannot assume its existence, except, perhaps, in vertical-market packages
where a turnkey system was being developed.

The third reason that Windows EMS support does not provide megabytes of
expanded memory for applications is that a disk cache serves the same
purpose. Code, resource, and other discardable segments can be destroyed
and quickly re-read from the disk cache using the loading and discarding
capabilities of the memory manager. Windows 2.0 includes a disk cacher,
which dynamically allocates and frees EMS pages as needed. Unlike a RAM
drive, optimum performance of the disk cache does not depend on a user

Windows 2.0, unlike the earlier versions, will run in the compatibility box
of OS/2. Even in this mode, Windows will use expanded memory.

How EMS Support Works

The Windows memory manager is responsible for controlling the dynamic
allocation of memory from the global heap, as well as allocation from
each task or library's local heap. Here, we are only interested in memory
allocated from the global heap. The Windows memory manager arbitrates
requests for blocks of memory, handling all the adminstrative overhead when
blocks must be moved or discarded to satisfy allocation requests. When a
block of memory is freed, that is, returned to the available pool of
memory, the Windows memory manager makes a note of this.

Windows pages in a fresh bank of EMS memory for every application that is
loaded. Figure 2 is a memory map showing three Windows applications; Write,
PageMaker, and Microsoft Excel, each in its own bank of expanded memory
pages. In Figure 2, the Microsoft Excel pages are all currently mapped into
physical addresses. Note that only the minimum number of expanded memory
logical pages are allocated for a given application, so that expanded
memory can be used as efficiently as possible.

Figure 2 shows that all of the pages in Microsoft Excel are mapped in. This
happens whenever Microsoft Excel is processing, or, in Windows terms,
whenever Microsoft Excel receives a message via the call to GetMessage. If
the user clicks on the PageMaker window, a context switch makes PageMaker
the active application. During context switching, the memory manager
will bank in all of the PageMaker expanded memory pages, causing all of the
Microsoft Excel pages to be banked out. This has implications for sharing
memory among applications, which will be discussed later.

While the application is processing, the allocation of memory banks
remains fixed. This means that Windows applications are still running in
640Kb (actually up to 256Kb more, or 896Kb, with a 64Kb EGA card installed),
and should be written to obtain satisfactory performance within this

The only objects that will be discarded are those that appear in the
physical address space, that is, in either the currently mapped EMS memory
or below the bank line. Memory objects that are banked out are never

Memory Protection

When an application's EMS memory is not mapped into the physical address
space, it is protected from other "misbehaving" applications. When Windows
is running with large frame EMS, that is, when expanded memory pages can be
mapped into any part of the physical address space, each application is
completely protected from others. When small frame EMS is running, only
those code segments that are in EMS memory are protected. Although this
memory protection is not as complete as that of OS/2, in which illegal
addressing results in a general protection fault, it does reduce the
damage in a way that Windows 1.x did not.

Memory Managers

While Windows is running with EMS memory, there are really two memory
managers present: the Windows memory manager and the EMM. The Windows memory
manager is the primary memory manager under Windows 2.0, calling on the EMM
only to control expanded memory. Its task is to allocate, map and unmap, and
free expanded memory, plus keep track of the EMS registers. Because there
are two separate memory managers, a Windows application can call the EMM
directly once it knows that EMS is present, to allocate memory on its own.

One thing you'll want to know is which objects are allocated above the bank
line and are therefore bankable, and which are allocated below the bank line
and are thus shareable. This discussion is complicated by the fact that EMS
4.0 supports two types of page frames: small and large. The small page frame
for EMS memory is 64Kb, while the large page frame for EMS memory can be up
to 896Kb, given the proper hardware setup. Because almost the entire
address space above 256Kb can be banked by using the large page frame, it
offers the most flexibility to the Windows memory manager. Figure 3 shows
how expanded memory is used for both types of page frames.

More than one instance of a given application, in the current
implementation, will share a given bank (although each instance will have
its own data segment), thus preventing any problems in the sharing of
instance data, either explicitly through GetInstanceData or implicitly
through sharing memory handles. Future versions may offer developers the
option of requesting a separate EMS bank for each instance of an

Certainly large frame gives the memory manager the most flexibility, but
even with small frame there are significant advantages to EMS support. For
small applications, all of the code and resources will fit into banked
memory, allowing many small applications to run with little performance
degradation. Applications like the control panel, the calculator, or the
calendar can be loaded together with a large application such as Microsoft
Excel with a minimum of interference. Small frame EMS support, then, allows
the user to take better advantage of the multitasking abilities in Windows.

Dynamic-Link Libraries

When small frame EMS is present, only the resources and code segments from a
task are loaded into the EMS banks. With large frame EMS the memory manager
places several other types of objects above the line in the EMS banks. These
include task data segments, library discardable code segments, and memory
allocated with GlobalAlloc. These last two deserve some attention because
they raise important implementation issues.

Dynamic-link libraries are accessible from any task. Examples of dynamic-
link libraries under Windows include Kernel, User, and GDI, as well as all
device drivers (keyboard, mouse, timer, display, and printer). Clearly,
library routines, such as the GDI routine TextOut, must be accessible from
any task at any time. When large frame EMS is present, the code in dynamic-
link libraries is handled in one of two ways: fixed code segments live below
the line, discardable code segments live above the line.

Large frame EMS support places library discardable segments above the line
in the bank of the calling task, creating a possibility that at any time a
given library code segment may appear in multiple banks. For example,
multiple copies of the dialog box control manager, which contains the window
procedures for the edit, push button, and other dialog box controls, might
concurrently reside in EMS memory. Although there is redundancy, the net
result is that applications have access to the library segments that they
need, and little room is taken up in the scarce portion of memory below the
bank line.

Ordinarily, when dynamic-link libraries allocate memory with GlobalAlloc and
a large EMS frame is present, the allocation occurs above the line. There
are times, however, when it is necessary for a library to have its global
memory below the line. Some libraries may have global memory objects that
they need to access at all times. In such a case, the GMEM_NOT_BANKED flag
is used in the GlobalAlloc call.

Windows 1.x printer drivers run into compatibility problems because they do
not use the GMEM_NOT_BANKED flag. Printer drivers tend to be the only
libraries that are loaded via the LoadLibrary routine; other libraries are
loaded as "dependent modules" of tasks. (A dependent module is a library
that must be present for a task to execute.) Libraries loaded with
LoadLibrary allocate global memory below the EMS bank line, while dependent
modules, by default, allocate global memory above the bank line.

Changes to the API

Only a single routine, LimitEMSPages, has been added to the Windows API.
This routine allows an application to control the number of EMS pages that
Windows allocates on its behalf. It does not, however, limit the number of
pages that an application can allocate for itself from the EMS memory

When you want to run Windows with EMS memory disabled, put the /n switch
after the WIN command at the DOS prompt:

  C> win /n

Virtual Memory

The biggest advantage of Windows/386 is its support of standard MS-DOS
applications. The current implementation of Windows/386 creates multiple
virtual machines (VMs), each of which behaves as an independent 8086.
Because each is effectively given its own 8086 machine with 640Kb of address
space, applications can run independently without interfering with each
other. Also, because of the ability of the Intel 80386 to intercept direct
memory mapped video output, each MS-DOS application can run in its own
window on the screen.

Note, however, that Windows/386 does not give each Windows application its
own 640Kb partition; instead, all Windows applications run in the same 640Kb
partition. This means that Windows applications do not have the same memory
advantages that MS-DOS applications have running under Windows/386.

Memory banking support in Windows/386 is the same as EMS support in Windows
2.0. Although it works a little differently, because the 386 has hardware
support for memory management, the differences will be transparent to the
Windows application.

Programming Guides

The programming guidelines listed here should be no surprise to Windows
programmers, who have been told to be good citizens since they started
writing their first window procedure. It is useful to reiterate them,
however, because Windows EMS support under Windows 2.0 and virtual memory
support under Windows/386 require that you follow them strictly. These
guidelines will apply to both, but a few are specific to the addition of
EMS support and will help you ensure that your applications run in both
Windows 2.0 and Windows/386.

Efficient Memory Use

There are no current plans for Windows memory management to allow Windows
applications to allocate past the 1Mb physical address space of the 8086.
Instead, Windows performs bank switching of memory, so each application
behaves as if it had the entire computer to itself.

The Windows memory manager lets you specify that memory allocated from the
global heap, when EMS is running, be below the bank line, so that it is
always visible from all applications. You do this with the GMEM_NOT_BANKED
flag to the GlobalAlloc call. You should regard this as a method of last
resort for sharing memory, because memory below the line is very scarce and
is only used for the most critical system objects. Unless you have a very
compelling reason, do not perform this type of allocation. A good example
of a global allocation that must be below the line is the allocation
required by a printer driver.

Code Structure

To optimize the performance of your application with small frame EMS, mark
the most important segments as PRELOAD in the module definition (.DEF)
file, and list their names at the beginning of the SEGMENTS list.
Performance is improved because, when Windows starts loading an application
into memory, it starts with the bankable EMS memory. Only after the EMS
bank is filled does Windows start using space below the line. Performance
improves with small frame EMS because the memory manager will not discard
objects from above the bank line.

Data Sharing

The clipboard, DDE, and libraries are methods of data sharing guaranteed to
work in current and future versions of Windows. Other than the clipboard
and DDE, you should not share memory by passing global memory handles
between applications. Passing long pointers to data objects should also be
avoided. You can't be sure that the target data will be banked in when the
receiving application is processing. Applications that ignore this
guideline will not run under planned versions of Windows 2.0 or

Be sure to copy the data from the clipboard into your own data areas before
you close and release the clipboard. Windows supports EMS by copying
clipboard objects that have been banked out into the currently banked
memory. After the clipboard is closed, these objects will be destroyed,
rendering them inaccessible to the receiving application.

When an application receives a global handle through the clipboard or DDE,
it must check the return value to the GlobalLock call. The Windows memory
manager copies into the current bank those DDE or clipboard objects that
reside in unbanked EMS memory. If the memory manager is not able to perform
this copying (such as in a low memory situation), it informs the receiving
application by returning null to GlobalLock.

Code Sharing

One innovation of Windows is its ability to share code. Only a single copy
of any given Windows routine is loaded into memory at any given time. It
can be accessed, via the dynamic linking mechanism, by several applications.
Window procedures are shared among multiple instances of windows and fine-
tuned through subclassing. If you write well-behaved Windows applications,
code sharing by EMS memory is not affected: since several instances of an
application run in the same bank of EMS memory, there is no problem in
sharing code, data, or resources.

Dynamic-link libraries are one method for sharing code among several
cooperating applications. Windows loads library discardable segments above
the bank line, so there can be multiple copies of some code segments in EMS
memory. This redundancy can be avoided by converting the library into a
windowless task and communicating with the calling applications via the
various message posting routines (PostMessage and PostAppMessage).

You can never execute code that belongs to another task by calling it
directly. In Windows routine terms, you can never call GetProcAddress on
another task in order to directly call the target code yourself. Although it
worked in Windows 1.x, the Windows 2.0 memory manager won't let you do this.
Instead, use the SendMessage routine to perform intertask calls.

An application can directly call the EMM to allocate EMS memory, provided it
follows EMS 3.2 guidelines; it may use the reallocation function (function
17) from LIM EMS 4.0. Windows must be notified so that it does not try to
use the 64Kb EMS 3.2 page frame; this is achieved by adding the switch
-LIM32 to the call to the resource compiler. Windows avoids putting code in
the 64Kb EMS 3.2 window for your application, but it can still use this
page frame to bank other applications.

Global Memory

You should not allocate global memory below the line, that is, you should
not use the GMEM_NOT_BANKED switch to GlobalAlloc except as a final resort.
When global memory objects are shared among applications, the call to
GlobalLock takes care of copying the appropriate blocks of memory, as
necessary. Of course, this should only be used with the clipboard and DDE
methods to guarantee compatibility with future versions of Windows 2.0,
most notably, Windows/386.

Your program should always check for failure in allocating memory,
reallocating memory, and locking memory. This is critical when using DDE or
the clipboard to share data, and it is generally a good Windows programming
practice. An invalid far pointer can be deadly, since it allows data to be
written or read from anywhere in the computer's address space.

Windows Hooks

Windows hooks allow you to trap certain types of events before they are
placed into the system queue. For example, you can write a keyboard hook to
watch for the <Alt>-P key, which may cause a background screen capture
program to print the screen on the default printer.

The impact of EMS memory on Windows hooks requires some extra programming
effort on your part. First of all, Windows hooks must appear below the bank
line and should be placed in fixed library seg-ments. Also, because a
window hook has no way of knowing whether the initiating task is currently
banked in, it must communicate with the initiating task via messages (most
likely SendMessage).

Debugging Support

You can now use CodeView(R), Microsoft's premier debugging tool, to debug
Windows 2.0 applications. This is a welcome improvement over Symdeb, a
powerful but cryptic debugging tool that appeals mostly to assembly language
programmers. CodeView requires either a monochrome monitor or a dumb
terminal, because when Windows is running it takes over the screen. Another
hardware requirement for using CodeView is that you must have EMS memory
installed since CodeView talks directly to the expanded memory manager to
minimize the amount of memory taken from the program being debugged.

To facilitate debugging when EMS is present, you may want to place the
following switch in your WIN.INI file:


which allows you to set breakpoints within discardable library code. Recall
that there can be multiple copies of discardable library code segments,
each in a different EMS bank. This switch tells the memory manager to update
the debugger's table of library discardable segments during every context
switch. Any debugger that works with Windows, including Symdeb, CodeView,
Answer, or Atron will work properly with EMS.


For the most part, expanded memory support under Windows will be
transparent to the programmer. With the exception of a single new Windows
routine, LimitEMSPages, a few flags, and resource compiler switches, the
interface that Windows programmers work with has not changed. What has
changed, however, is that there must be stricter adherence to interactions
between tasks. Data sharing, for example, must be performed with one of the
three supported methods: the clipboard, DDE, or via a shared dynamic-link
library. Directly calling into another task's code, which is never a good
idea, is not possible with the Windows 2.0 memory manager.

Programmers will benefit most from the improved performance of Windows 2.0
when running several large applications, which will make products that
interact with applications like Microsoft Excel and PageMaker more
attractive to users. Windows programmers can also, of course, write their
own large, powerful applications. Because the EMS support is transparent to
the Windows programmer, he or she can use the existing programming
interface and let the Windows memory manager take advantage of EMS.

Figure 1:  This memory map shows an MS-DOS application such as Lotus 1-2-3
           using EMS 3.2 expanded memory.

                   Physical Address            Logical Pages of
                        Space                  Expanded Memory

            1024Kb┌─────────────────┐        ┌─────────────────┐
                  │   System ROM    │        │       L-0       │
             960Kb├─────────────────┤        ├─────────────────┤
                  │     Unused      │        │       L-1       │
 ┌──────────┬832Kb├─────────────────┤        ├─────────────────┤
 │EMS 3.2   │     │       P-0       ├────┐   │       L-2       │
 │Page Frame│     ├─────────────────┤    │ ┌─├─────────────────┤
 │made up of│     │       P-1       ├──┐ └►│ │       L-3       │
 │four 16Kb │     ├─────────────────┤  │   └─├─────────────────┤
 │physical  │     │       P-2       │  │     │       L-4       │
 │pages.    │     ├─────────────────┤  │     ├─────────────────┤
 │          │     │       P-3       │  │     │       L-5       │
 └──────────┴786Kb├─────────────────┤  │   ┌─├─────────────────┤
                  │   EGA Memory    │  └──►│ │       L-6       │
             640Kb├─────────────────┤      └─├─────────────────┤
                  │                 │        │       L-7       │
                  │                 │        ├─────────────────┤
                  │     MS-DOS      │        │       L-8       │
                  │   Application   │        ├─────────────────┤
                  │     Program     │        │       L-9       │
                  │                 │        ├─────────────────┤
                  ├─────────────────┤        │      L-10       │
                  │ Expanded Memory │        ├─────────────────┤
                  │     Manager     │        │      L-11       │
                  ├─────────────────┤        ├─────────────────┤
                  │     MS-DOS      │        │      L-12       │
                  └─────────────────┘        ├─────────────────┤
                                             │      L-13       │
                                             │      L-14       │ │logical
                                             │      L-15       │
                                             │        ∙        │
                                             │        ∙        │
                                             │        ∙        │

Figure 2:  This sample memory map shows Write, Pagemaker, and Microsoft
           Excel code and data segments loaded into EMS memory. The Micro-
           soft Excel segments are currently mapped into the physical
           address space of the CPU.

┌────────────┐       ┌──────────────┐         ┌───────────────────┐
│  Banked    ├───────┤  System ROM  │         │  Write            │
└────────────┘ 960Kb └──────────────┘         ├───────────────────┤
┌────────────┐        ──────────────          │  Write            │
│  12-16Kb   │        ──────────────          ├───────────────────┤
│  physical  ├─────── ──────────────          │  Write            │
│  pages     │        ────────────── ┐        ├───────────────────┤
└────────────┘        ────────────── ├────┐   │  PageMaker        │
┌────────────┐ 768Kb  ┌─────────────┐┘    │   ├───────────────────┤
│  Not       ├────────┤  EGA        │     │   │  PageMaker        │
│  Banked    │        │  Memory     │     │   ├───────────────────┤
└────────────┘ 640Kb  └─────────────┘     │   │  PageMaker        │
┌────────────┐        ──────────────      │ ┌─├───────────────────┤
│  24-16Kb   │        ──────────────      │ │ │  Microsoft Excel  │
│  physical  ├─────── ──────────────      │ │ ├───────────────────┤
│  pages     │        ────────────── ┐    └►│ │  Microsoft Excel  │
└────────────┘        ────────────── ├──┐   │ ├───────────────────┤
─ Bank Line ─  256Kb  ┌─────────────┐┘  │   │ │  Microsoft Excel  │
┌────────────┐        │             │   │   └─├───────────────────┤
│            │        │             │   │     │  Write            │
│            │        │  Windows    │   │     ├───────────────────┤
│            │        │  Shareable  │   │     │  Write            │
│            │        │  Memory     │   │     ├───────────────────┤
│            │        │             │   │     │  PageMaker        │
│  Not       │        │             │   │     ├───────────────────┤
│  Banked    │        ├─────────────┤   │     │  PageMaker        │
│            ├────────│  Windows    │   │     ├───────────────────┤
│            │        ├─────────────┤   │     │  PageMaker        │
│            │        │  Expanded   │   │   ┌─├───────────────────┤─┐16Kb
│            │        │  Memory     │   │   │ │  Microsoft Excel  │ ├logical
│            │        │  Manager    │   │   │ ├───────────────────┤─┘page
│            │        │             │   └──►│ │  Microsoft Excel  │
│            │        ├─────────────┤       │ ├───────────────────┤
│            │        │  MS-DOS     │       │ │  Microsoft Excel  │
└────────────┘        └─────────────┘       └─└───────────────────┘
                        Physical                Logical Pages of
                        Address                 Expanded Memory

Figure 3:  Expanded Memory is Used for Both Types of Page Frames

                                Always               Above
                               below the         ┌──the line───┐
                                 line            Small     Large
                                                 Frame     Frame

Task databases                     √
Module (EXE) headers               √
Library data segments              √
Library fixed Code                 √
Thunks                             √
Library resources                  √
Task data segments                                 √         √
Task Resources                                     √         √
Task data segments                                           √
Library discardable Code                                     √
Dynamically Allocated Memory
(via GlobalAlloc)                                            √

Glossary of Windows Memory Terms

Bank line - Also called the EMS swap line, it is the memory address below
which memory is not banked and above which memory is banked. When Windows is
running with expanded memory, the nonbanked memory is always available,
while the banked memory is only available when the task that owns the banks
is running.

Banked memory - Memory that is made to appear within (mapped into) the
physical address space of the 8088 by means of a software driver (the
Expanded Memory Manager) working with an expanded memory card.

Context switch - The changes that Windows must make when the user turns his
attention from one Windows application to another. For example, suppose both
WindowsWrite and WindowsPaint are open. If the user has been entering text
into WindowsWrite and then clicks the mouse in WindowsPaint, the system
performs a context switch. The only thing the user sees is that the caption
bar changes color. If the newly active window was covered by another
overlapping window in Windows 2.0, it is brought to the top.

Discardable memory object - One of the flags that the Windows memory manager
sets to indicate various characteristics of the memory object when it
allocates memory. When the Windows memory manager is unable to satisfy
allocation requests by simply moving memory objects, it will throw away
discardable memory objects on a least-recently-used basis.

Dynamically allocated memory - The memory that a Windows program directly
obtains from the Windows memory manager. In this article, the term is
synonymous with globally allocated memory.

Enhanced Expanded Memory Specification (EEMS) - An extension to the Expanded
Memory Specification that allows any part of the 8088 address space to be

Expanded Memory Specification (EMS) - This specification, also known as LIM
EMS, defines how a program allocates and uses expanded memory. This consists
of calls to the expanded memory device driver (see Expanded Memory

Expanded Memory Manager (EMM) - A device driver that controls expanded
memory. The EMS defines how a program communicates with the EMM to allocate,
map, and free logical pages of expanded memory.

Extended memory - The memory above 1Mb on a 80286- or 80386-based
microcomputer. Memory of this type can only be directly addressed when
running in protected mode, such as in OS/2. Ordinarily, extended memory is
used for RAM disks and print spoolers. Most memory cards can be configured
for expanded memory, extended memory, or both.

Handle - A token that is returned when an object is created. Handles
identify Windows' dynamically allocated memory objects.

Logical page - A segment of expanded memory. EMS memory is allocated in
logical pages, which are typically 16Kb. In order to use data stored in a
logical page, EMS must map the logical page into a physical page.

Mapping - The process by which a logical page of expanded memory is made to
appear in a physical page, that is, within the address space of the 8088, so
that programs can read and write to the memory.

Old application support - The ability of Windows to execute standard,
character-based MS-DOS applications, such as Microsoft Word, Lotus 1-2-3, or
Microrim(R) R:BASE(R) System V.

Page frame - A collection of physical pages into which expanded memory
logical pages can be made to appear.

Physical page - The absolute physical space in the 8088's 1Mb of address
space into which EMS logical pages are mapped.

Thunk - A small piece of code that sits in a fixed place in memory to
intercept the calls to a routine located in a moveable or discardable code
segment. Thunks are an important part of the dynamic linking mechanism of
Windows, allowing the memory manager to move or discard code segments with
relatively little overhead expended towards notifying the prospective
callers of a routine that the routine is no longer resident. Subsequent
calls cause the thunk to reload the segment containing the routine. Return
thunks are created to intercept a return to a discarded code segment. Thunks
also perform context switches; this is what the thunk created by
MakeProcInstance does to ensure that a dialog box procedure is using the
correct data segment.

Window - Another term for an EMS page frame.

Windows memory manager - The part of Windows that loads code and data
segments into memory. The Windows memory manager attempts to satisfy dynamic
allocation requests by first moving memory objects; if this is not
sufficient, it then discards memory objects that are marked as discardable.
The memory manager in Windows 2.0 has been extended to bank-switch memory
between Windows applications.


LIM EMS 4.0: A Definition For the Next Generation of Expanded Memory

Marion Hansen and John Driscoll☼

Back in the bad old days, if your spreadsheet program returned a memory-
full error, you were out of memory──and out of luck. All that changed in
1984, when Lotus Development Corp., Intel Corp., and Microsoft Corp.
smashed the 640Kb conventional memory barrier with their Expanded Memory
Specification (EMS). The EMS allowed the creation of another kind of
memory: expanded memory.

Expanded resides on one or more add-in boards in the computer (Intel's
Above(TM) Board and AST's Advantage!(TM) are examples). The EMS defines a
segment of memory, located within the first 1Mb of memory, as the page
frame. The page frame is a window into expanded memory.

Just after an application program starts executing, it can allocate a
certain number of 16Kb pages of expanded memory for its own use. By
mapping pages in and out of the frame, the program can access any area of
expanded memory it has allocated for itself.

MS-DOS(R) cannot access expanded memory directly; instead, the job of
handling the extra memory goes to the Expanded Memory Manager (EMM), which
is defined by the EMS. EMM functions can be added to any program to enable
it to use expanded memory. (See "Expanded Memory: Writing Programs that
Break the 640Kb Barrier," MSJ, March 1987, for full details on expanded
memory and how to write programs that use it.)

To avoid confusion, remember that EMS is the Expanded Memory
Specification, and that EMM is the Expanded Memory Manager. EMS (the spec)
defines the EMM (the manager), and the EMM provides the functions that
application programs need to use expanded memory.

In the fall of 1987 Lotus, Intel, and Microsoft released the latest EMS,
Version 4.0. This article describes the enhancements to EMS 4.0 and how to
use them for even greater expanded memory performance.

New Feature Set

Intel sent a free EMM 4.0 upgrade to all registered Above Board users.
Except for faster execution of expanded memory tests at power-on, however,
users will not see improved performance with their existing programs
until they have access to new application programs written specifically to
the EMS 4.0 spec. Even so, certain features of EMS 4.0 have generated
excitement in the expanded memory world:

  ■  With AST's endorsement, EMS 4.0 is now the expanded memory standard
     for the personal computer industry.

  ■  The 15 new functions defined by EMS 4.0 provide faster performance
     and more efficient use of memory.

  ■  EMS 4.0 supports a full 32Mb of expanded memory; earlier versions
     only supported up to 8Mb.

The OS/2 systems don't run on 8088- or 8086-based computers. EMS 4.0
extends the useful life of existing MS-DOS-based applications on 8088-,
8086-, 80286-, and 80386-based computers.

  ■  Many software vendors are already updating their programs (such as
     Lotus(R) 1-2-3(R) Version 3 and Microsoft Windows 2.0) in order  to take
     advantage of EMS 4.0's new features.

  ■  Software written for earlier versions of the EMS is compatible with
     EMS 4.0.

  ■  EMM 4.0 runs on existing hardware; you don't need new expanded
     memory boards to install EMM 4.0. However, not all expanded memory
     boards currently support every enhancement to EMM 4.0. For example,
     the AST RAMpage!(R) board and the Intel Above Board 2 support placing
     the page frame in memory below 640Kb, while the Intel Above Board
     286 currently does not.

EMS 4.0 Enhancements

The enhancements to EMS 4.0 improve performance and memory efficiency and
make it easier to write programs that use expanded memory.

  ■  With EMM 4.0, one calls maps in all logical pages that can be
     physically mapped  to one page frame; earlier versions required each
     page to be mapped separately.

  ■  New functions allow application programs to dynamically increase
     and decrease the amount of expanded memory allocated to them. With
     each program using just the amount of expanded memory it needs,
     multiple programs can share expanded memory more efficiently.

  ■  Now you can name handles-the values that the EMM assigns and uses to
     identify a block of memory requested by an application program-so
     that data and code can be shared by application programs.

  ■  EMS 4.0 adds special functions for operating systems (such as MS-
     DOS) and environments (such as Windows and Desqview) to allow
     programs  to isolate themselves from all other software in the
     system, thus  providing a safe environment for programs sharing
     expanded memory.

  ■  New EMS functions directly support executing code in expanded memory.
     Earlier versions supported the execution of code in expanded
     memory, but implementation was cumbersome. For example, before EMS
     4.0, terminate-and-stay-resident (TSR) programs required commands to
     explicitly map the new context into expanded memory when the TSR
     program was called up and to restore the previous context when the
     TSR returned control to the application program. Now the job of
     mapping and restoring contexts can be handled directly with EMS 4.0
     functions, which allows for easier and more efficient execution of

  ■  Earlier EMS versions put the page frame in an unused 64Kb block of
     memory between 640Kb and 1Mb. EMS 4.0 supports the page frame
     anywhere in the first 1Mb of memory. Hardware may limit the
     location of the page frame-in an 80286-based computer such as an IBM(R)
     PC/AT(R), for example, the page frame can reside only between 256Kb
     and 1Mb. For an 80386-based computer or an IBM PS/2(TM) computer
     with the Micro Channel(TM), on the other hand, the page frame can be
     anywhere in the first 1Mb of memory.

  ■  Before EMS 4.0, the page frame held four pages. Now you can define a
     page frame of up to eight pages in memory above 640Kb. The size of
     the page frame in memory below 640Kb is limited only by the amount of
     available memory. (In some implementations of the EMM, you can
     use all 640Kb for the page frame by putting the EMM itself into
     expanded memory.)

  ■  Although the standard size of expanded memory pages is 16Kb, some
     expanded memory boards use smaller pages. EMS 4.0 supports both
     16Kb pages and smaller-sized pages.

The Page Frame

Although EMM 4.0 supports locating a page frame in any available memory
between 0Kb and 1Mb, how you plan to use the page frame determines whether
you'll place it above or below 640Kb. For application programs, continue
to locate the page frame above 640Kb because existing applications may
not work when the page frame is located below 640Kb. Locate page frames
used by most operating systems and environments in memory below 640Kb.
(An exception to this is the Windows/386 EMM, which cannot use a page
frame located below 640Kb.) In any case, you cannot create a page frame in
memory below 640Kb unless you first create a page frame in memory above

New EMM Parameters

The LIM EMM 4.0 device driver has new parameters, one of which is of
special interest to users who are concerned with speed and efficiency.

The handle count parameter lets you tell the EMM to support as many
handles as a particular application program needs. (A handle is a value
that the EMM assigns and uses to identify a block of memory requested by
an application program.) The EMM allocates memory based on the number of
handles requested by the application. By specifying a small handle count,
you can save conventional memory and allow the EMM to run faster.

The EMM 4.0 handle count default is 64 handles; programs written for
earlier versions use a maximum of 32 handles. If you are using older
programs with EMM 4.0, consider changing the EMM handle count to 32 for
faster execution. The maximum number of handles is 254.

The EMM Functions

The EMM functions provide the tools application programs need to use
expanded memory. EMS 4.0 doubled the number of EMM functions. Figure 1
lists all 30 functions; the new functions are shaded.

Functions 16 through 30 are new to EMM 4.0. This section describes each
new function. Functions 26, 28, and 30 are for use by operating systems
(such as MS-DOS) and environments (Windows and Desqview, for example);
the rest are for application programs.

Program Functions

Get/Set Partial Page Map (Function 16) handles situations Functions 8, 9,
and 15 cannot handle. Functions 8 and 9, respectively, handles only four-
page page frames and can save and restore expanded memory pages only when
the page frame is located between 640Kb and 1Mb. Function 15 saves and
restores all the pages (no matter how many pages are in the page frame),
even pages located below 640Kb. Function 16 also saves and restores all
pages anywhere between 0 and 1Mb, but, unlike Function 15, it lets you
choose the pages you want to save or restore.

Function 16 has three subfunctions. The Get Partial Page Map subfunction
saves a partial mapping context for specific mappable memory regions in a
system. This subfunction can be faster than Function 15 because it saves
only a subset of the entire mapping context and uses much less memory for
the save area. You can use the Get Partial Page Map subfunction with or
without a handle, while Function 8 requires a handle.

The Set Partial Page Map subfunction restores a partial mapping context
for specific mappable memory regions in a system. Like Get Partial Page
Map, this subfunction uses far less memory for the save area than Function
15, and you can use it with or without a handle.

The Get Size of Partial Page Map Save Array subfunction returns the
storage requirements for the array passed by the Get/Set Partial Page Map
subfunctions. Use this subfunction before the other two

Map/Unmap Multiple Handle Pages (Function 17) maps or unmaps (in one
invocation) logical pages into as many physical pages as the system
supports. This means less overhead than mapping pages one at a time (as
required by EMS versions before 4.0). Use this function instead of
Function 5 for application programs that do a lot of page mapping.

Reallocate Pages (Function 18) allows an application program to
dynamically increase or decrease the number of logical pages allocated to
an EMM handle, so programs can share expanded memory more efficiently.
Function 18 does not determine the number of pages a program needs;
rather, the program itself must specify the number of pages to allocate
or deallocate (depending on its needs at any one time).

Get/Set Handle Attribute (Function 19) defines a handle as volatile or
nonvolatile. If it is nonvolatile, the handle, its name (if it has one),
and the contents of the pages allocated to it are maintained after a warm
boot. For example, this function saves the contents of a RAM disk in
expanded memory after a warm boot. Function 19 is not supported by most
hardware because it disables memory refresh signals for a long period
of time.

Function 19 has three subfunctions. The Get Handle Attribute
subfunction returns the attribute (volatile or non-volatile) associated
with a handle. The Set Handle Attribute subfunction changes the
attribute associated with a handle. Before using Function 19's other two
subfunctions, you can use the Get Attribute Capability subfunction to
determine whether the EMM supports the non-volatile attribute.

By naming the handle associated with a block of memory, Get/Set Handle
Name (Function 20) lets application programs share the same area in
expanded memory associated with a handle. Assigning a name to a handle
also protects the memory specified by the handle because only a program
that knows the handle name can get that handle and use the memory assigned
to it. Function 20 has two subfunctions. The Get Handle Name subfunction
gets the eight-character name assigned to a handle. The Set Handle Name
subfunction assigns an eight-character name to a handle.

Get Handle Directory (Function 21) has three subfunctions. The Get
Handle Directory subfunction returns an array which contains all active
handles and their associated names (if any).

The Search for Named Handle subfunction searches the handle name directory
for the specified handle name. If it finds the name, the subfunction then
returns the handle number associated with the name. An application
program is able to use this subfunction to determine if a shared handle

The Get Total Handles subfunction returns the total number of handles
that the EMM supports. (Different versions of EMM support different
numbers of handles. EMM 4.0 supports up to 254 handles; earlier
versions support just 32).

Functions 22 and 23 make executing code in expanded memory easier and more
efficient. Alter Page Map and Jump (Function 22) changes the memory
mapping context and transfers control to the specified address. The
original memory mapping context is lost. This function is analogous to
the FAR JUMP in the 8086 family architecture.

Alter Page Map and Call (Function 23) contains two subfunctions. The Alter
Page Map and Call subfunction saves the current memory mapping context,
alters the specified memory mapping context, transfers control to the
specified address, and restores the state of the specified mapping
context after the return. This subfunction is analogous to the FAR CALL
in the 8086 family architecture.

The Alter Page Map and Call subfunction pushes information onto the stack.
The Get Page Map Stack Space Size subfunction returns the number of
bytes of stack space the Alter Page Map and Call subfunction needs to do
this. Use the information provided by Get Page Map Stack Space Size to
alter the stack size before using either Function 22 or the Alter Page Map
and Call subfunction.

Move/Exchange Memory Region (Function 24) lets you move large amounts of
memory without mapping and unmapping pages. You can move or exchange up
to 84 pages without mapping any logical pages. Function 24 has two
subfunctions. The Move Memory Region subfunction copies and the Exchange
Memory Region subfunction exchanges: conventional memory to
conventional memory, conventional memory to expanded memory, expanded
memory to conventional memory, and expanded memory to expanded memory.
The subfunctions maintain the current mapping context, so the program
does not have to save and restore it.

Get Mappable Physical Address Array (Function 25) tells an application
what physical memory is mappable. Function 2 (get page frame address) gives
the segment address of the first four pages in the page frame. Use Function
25 to find the amount of mappable physical memory for page frames larger
than four pages. Function 25 has two subfunctions. The Get Mappable Physical
Address Array subfunction returns an array containing the segment address
and physical page number for each mappable physical page in a system. The
array is a cross reference between physical page numbers and the actual
segment addresses for each mappable page in the system. The array is sorted
by segment address in ascending order, but the physical page numbers
associated with the segment address are not necessarily in ascending order.

The Get Mappable Physical Address Array Entries subfunction gets the
number of entries needed by the array returned by the Get Mappable
Physical Address Array subfunction. Before using the Get Mappable
Physical Address Array Entries subfunction, use the Get Mappable
Physical Address Array subfunction to determine how much memory to
allocate for storing the physical address array.

Allocate Raw Pages (Function 27) with two subfunctions allocates the
number of raw pages requested by the operating system or environment
and assigns a unique EMM handle to these pages. (Some expanded memory
boards have pages of less than the standard 16Kb. These non-standard sized
memory pages are called raw pages.) Handles assigned to raw pages are
called raw handles. Function 4 (Allocate Pages) allocates only 16Kb
pages; it does not support raw pages.

Programs whose page frames are in conventional memory must use Prepare
Expanded Memory Hardware for Warm Boot (Function 29) when they detect
Ctrl-Alt-Del. This function prepares the expanded memory hardware for an
impending warm boot.

Environment Functions

The following functions are for inclusion in operating system and
environment programs so the operating system/environment knows the kind
of hardware support available for expanded memory. Never use Function 26,
28, or 30 in application programs.

Get Expanded Memory Hardware Information (Function 26) is only for use
by operating systems (such as DOS) and environments (such as Windows
386 and Desqview). The operating system/environment can disable Function
26 at any time. The function has two subfunctions. The Get Hardware
Configuration Array subfunction returns an array containing expanded
memory hardware configuration information for use by operating systems
and environments. The operating system/environment uses this information
to determine the hardware support for expanded memory, including raw page
size, alternate register sets available, context save area size, and
register sets for DMA channels.

Some expanded memory boards have pages of less than the standard 16Kb,
called raw pages. Function 3 (Get Unallocated Page Count) returns only
the number of 16Kb pages. The Get Unallocated Raw Page Count subfunction
returns the number of unallocated raw mappable pages and the total number
of raw mappable pages in expanded memory to the operating

Alternate Map Register Set (Function 28) is for use by operating systems
and environments only. It has nine subfunctions.

The Get Alternate Map Register Set subfunction responds in one of two
ways, depending on the setting of the map register set which is active
when the function is invoked. If the map register set is equal to zero, a
pointer to a context save area is returned. If the map register set is
greater than zero, the number of the alternate map register set is

The Set Alternate Map Register Set subfunction responds in one of two
ways, depending on the map register set specified. If the alternate map
register set equals zero, map register set zero is activated, and the
contents of the map register context save area is copied into register
set zero on each expanded memory board in the system. If the alternate
map register set specified is not zero, the alternate map register set
specified is activated. The restore area, which the operating system is
pointing to, is not used.

The Get Alternate Map Save Array Size subfunction returns the storage
requirements for the map register context save area that is referenced by
the other subfunctions.

If an alternate map register is available, the Allocate Alternate Map
Register Set subfunction gets its number and copies the currently active
alternate map register set's contents into the newly allocated alternate
map register set's mapping registers. This does not change the alternate
map register set in use but prepares the new alternate map register set
for a subsequent Set Alternate Map Register Set subfunction. Operating
systems can use this subfunction to quickly switch mapping contexts.

The Deallocate Alternate Map Register Set subfunction returns the
alternate map register set to the memory manager. The memory manager can
then reallocate the alternate map register set. This subfunction also
makes the mapping context of the specified alternate map register
unavailable for reading or writing, thus protecting the pages previously
mapped in an alternate map register set by making them inaccessible. The
current alternate map register set cannot be deallocated, so memory
currently mapped into conventional and expanded memory is inaccessible.

The four DMA subfunctions let operating systems/environments use DMA
register sets. (Hardware that supports these subfunctions is not yet
available.) If a DMA register set is available, the Allocate DMA Register
Set subfunction gets the current number of a DMA register set for an
operating system/environment. This subfunction is useful with
multitasking operating systems, in which you would like to switch to
another task when one task is waiting for DMA to complete. The Deallocate
DMA Register Set subfunction deallocates the specified DMA register set.

The Enable DMA on Alternate Map Register Set subfunction allows DMA
accesses on a specific DMA channel to be associated with a specific
alternate map register set. This function is useful in a multitasking
operating system, where you would like to switch to another task until
the first task's DMA operation completes. The Disable DMA on Alternate
Map Register Set subfunction disables DMA accesses for all DMA channels
which were associated with a specific alternate map register set.

Enable/Disable OS/E Function Set (Function 30) is only used by operating
systems and environments. It includes three subfunctions which are
Enable OS/E Function Set subfunction that will enable Function 26 (Get
Expanded Memory Hardware Information), Function 28 (Alternate Map
Register Sets), and also Function 30 (Enable/Disable Operating System

The Disable OS/E Function Set subfunction disables Functions 26, 28, and

The Return Access Key subfunction lets the operating system/environment
return the access key to the EMM. Returning the access key to the EMM
enables access to the operating system/environment function set.

Example Programs

The following two examples (written in Microsoft(R) C, Version 5.0, and
Microsoft MASM, Version 5.0) illustrate how functions new to EMS 4.0 can
be implemented. The first example shows how expanded memory can be shared
between two application programs. The first program (SAVSCR.EXE) saves
the current screen, initializes a blank screen in expanded memory, and
then exits (see Figure 2). The second program (SWPSCR.EXE) saves its
current screen, displays the blank screen and the first program's current
screen, and then restores its own current screen.

Before it can use expanded memory, SAVSCR.EXE must first see if the EMM is
present (by getting the device name from the device driver header).
Because SAVSCR.EXE uses 4.0 functions, it also checks the EMM version
(Function 7), and exits if EMM is earlier than version 4.0 (see Figure 3).

SAVSCR.EXE next determines the number of unallocated pages in expanded
memory (Function 3) and exits if there are fewer pages than it needs.
Then the program allocates the expanded memory pages it needs and gets an
EMM handle (Function 4), assigning the unique name SHARED to the handle
(Function 20). This unique handle name will be used later by the second
application program to find the handle so it can use the same expanded
memory (see Figure 4).

Next, SAVSCR.EXE uses the Map/Unmap Multiple Handle function (Function 17)
to map the needed pages into the page frame. This is done with one
function call; earlier versions of the EMM require one call for each page
(see Figure 5).

Then the SAVSCR.EXE program copies the current screen to logical page 0
and physical page 0, using the Move Memory Region subfunction (Function
24). Although this particular move does not span any more than one
expanded memory page, the same function call can transfer up to 1Mb of
memory without mapping in any logical pages of expanded memory (see
Figure 6).

Finally, SAVSCR.EXE unmaps all of the pages using the Map/Unmap Multiple
Handle Pages function (Function 17) and then exits without returning the
handle to the EMM (see Figure 5).

Expanded memory is now protected. The second program, SWPSCR.EXE, can find
this same expanded memory by using the Search for Named Handle

First, SWPSCR.EXE verifies that the EMM is present and is version 4.0 or
greater, as the first program did. However, unlike the SAVSCR.EXE
program, SWPSCR.EXE does not need to allocate pages because it can use the
same handle named SHARED that SAVSCR.EXE received.

SWPSCR.EXE uses the Search for Named Handle subfunction (Function 21) to
determine if the handle named SHARED is present. If the first program
(SAVSCR.EXE), has not been executed already, SWPSCR.EXE will exit,
because it could not find the handle named SHARED.

Finally, SWPSCR.EXE uses the Exchange Memory Region subfunction (Function
24) to: swap its current screen with the logical page that has the blank
screen; swap the blank screen with the logical page that has the
original screen; and then restore its own current screen. (Because the
Exchange Memory Region subfunction handles all mapping, none of the
logical pages have to be mapped before this call.)

SWPSCR.EXE unmaps all expanded memory pages be-fore exiting (Function 17)
as seen in Figure 7.

SWPSCR.EXE illustrates how EMM 4.0 has simplified executing code in
expanded memory. (For an idea of just how much EMM 4.0 has accomplished
in this area, refer to the KERNEL module in "Expanded Memory: Writing
Programs That Break the 640Kb Barrier," MSJ, March 1987.)

As in the first example, MAPCALL.EXE (see Figure 8) checks for the
version of EMM and allocates expanded memory. Then it uses the MS-DOS
Load Overlay function to load two modules (MODULE1.EXE and MODULE2.EXE)
into expanded memory (see Figure 9).

Next, MAPCALL.EXE uses the Alter Page Map and Call subfunction (Function
23) to execute MODULE1.EXE (see Figure 10).

MODULE1.EXE in turn uses the Search for Named Handle subfunction to
determine which handle to use and uses the Alter Map Page and Call
subfunction to execute MODULE2.EXE (see Figure 11).

MODULE2.EXE prints a message to the screen and does a far return. The far
return causes the EMM to restore the map-ping specified in the old map
page definition and return control to MODULE1.EXE (see Figure 12).

MODULE1.EXE then does a far return, which causes the EMM to return control
to MAPCALL.EXE and to restore the original mapping context. MAPCALL.EXE
then releases its handle and exits to MS-DOS.

To Get EMS 4.0

If you are interested in developing application programs that use
expanded memory, call Intel for a free copy of the Lotus/Intel/Microsoft
Expanded Memory Specification. From the United States and Canada, call
(800) 538-3373. Outside the United States and Canada, call (503) 629-7354.

Figure 1:  EMM 4.0 FUNCTIONS

Function Name              No.  Description

GET STATUS                 1    Returns a status code to tell you whether
Function Name              No.  Description
GET STATUS                 1    Returns a status code to tell you whether
                                the EMM is present and the hardware/software
                                is working correctly.

GET PAGE FRAME ADDRESS     2    Gives the program the location of the page

GET UNALLOCATED PAGE       3    Tells the program the totalCOUNT number of
                                pages in expanded memory and the number of
                                unallocated pages.

ALLOCATE PAGES             4    Allocates the number of expanded memory
                                pages requested by the program; assigns a
                                unique EMM handle to the set of pages

MAP HANDLE PAGE            5    Maps the specified logical page in expanded
                                memory to the specified physical page within
                                the page frame.

Function Name              No.  Description

DEALLOCATE PAGES           6    Deallocates the pages currently allocated to
                                an EMM handle.

GET EMM VERSION            7    Returns the version number of the EMM

SAVE PAGE MAP              8    Saves the contents of the page mapping
                                registers of all expanded memory boards.

RESTORE PAGE MAP           9    Restores the contents of the page mapping

                          10    Reserved.

                          11    Reserved.

GET EMM HANDLE COUNT      12    Returns the number of active EMM handles.

GET EMM HANDLE PAGES      13    Returns the number of pages allocated to a
Function Name              No.  Description
GET EMM HANDLE PAGES      13    Returns the number of pages allocated to a
                                specific EMM handle.

GET ALL EMM HANDLE        14    Returns the active EMM handles PAGES and the
                                number of pages allocated to each one.

GET/SET PAGE MAP          15    Saves and restores the mapping context of
                                the active EMM handle.

GET/SET PARTIAL PAGE MAP  16    Saves a partial mapping context for specific
                                mappable memory regions in a system.

MAP/UNMAP MULTIPLE        17    Maps/unmaps (in a single HANDLE PAGES
                                invocation) logical pages into as many
                                physical pages as the system supports.

REALLOCATE PAGES          18    Increases or decreases the amount of
                                expanded memory allocated to a handle.

GET/SET HANDLE            19    Lets an application determine and ATTRIBUTE
Function Name              No.  Description
GET/SET HANDLE            19    Lets an application determine and ATTRIBUTE
                                set a handle as volatile or non-volatile.

GET/SET HANDLE NAME       20    Gets the eight-character name currently
                                assigned to a handle assigns an eight-
                                character name to a handle.

GET HANDLE DIRECTORY      21    Returns information about active handles and
                                the names assigned to each.

ALTER PAGE MAP AND JUMP   22    Alters the memory mapping context and
                                transfers control to the specified address.

ALTER PAGE MAP AND CALL   23    Alters the specified mapping context and
                                transfers control to the specified address.
                                A return can then restore the context and
                                return control to the caller.

MOVE/EXCHANGE MEMORY      24    Copies or exchanges a region ofREGION memory
                                from conventional to conventional memory,
Function Name              No.  Description
                                from conventional to conventional memory,
                                conventional to expanded memory, expanded to
                                conventional memory, or expanded to expanded

GET MAPPABLE PHYSICAL     25    Returns an array with the segment ADDRESS
                                ARRAYaddress and physical page number for
                                each mappable physicalpage in a system.

GET EXPANDED MEMORY       26    Returns an array containing the HARDWARE
                                INFORMATION hardware capabilities of the
                                expanded memory system.

ALLOCATE RAW PAGES        27    Allocates the number of non-standard size
                                pages that the operating system requests and
                                assigns a unique EMM handle to these pages.

ALTERNATE MAP REGISTER    28    Lets an application program SET simulate
                                alternate sets of hardware mapping
Function Name              No.  Description

PREPARE EXPANDED          29    Prepares the expanded memory MEMORY HARDWARE
                                FOR hardware for an impending WARM BOOT warm

ENABLE/DISABLE OS/E       30    Enables and disables EMM Functions designed
                                for use by operating systems and
                                environments (Functions 26, 28, and 30).

Figure 2:  SAVSCR Main Program

#include <c:\msc\include\dos.h>
#include <c:\msc\include\memory.h>
#include <c:\msc\include\stdio.h>
#include <c:\emm\demo\emm.h>

/* set up size and base of video ram */
#define VIDEO_RAM_SIZE 4000
#define VIDEO_RAM_BASE 0XB8000000
union REGS    inregs,    outregs;
struct SREGS     segregs ;

char far *video_ram_ptr = {VIDEO_RAM_BASE); /* video start address (CGA)*/
unsigned long int video_ram_size =;         /* bytes in video ram */
unsigned int emm_handle ;                   /* our emm handle */
char emm_device_name[] = "EMMXXXX0";        /* Device Name of EMM */
char emm_handle_name[8] ="shared" ;         /* name for handle to be shared
char far *emm_ptr;                          /* pointer to page frame */
char far *(*page_ptr) ;                     /* pointer to page in the frame *
int pages_needed = 4;
struct log_phys {
  int log_page_number;
  int phys_page_number;
       } current_pages [4] ;
struct log_phys far *map_unmap_ptr ;
int  result ; /* result passed back from function calls */

main ()
   check_emm_version_number(); /*Check for EMM > 4.0 */

   result = get_expanded_memory(&emm_ptr, pages_needed, &emm_handle,
   if (result != 0) exit(1);  /* exit if error */

   result = map_unmap_multiple_pages (current_pages, /* Map in */
                                      emm_handle, 1); /* pages */

   move_exchg_to_expanded(MOVE_MEMORY_REGION, /* Copy video screen to */
   video_ram_ptr, emm_handle, 0, video_ram_size);  /* logical page 0 */

   /* make a null video screen at logical page 1      */
   page_ptr = (emm_ptr + 0x4000);        /* make null screen at */
   memset (page_ptr, 0, VIDEO_RAM_SIZE); /* at logical page 1   */

 /* Unmap all pages so they are protected  */
   result = map_unmap_multiple_pages (current_pages, emm_handle, 0);


Figure 3:  This function checks to see if the EMM is present and if the
           version number is >= 4.0. It uses EMM function 7, GET. VERSION


int check_emm_version_number()
   char *emm_device_name_ptr ;

   inregs.h.ah = 0x35; /* Use the DOS get interrupt function (0x35) to */
   inregs.h.al =EMM_INT;/* get the pointer at interrupt vector 0x67, */
   intdosx(&inregs, &outregs, &segregs);/* and check for device name. */
   emm_device_name_ptr = (segregs.es * 65536) + 10;
   if (memcmp(emm_device_name, emm_device_name_ptr,8) !=0)
       printf("Expanded memory manager not present\n");
   inregs.h.ah = GET_VERSION ;     /* set function code and check for  */
   int86(EMM_INT,&inregs,&outregs);/* version >= 4.0                   */
   if (outregs.h.ah != 0) exit(1);
   if ((outregs.h.ah == 0) & (outregs.h.al < 0x40))
     printf("Expanded memory manager does not support LIM 4.0");
     exit(1) ;

Figure 4:  This function gets the amount of expanded memory requested,
           returns a pointer to the page frame, and assigns the name to the

Obtaining the Requested Amount of Memory

int get_expanded_memory(emm_ptr_ptr, pages, emm_handle_ptr, name)

char *(*emm_ptr_ptr);         /* Pointer to expanded memory page frame */
int pages;                    /* Number of pages to allocate */
unsigned int *emm_handle_ptr; /* Pointer to emm handle */
char *name;

   inregs.h.ah = GET_UNALLOCATED_PAGE_COUNT ;  /* Check to see if there */
   int86(EMM_INT, &inregs, &outregs); /* enough unallocated pages left.*/
   if (outregs.h.ah != 0) return(1);
   if (outregs.x.bx < pages) return(2);

   inregs.h.ah = ALLOCATE_PAGES ;  /* Get a handle and allocate */
   inregs.x.bx = pages;            /* the requested pages.*/
   int86(EMM_INT, &inregs, &outregs);
   if (outregs.h.ah != 0) return(3);
   *emm_handle_ptr = outregs.x.dx ;

   inregs.h.ah = GET_FRAME_ADDRESS; /* Get page frame segment address */
   int86(EMM_INT, &inregs, &outregs); /* and make it a pointer. */
   if (outregs.h.ah != 0) return(4);
   *emm_ptr_ptr = (unsigned long int) (outregs.x.bx *65536);

   inregs.x.ax = SET_HANDLE_NAME ; /* assign name to handle */
   inregs.x.dx = *emm_handle_ptr ;
   inregs.x.si = FP_OFF(name) ;
   segregs.ds  = FP_SEG(name);
   int86x(EMM_INT, &inregs, &outregs, &segregs);
   if (outregs.h.ah != 0) return(5);



Figure 5:  Implementing the MAP/UNMAP MULTIPLE PAGES EMM Function

int map_unmap_multiple_pages (log_phys_pages,handle,map_unmap)

struct log_phys *log_phys_pages ;  /* Pointer to log_phys struct */
unsigned int handle;               /* Handle to map or unmap */
unsigned int map_unmap;            /* 0 = map, 1 = unmap */
 int i ;
 struct log_phys *temp_ptr;

 temp_ptr = log_phys_pages;

 for (i=0 ; i<=3; i++)
 /* Setup the structure to Map or unmap the logical pages 0 to 3 */
   log_phys_pages->phys_page_number = i;
   if (map_unmap == 1)
     log_phys_pages->log_page_number = i;
     log_phys_pages->log_page_number = 0xFFFF ;

   log_phys_pages++ ;


 inregs.x.dx = handle;
 inregs.x.cx = 4;
 inregs.x.si = FP_OFF(temp_ptr);
 segregs.ds  = FP_SEG(temp_ptr);
 if (outregs.h.ah != 0) return(1);

Figure 6:  This function implements the MOVE or EXCHANGE MEMORY  REGION
           function to move or exchange conventional memory with expanded
           memory pages.

Implementing the MOVE or EXCHANGE MEMORY REGION Function

int move_exchg_to_expanded(function_number, conv_buffer,  handle,
page, length)
unsigned int function_number ; /* Move or Exchange*/
char far *conv_buffer ;        /* conventional memory with */
int handle;                    /* EMM memory associated with this handle*/
int page ;                     /* at this physical page */
unsigned long int length;      /* and for this many bytes */

#pragma pack(1) /* Make sure the following structure
                is byte aligned */
struct move_exchg
   unsigned long int region_length;
   char source_type ;
   unsigned int source_handle ;
   unsigned int source_offset ;
   unsigned int source_seg_page;
   char dest_type;
   unsigned int dest_handle;
   unsigned int dest_offset;
   unsigned int dest_seg_page;
  } move_exchg_struct;
struct move_exchg *move_exchg_ptr;

  move_exchg_struct.region_length   = length ;
  move_exchg_struct.source_type     = 0;
  move_exchg_struct.source_handle   = 0;
  move_exchg_struct.source_offset   = FP_OFF(conv_buffer);
  move_exchg_struct.source_seg_page = FP_SEG(conv_buffer);
  move_exchg_struct.dest_type       = 1;
  move_exchg_struct.dest_handle     = handle;
  move_exchg_struct.dest_offset     = 0 ;
  move_exchg_struct.dest_seg_page   = page;

  inregs.x.ax = function_number;
  move_exchg_ptr = &move_exchg_struct;
  inregs.x.si = FP_OFF(move_exchg_ptr);
  segregs.ds  = FP_SEG(move_exchg_ptr);
  int86x(EMM_INT, &inregs, &outregs, &segregs);
  if (outregs.h.ah != 0) exit(1);

  return(outregs.x.ax) ;


Figure 7:  The Main Program for SWPSCR

#include <c:\msc\include\dos.h>
#include <c:\msc\include\memory.h>
#include <c:\msc\include\stdio.h>
#include <c:\emm\demo\emm.h>
#define VIDEO_RAM_SIZE 4000
#define VIDEO_RAM_BASE 0XB8000000

union REGS    inregs,    outregs;
struct SREGS     segregs ;

char far *video_ram_ptr = {VIDEO_RAM_BASE}; /* video start address (CGA)*/
unsigned long int video_ram_size ={4000};   /* bytes in video ram */
unsigned int emm_handle ;                   /* emm handle */
char emm_device_name[] = "EMMXXXX0";        /* Device Name of EMM */
char emm_handle_name[8] ="shared" ; /* name for handle to be shared */
char far *(*expanded_memory_ptr) ; /* pointer to page frame */
char far *(*page_ptr) ;           /* pointer to page in the frame  */
long target_time,current_time ;
struct log_phys {      /* structure to hold the mapping of logical */
  int log_page_number; /* pages to physical pages */
  int phys_page_number;
       } current_pages [4] ;

main ()


   search_for_handle(emm_handle_name, &emm_handle);

   /* Exchange screens*/
                          emm_handle, 1, video_ram_size);

   time(&current_time); /* Delay so user can see screen changes */
   target_time = current_time + 3;
   while (current_time < target_time) time(&current_time);

  /* Display Screen */
   move_exchg_to_expanded(EXCHANGE_MEMORY_REGION, video_ram_ptr,
                          emm_handle, 0, video_ram_size);
   time(&current_time); /* Delay so user can see screen change */
   target_time = current_time + 3;
   while (current_time < target_time) time(&current_time);

   /* Restore original screen */
                          emm_handle, 1, video_ram_size);

   result = map_unmap_multiple_pages (current_pages, emm_handle, 0);

   exit(0) ;

Figure 8:  The Main Program for MAPCALL

#include <c:\msc\include\dos.h>
#include <c:\msc\include\memory.h>
#include <c:\msc\include\stdio.h>
#include <c:\emm\demo\emm.h>

union REGS    inregs,    outregs;
struct SREGS     segregs ;

unsigned int emm_handle ;
char emm_device_name[] = "EMMXXXX0";                  /* Device Name of EMM *
char far emm_handle_name[8] ="mapcall";               /* Name for handle */
char far mod1_name[] ="c:\\emm\\demo\\module1.exe\0"; /* Name of modules */
char far mod2_name[] ="c:\\emm\\demo\\module2.exe\0"; /* to be loaded */
char far *emm_ptr ;
char far *(*page_ptr) ;
int pages_needed = 16 ;
struct log_phys {
  int log_page_number;
  int phys_page_number;
       } ;
struct log_phys far current_pages[4];
struct log_phys far map_call_pages ;
int  result;

main ()


   result = get_expanded_memory(&emm_ptr, pages_needed, &emm_handle,
   if (result != 0) exit(1);

           /* Map in pages */
   result = map_unmap_multiple_pages (current_pages,
                                      emm_handle, 1);

   *page_ptr = emm_ptr;  /* Load Module 1 into logical page 0  */
   load_overlay(mod1_name, page_ptr, 0); /* at physical page 0 */

           /*Load Module 2 into logical page 1 at physical page 1 */
   load_overlay(mod2_name, page_ptr, 1);

            /* Unmap all pages */
   result = map_unmap_multiple_pages (current_pages,
                                      emm_handle, 0);

          /* Map and call to module in page 0 and physical page 1 */
    map_call_pages.log_page_number  = 0;

    map_call_pages.phys_page_number = 0;

    map_and_call(&map_call_pages, 1, &current_pages, 0, emm_handle);

   inregs.h.ah = DEALLOCATE_PAGES ; /* Release handle before
                                       exiting */
   inregs.x.dx = emm_handle ;
   int86(EMM_INT, &inregs, &outregs);


Figure 9:  Implementing the ALTER PAGE MAP AND CALL EMM Function

int map_and_call(new_map_ptr, new_length, old_map_ptr, old_length,
struct log_phys *new_map_ptr;
char new_length;
struct log_phys *old_map_ptr;
char old_length;
unsigned int handle;
# pragma pack(1) /* Make sure structure is byte aligned */
  struct map_call_struct {
     unsigned int offset_target_address ;
     unsigned int seg_target_address ;
     char new_page_map_length ;
     unsigned int offset_new_page_map;
     unsigned int seg_new_page_map ;
     char old_page_map_length ;
     unsigned int offset_old_page_map;
     unsigned int seg_old_page_map ;
   } map_call;
   struct map_call_struct *map_call_ptr;

  map_call_ptr = &map_call ;
  map_call.offset_target_address = 0 ;
  map_call.seg_target_address    =0xd000;
  map_call.new_page_map_length   = new_length;
  map_call.offset_new_page_map   = FP_OFF(new_map_ptr);
  map_call.seg_new_page_map      = FP_SEG(new_map_ptr);
  map_call.old_page_map_length   = old_length;
  map_call.offset_old_page_map   = FP_OFF(old_map_ptr);
  map_call.seg_old_page_map      = FP_SEG(old_map_ptr);

  inregs.h.ah =0x56; /* Set up for Alter Page Map and Call EMM function */
  inregs.h.al = 0;
  inregs.x.dx = handle ;
  map_call_ptr = &map_call ;
  inregs.x.si = FP_OFF(map_call_ptr);
  segregs.ds = FP_SEG(map_call_ptr);
  if (outregs.h.ah != 0) return(1);

  return(0) ;

Figure 10:  Implementing the MS-DOS Load Function 4BH This function
            implements the MS-DOS load function 4BH. It sets AL to 3 causing
            the function to load the file and apply the relocation factor
            without executing the file.

int load_overlay(load_file_name,relocation_ptr,page)
char *load_file_name ;
char *(*relocation_ptr);
unsigned int page; /* physical page at which to load */

struct reloc {
  unsigned int load_seg; /* Which segment to load file */
  unsigned int reloc_factor; /* Which segment to use for
                                relocation */
  } reloc_struct;
  struct reloc *reloc_struct_ptr;

  reloc_struct.load_seg = FP_SEG(*relocation_ptr) + (page * 0x400);
  reloc_struct.reloc_factor = FP_SEG(*relocation_ptr) + (page * 0x400);

  inregs.h.ah = 0x4B ; /* Dos Exec function code */
  inregs.h.al = 3;  /* load but do not execute */
  inregs.x.dx  = FP_OFF(load_file_name);
  segregs.ds   = FP_SEG(load_file_name);

  reloc_struct_ptr = &reloc_struct ;
  inregs.x.bx  = FP_OFF(reloc_struct_ptr);
  segregs.es  = FP_SEG(reloc_struct_ptr);

  intdosx(&inregs, &outregs, &segregs);


Figure 11:  NAME Start

; Use the DOSSEG directive to ensure that the code segment is the
; first segment in the module. Since this piece of code will be
; loaded in at the page frame at D000:0000H the Alter Page Map and
; Call will use D000:0000H as the entry point.


Data  ends

start proc far
     push    ds
     PUSH    dx
     mov     DX,data ; set up data seg into ds
     mov     ds,dx
     pop     dx
     mov     ah, 09  ; set function code for DOS display string
     mov     dx, offset enter_msg
     int    21H
     mov    si, offset handle_name
     mov    ax, 5401H              ; set function to search for named
                                   ; handle
     int    67H                    ; and invoke EMM
     or ah, ah
     jnz exit

     mov     si, offset map_call ; Set up registers for Alter Page
                                 ; Map and Call
     mov     al, 0               ; indicate that values are pages
     mov     ah, 56H             ; set function to map and call
     int     67H                 ; invoke emm
     or ah, ah
     jnz exit

     mov     ah, 09                ; set function for DOS display
                                   ; string
     mov     dx, offset exit_msg
     int 21H
     pop     ds
start endp
cr    equ  0Dh
lf    equ  0AH
log_phys_map_struct   STRUC
   log_page_number    DW ?
   phys_page_number   DW ?
log_phys_map_struct   ENDS

map_call_struct      STRUC
    target_address      DD ?   ;  Pointer to which EMM will transfer
                               ;  control
    new_page_map_length DB ?   ;  Number of new pages to be mapped on
                               ;  call.
    new_page_map_ptr    DD ?   ;  Pointer to array of
                               ;  log_phys_map_struc.
    old_page_map_length DB ?   ;  Number of pages to mapped on
                               ;  return.
    old_page_map_ptr    DD ?   ;  pointer to array of
                               ;  log_phys_map_struc
    reserved            DW 4 DUP (?)
map_call_struct ENDS
new_map  log_phys_map_struct <1,1>   ;  mapping before call
old_map  log_phys_map_struct <0,0>   ;  mapping after call

map_call  map_call_struct <0D0004000H,1,new_map,1,old_map>

handle_name db 'mapcall',0           ; handle name is ascciz string
enter_msg   db 'Entering Module 1',cr,lf,'$'
exit_msg    db 'Exiting Module 1',cr,lf,'$'
Data  ends
end  start

Figure 12:  NAME Start  (MODULE2.EXE)


Data  ends

start proc far
     push    ds
     PUSH    dx
     mov     DX,data ; Set up date segment
     mov     ds,dx
     pop     dx
     mov     ah, 09 ; Set function code for DOS display string.
     mov     dx, offset enter_msg
     int 21H
     mov     ah, 09 ; Set function code for DOS display string.
     mov     dx, offset exit_msg
     int 21H
     pop     ds
start endp

cr    equ  0Dh
lf    equ  0AH
enter_msg   db 'Entering Module 2',cr,lf,'$'
exit_msg    db 'Exiting Module 2',cr,lf,'$'
Data  ends
end  start


Vol. 3 No. 2  Table of Contents

Microsoft(R) Windows Adapts to the Unique Needs of the Japanese Market

A Japanese version of Microsoft Windows offers support for double-byte kanji
characters and has the ability to convert kana characters into kanji. It
also enables software vendors to build graphical applications that run on
many disparate Japanese hardware systems despite their incompatibility.

Utilizing OS/2 Multithread Techniques in Presentation Manager Applications

While OS/2 provides the preemptive multitasking that Windows lacks, the
Presentation Manager's message-based architecture can cause problems when
doing lengthy processing. OS/2 threads offer alternative solutions to the
problems that occur when programming in Presentation Manager.

OS/2 LAN Manager Provides a Platform for Server-Based Network Applications

The OS/2 LAN Manager API, with its named pipe and remote procedure call
facilities, offers a rich set of functions for building network intrinsic
applications. These applications can exploit the potential of distributed
processing from an OS/2 or DOS workstation.

Writing OS/2 Bimodal Device Drivers: An Examination of the DevHlp API

Device drivers are the necessary linkage between the operating system and
peripheral devices that enable you to use special hardware with standard
applications software. Our study of OS/2 continues with an in-depth look at
the unique capabilities of OS/2 device drivers.

Exploring the Structure and Contents of the MS-DOS(R) Object Module

A programmer does not usually need to think about the contents of the
intermediate files prepared for the linker when using commercial compilers.
This excerpt from The MS-DOS(R) Encyclopedia examines the object module
format and provides insights into the operation of LINK and LIB.

A Guide to Program Editors, the Developer's Most Important Tool

Text editors for programmers have improved greatly, prompting MSJ to conduct
an evaluation of twelve editors. The three editors that we thought were
best──BRIEF(R), ME, and VEDIT PLUS──are reviewed in depth, and a features
chart gives you a good idea of what all twelve can do.


This issue of MSJ covers the broadest range of topics yet. In addition to
our continuing coverage of the OS/2 systems with articles on the OS/2 LAN
Manager, bimodal device drivers, and the Presentation Manager, we have an
article on developing software for the Japanese market, a tutorial on the
MS-DOS(R) object file format, and a comprehensive look at available
program editors.

As time goes by, we pass yet another milestone in the progress of OS/2.
With the shipment of Version 1.0 of the standard edition, OS/2 has become
a users' as well as a developers' product, and many developers have begun
to focus on the advanced managers. The Presentation Manager is no longer
just a concept or a specification; now that it is in developers' hands, it
is a real development environment. In this issue, Ray Duncan explains how
to write a device driver for OS/2, and Charles Petzold examines the use of
OS/2 threads within Presentation Manager

OS/2 has the potential not only to improve the applications we associate
with MS-DOS, but to open the way for entirely new classes of applications.
Among the most exciting of these is distributed processing. PC networking
to date has in most cases meant little more than file and printer sharing.
We present an overview of the possibilities for the development of
server-based distributed processing applications with the OS/2 LAN Manager.

Recently in talking with subscribers, we were reminded just how important,
and how personal, the choice of a program editor is to the professional
programmer. We heard many different opinions, all of which were
uncharacteristically strong. Given the importance of the subject, we
thought it worthwhile to depart from our normal subject material and
commissioned a comprehensive review of the available editors. The results
were surprising. Please drop us a line if you think we should review
other categories of programmer tools.

We want this to be your Journal. Please write and tell us how we can make
it better for you.──Ed.


Editor and Publisher


Technical Editor

Associate Editor

Production Editor

Staff Editor

Editorial Assistant


Art Director

Associate Art Director


Circulation Manager

Assistant to the Publisher

Administrative Assistant

Copyright(C) 1988 Microsoft Corporation. All rights reserved; reproduction
in part or in whole without permission is prohibited.

Microsoft Systems Journal is a publication of Microsoft Corporation, 16011
NE 36th Way, Box 97017, Redmond, WA 98073-9717. Officers: William H.
Gates, III, Chairman of the Board and Chief Executive Officer; Jon Shirley,
President and Chief Operating Officer; Francis J. Gaudette, Treasurer;
William Neukom, Secretary.

Microsoft Corporation assumes no liability for any damages resulting from
the use of the information contained herein.

Microsoft, XENIX, CodeView, and MS-DOS are registered trademarks of
Microsoft Corporation. IBM and PC/AT are registered trademarks of
International Business Machines Corporation. Personal System/2 and PS/2 are
trademarks of International Business Machines Corporation. BRIEF is a
registered trademark of UnderWare, Inc. CP/M is a registered trademark of
Digital Research, Inc. dBASE and dBASE III PLUS are registered trademarks of
Ashton-Tate Corporation. Hercules is a registered trademark of Hercules
Computer Technology. Intel is a registered trademark of Intel Corporation.
Macintosh is a registered trademark of Apple Computer, Inc. NEC is a
registered trademark of NEC Corporation. Paradox is a registered trademark
of Ansa Software, a Borland Company. 3+Open is a trademark of 3Com
Corporation. Solution Systems is a registered trademark of Solution Systems.
UNIX is a registered trademark of American Telephone and Telegraph Company.
WordStar is a registered trademark of MicroPro International Corporation.
WYSE is a trademark of WYSE Technology.


Bringing Windows to the Expanding Japanese Market

by Tom Sato and Lin F. Shaw

Machine independence is a cornerstone of the Microsoft(R) Windows
philosophy. While this particular issue is largely ignored in the United
States-due to the fact that the PC environment is dominated by IBM(R) PC
AT(R) machines and compatibles──in Japan, Windows-based software is truly
compatible across incompatible hardware architectures.

Microsoft Windows has given the Japanese community of independent
software vendors (ISVs) the opportunity to develop applications with
only one set of source codes for a wide range of machines, thus
eliminating the laborious adaptation of software to different machines.
For Western ISVs there is an added advantage. Windows applications
written for IBM PCs and compatibles run on all the various Japanese
computers. Windows-based applications, therefore, present a tremendous
export opportunity for Western ISVs, since Japan is a country where real
growth in the software industry has yet to come (see Figures 1 and 2☼).

Microsoft Windows Version 1.03 was first released by NEC in December 1986
for use on its PC-9801 series of personal computers, and in its first six
months sold over 25,000 copies. Matsushita has also joined the original
family of Windows equipment manufacturers (OEMs) by releasing their own
adaptations of Windows for their new machines at the September 1987 Tokyo
DATA show (see Figures 3 and 4☼).

The Japanese Market

In 1982, NEC launched the PC-9801 series. Fourteen compatible variations
later, with its millionth unit shipped last March, the PC-9801 series
dominates the Japanese PC market, with a market share estimated at over
60 percent. The rest of the market is split among various companies, all
of which have released incompatible MS-DOS(R) machines.

In spite of these numbers, however, the fact is that the penetration rate
of personal computers in the Japanese office is nowhere near that of the
United States, as many Japanese businessmen have yet to become accustomed
to PCs. The primary reason is that the Japanese language has a great
many character sets and kanjis (Chinese characters). This has prevented
the spread of mechanical typewriters──they have more than 4000 characters
and are very clumsy to use──and, therefore, computers. Amazingly, most
business correspondence in Japan is still done by hand.

Fortunately, there is a sign that this is all going to change. Several
years ago, some of the electronics giants started to sell electronic
Japanese typewriters. For the most part, these are 8-bit computers with
an LCD screen, a thermal transfer printer and built-in syllabic kana to
kanji conversion software. These machines provide more capability,
portability, efficiency, and also better prices than the previous
generation of mechanical typewriters. This is the latest booming
business in Japan, with an installed base of more than 5 million

Nevertheless, these electronic typewriters still have limitations──they
are not as flexible as word-processing software on 16-bit PCs; storage
is limited and the editing screen is really too small for general office
use. However, the popularity of the Japanese electronic typewriters
has led to significant increases in the sale of substantially more
powerful 16-bit PCs.

The standard configuration for a typical Japanese PC is an 80286 running
at 10 MHz, a 640x480 pixel screen, 640Kb of RAM, 256Kb of ROM for kanji
fonts, and dual 1.2Mb floppy disk drives. Machines with an internal 20Mb
hard disk drive are also becoming more popular, and there are now
machines with higher screen resolution and more detailed kanji fonts
available. However, the speed of Japanese computers is slower than that of
their counterparts in the United States. This is largely due to a great
deal of overhead, for instance searching for kanji bitmaps and having to
deal with larger screen and kana-kanji interfaces.

Japanese Language

Modern Japanese is written in a mixture of kanji characters and two types
of kana signs. There are 46 basic kana signs; 45 basic syllables arranged
as in Figure 5☼, consisting of five vowels in the order a, i, u, e, o and
then five vowels preceeded in turn by nine consonants in the order k, s
(or alternatively sh), t (or ts), n, h (f), m, y, r, w, and the final
consonant for kana, n.

Two small strokes on the upper right hand corner of a character are
added as voiced marks or dakuten. They apply to kana beginning with k, s,
t, and h and represent 20 more syllables beginning with voiced
consonants g, z, d, and b respectively. A small round circle on the
upper right hand corner is a semi voicing sound mark (han-dakuten). It
applies to the kana beginning with h and represents five further
syllables begining with voiced consonants p. Further, nine of the kanas
are sometimes written in small characters to form dipthongs (see
Figure 6☼).

There are two types of kana signs: kata-kana and hira-gana There is a
one-to-one correspondence between the kata-kana and hira-gana; for
example, for each syllable there is both a hira-gana letter and kata-kana
letter. Hira-gana is most commonly used with a mixture of kanji, and kata-
kana is always used for foreign words. Kata-kana is a simplified form of
hira-gana, written for the most part in simple strokes, whereas hira-ganas
are written with curves. On an 8x8 bitmap, hira-gana is too intricate to
describe. Therefore, both the Japanese keyboard and the Japanese 1-byte
character code for alphanumeric characters contain only the kata-kana
(see Figure 7☼).

Kanji Characters

Kanji characters are used frequently to represent one or more kana
characters. There are many homonyms in Japanese and the use of kanji
clarifies the meaning of the written word. Nouns and verbs are typically
kanjis or a mixture of hira-ganas and kanjis. A typical kanji dictionary
lists over 15,000 kanji characters. However, the Japanese elementary
schools only teach 1878 of them and in practice about 3500 kanjis have
normal daily use.

The same kanji character may have different pronunciations depending on
its context or the other kanji with which it forms a word. For example, a
kanji character for the word "leaf" has three pronunciations: ha, ba, and
yo-u. Conversely, the same pronunciation is used for many kanji; for
example, as many as twenty different kanjis can be pronounced ai.

Text Layout

Japanese is traditionally written vertically in columns running from top
to bottom and from right to left across the page. Most books and
newspapers are written this way. However, it can also be written
horizontally as English is. Business correspondence, in fact, is usually
written horizontally. Most Japanese software, therefore, displays
horizontally and only supports vertical columns when printing a document
or on a screen in sophisticated desktop publishing software.

Japanese text is written without spaces between words. The use of a
mixture of kanji and kana in a regular sentence provides implicit breaks
to a sentence and the Japanese would have a very difficult time reading
text of only kana characters. It would be analogous to reading English
text without spaces between words.

Japanese text can wrap from one line to another at any point, since there
are no spaces between words. This includes two successive kanji which
make up one logical "word." There is no need to justify Japanese text, as
all Japanese characters are the same size.

Shift-JIS Code

Japanese Industrial Standards (JIS) define two levels of kanjis for
computers. The JIS C 6226 standard defines the image and the code of the
3500 most commonly used kanji characters as level 1 kanjis. Level 2
defines the optional additional 2500 kanjis which are more obscure and are
used for the names of people and places. Most 16-bit computers in Japan
now support level 2, making the number of kanjis usually available to
Windows more than 6000. They are stored in bank-switched ROMs as 16x16
pixel images.

Kanjis, as well as hira-ganas, kata-kanas, alphanumeric characters, Greek
and Russian characters, and other signs supported in ASCII character
sets, are coded in the JIS coding scheme. JIS was designed like ASCII to
be a transmission standard as well as a storage standard. When
transmitting data over communications lines, reliability is extremely
important. That is why parity exists for 7-bit ASCII.

The JIS standard was also encoded in 7 bits to use parity. In order to be
able to distinguish between 7-bit ASCII and 7-bit JIS, the ASCII shift-in
(SI) and shift-out (SO) controls were used. A kanji character is composed
of two successive 7-bit patterns, limiting us to 16,384 values. In order
to avoid the 32 control characters, space, and rub-out, there are only
94x94 (8836) available and defined characters.

As the use of parity in personal computers became less important, and
since many U.S. applications did not understand SI and SO, Microsoft
defined a new kanji encoding scheme for MS-DOS; the Microsoft Kanji
Encoding Scheme, which uses two 8-bit values. It is commonly known as the
Shift-JIS Code, because the character codes of JIS have been shifted.

Figures 7☼ and 8 describe how this works. Of all the codes available
between 00H and FFH, 00H to 7FH are ASCII characters and control codes,
and A0H to DFH are kata-kana characters. This leaves 64 positions for
the first byte of the double-byte value. The second byte could be between
00H and FFH. Since 64x256 is 16,384, there is more than enough room for
all the JIS characters.

Since JIS has a 94x94 matrix, Microsoft decided to map it into a 47x188
matrix in the set of 64x256 possibilities. This allowed 68 values to be
avoided for the second byte. The control characters between 00H-1FH and
7FH-FFH were not used in order to avoid conflict with U.S. applications,
nor were FDH and FEH used, because CP/M(R) 86 was using them when the
scheme was introduced. Also, 20H-3FH were avoided in order to make
parsing of characters such as ";", "(,)", and ":" easier for U.S.
language compilers.

Figure 8 shows how the Microsoft Kanji Encoding Scheme is shifted from
the JIS coding scheme without actually changing the original JIS
ordering. Just remember that a code between 81H-9FH and E0H-FCH means
that the following code is the second byte of the double-byte character
and together they give the code of a kanji character.

Kana-Kanji Conversion

With all these hira-gana, kata-kana and kanji characters, making a
computer to support the Japanese language becomes complex. On the MS-DOS
level, to input Japanese, a kana-kanji conversion program is installed
as a device driver. There are various kana-kanji conversion programs on
the market and most of them are porting their software to run under
Windows. Usually kana-kanji conversion units are packaged free of charge
with applications software. Companies lacking a kana-kanji conversion
program can buy a license from kana-kanji software vendors.

These conversion programs are typically activated by pressing a "hot
key." In the case of the NEC(R) PC-9801 series, for example, the hot-key
combination is CTRL-XFER and the bottom of the screen becomes the
conversion window.

There are various methods of input but all have kata-kana input and
romanji (Roman character) input. Although the keyboard supports all the
kata-kana characters, many Japanese are familiar with the standard
English QWERTY typewriter keyboard and prefer to input using Roman
characters. For example, the word "kanji" is typed K A N J I and as you
type it in, the conversion program accepts the Roman characters and
displays the correct hira-ganas. When the <XFER> key is pressed, the
program searches through its dictionary to find the kanji characters for
that word.

This process is complicated for various reasons, including the Japanese
homonyms mentioned earlier. Secondly, the inflectional endings of
Japanese verbs increase the number of word roots so much that the
dictionary may have to contain well over 100,000 word roots. Obviously,
this would require an enormous amount of memory.

The problem is solved by software that draws on a grammar of Japanese
inflection to analyze the phonetic spelling typed in by the user. The
software displays the most appropriate and most commonly used kanji
combinations. Good software will give the correct kanji approximately 80
percent of the time. If the software gives the wrong kanjis, the user can
hit the <XFER> key again and the next kanji choice will appear. To see
all the different ways of spelling the word, you press <SHIFT>-<XFER>.
Some conversion packages can convert multiple phrases, even a whole
sentence. Figure 9☼ shows the kana-kanji conversion unit as it would
appear in a Windows session.

Kanji Classification

Microsoft Windows 1.03 applications can be classified according to their
abilities to process kanji input. Please note that this specification is
only intended for Windows 1.03. There will be additions in the Windows
2.0 specification.

Level 1: Original U.S. Applications. These applications are not able to
treat double-byte kanji text. Most of the first releases of U.S.
applications are level 1 applications.

Level 2: Modified U.S. Applications. These applications can process
double-byte kanji characters as a single entity. They accept and display
shift-JIS kanji inputs, advance the cursor and handle backspaces with
kanji text, sort kanji text, and do right justification on kanji text.
However, these applications do not handle in-line kana-kanji conversion,
and the conversion process is transparent to these applications. The
conversion is done by a separate conversion module called KKAPP which is
activated and deactivated using special keys. In-line conversion means
that the kana and kanji input actually takes place in the applications
window and not in the pop-up windows at the bottom of the screen. KKAPP
defines the user interface for kanji conversion for all level 2
applications. Most kanji applications are currently at this level.

Level 3: Applications with Full Kanji Support. These applications can
handle kana-kanji conversion. They call the KKAPP using the level 3
application function ConvertRequest. They allow the user to change a
kanji selection from a list of kanji candidates having the same
pronunciation, input kanji strings without using an explicit convert
command (free input), do full sentence or full paragraph conversion, and
select kanji candidates from a menu list using the mouse.

Conversion Under Windows 1.03

Under MS-DOS, the conversion module traps the keyboard input, converts it
to the appropriate 2-byte characters and then transfers those
characters to MS-DOS. Under Windows there are six modules that are
involved in kana-kanji conversion: the keyboard driver, USER, FEP,
KKAPP, KKLIB, and the application. Figure 10 describes each of the

Keyboard Driver

Kana-kanji conversion starts with the user typing in a key. This keyboard
input is processed by the Windows keyboard driver and sent to USER.

The USER library module turns keyboard input into a Windows message
(either WM_KEYUP or WM_KEYDOWN) and sends the message to the appropriate
module. If WH_KEYBOARD has been set using SetWindowsHook, then USER sends
the keyboard messages to the keyboard filter; otherwise, USER sends the
keyboard message to the current application.

The KKLIB library module sits between USER and KKAPP and controls the
flow of keyboard input. Upon request from KKAPP, KKLIB will install the
keyboard filter function to receive all the keyboard input. In the filter
function, KKLIB decides whether to send the keyboard message to KKAPP or
to return the keyboard message to USER without processing it (as when the
kana-kanji conversion mode is turned off).

KKLIB also provides an entry point in level 3 applications for kana-kanji
conversion requests or inquiries. This function in KKLIB
(ConvertRequest()) in turn translates these requests into Windows
messages (WM_ CONVERTREQUEST) and sends the messages to KKAPP.

The KKAPP application module does the kana-kanji conversion. Depending
on which kind of application is using KKAPP (level 2 or level 3), KKAPP
will take the appropriate actions. If a level 2 application is the
active window and the conversion mode is on, KKAPP will open up its
conversion window to display the conversion process, and send the
final result to the application through WM_CHAR messages. If a level 3
application is active, KKAPP does not display the conversion process
and simply answers convert requests from the application. One KKAPP is
required for each FEP.

The front end processor module does the actual conversion. This module
has the dictionary and the conversion algorithm and KKAPP interfaces
directly to it. It is installed as an MS-DOS device driver.

A level 2 application is not involved in kana-kanji conversion. It simply
receives the converted results in WM_CHAR messages from KKAPP. A level 3
application does participate in kana-kanji conversion by calling
ConvertRequest() and receives the results back in the KanjiStruct

Double-Byte Kanji

Writing a Windows application for the Japanese market involves a number
of things. First, all messages and resources in the resource file need to
be translated into Japanese. The double-byte support code of the
resource compiler is enabled when the keyword KANJI is seen in the
resource file. This keyword must be followed by four numbers stating the
two ranges of valid first byte ranges of kanji. Figure 11☼ provides an
example of the translated resource file for the clipboard desktop

Windows virtualizes 2-byte support by providing string handling routines.
Applications should not make any assumption about the system they are
running on. Using the function AnsiUpper to convert to an upper character
or the function AnsiLower to convert to a lower character enables the
same application to run correctly both in the U.S. and in other
countries. Windows further provides the AnsiNext call to scan forward and
AnsiPre to scan backwards. It is important to use these calls so that an
application does not get out of sync and misinterpret consecutive
kanjis as different kanjis.

Another problem is that the second byte of the shift-JIS character set
overlaps with Roman characters starting at 40H. Applications should be
careful not to mistreat the second byte of a kanji as a Roman character,
particularly when implementing the searching function or when parsing
path names looking for the '\' file separator.

In screen editing, applications that use Windows Edit Control will run
without modification in Japan. Applications that do not use Edit
Control should buffer two WM_CHAR messages to make one single TextOut()
call when 2-byte kanjis are encountered. When the application is
advancing, backspacing, positioning the cursor, and line-wrapping on
the screen, it must treat 2-byte kanjis as one entity.

Lastly, sorting that involves kanji is slightly more complicated. The
shift-JIS character set places its 1-byte kanas in the A0H-CFH range,
and the kanjis in the two ranges 80H-9FH and E0H-FCH. In Japan, 1-byte
kanas are sorted after 1-byte Ro-man characters in the 20H-7EH range, but
before all the 2-byte characters. This necessitates swapping kana
characters with the kanjis which are in the 80H-9FH range when doing a

In Conclusion

Microsoft is currently working on an additional specification for the
kana-kanji interface for Windows 2.0. In the meantime, ISVs should be
able to see that a major opportunity exists to export Windows-based
applications to the Japanese market. Hopefully, the information
provided here will offer some insight into the work required to pursue
it. ISVs interested in developing Japanese Windows applications are
urged to contact the OEM Technical Support Group, Microsoft KK, Sanban cho
Yayoi kan, 6-2 Sanban cho, Chiyoda ku, Tokyo, JAPAN, for more detailed
information. We look forward to hearing from you.

Figure 8:  Microsoft Kanji Encoding Scheme

00 •

10 •         21                    7E
             ▼                     ▼
20 •     21►┌───────────────────────┐
            │                       │
30 •        │                       │
            │      JIS level 1      │
40 •        │                       │
            │                       │
50 •        ├───────────────────────┤
            │                       │
60 •        │      JIS level 2      │
            │                       │
70 •        │                       │
80 •              81►┌──────────────┐┌───────┬──────────────────────┐
                     │     Microsoft translation of JIS level 1     │
90 •                 │              ││       │                      │
A0 •              9F►└─────────────┘└───────┴─────────────────────┘
                       Microsoft translation of JIS level 2, part 1
B0 •                                                           
                     │              │ │     │ │                     │
C0 •                40             7E 80   9E 9F                   FC
                     │              │ │     │ │                     │
D0 •                 │              │ │     │ │                     │
                     ▼              ▼ ▼     ▼ ▼                     ▼
E0 •              E0►┌──────────────┐┌───────┬──────────────────────┐
                  EF►│ Microsoft translation of JIS level 2, part 2 │
F0 •              F0►├──────────────┤├───────┼──────────────────────┤
                  FC►Extra room for extensions to the JIS kanji standard
    •    •   •   •   •   •   •   •   •   •   •   •   •   •   •   •
    00  10  20  30  40  50  60  70  80  90  A0  B0  C0  D0  E0  F0

Figure 10:  The kana-kanji conversion process under Windows Version 1.03.

                          │       Keyboard Drivers      │
     ░░ Level 1           │                             │
     ▒▒ Level 2             ▒  ▓                      ░
     ▓▓ Level 3           │           USER.EXE          │
                          │               ▓▓▓▓▓▓▓      │
                          Keyboard        ▓ ▒  ▒ ▓    ░
                          hook set       Conversion   ░
                            ▒  ▓          mode off    ░
                      ┌─────▼──▼────┐     ▓ ▒  ▒ ▓    No
                      │    KKLIB    ▓▓▓▓▓▓▓ ▒  ▒ ▓ keyboard
                      │             ▒▒▒▒▒▒▒▒▒  ▒ ▓   hook
                      └─────▒──▓────┘          ▒ ▓    ░
                            ▒  ▓               ▒ ▓    ░
  ┌─────────────┐     ┌─────▼──▼────┐          ▒ ▓    ░
  │     FEP     │◄───►│    KKAPP    │          ▒ ▓    ░
  │             │     │             │          ▒ ▓    ░
  └─────────────┘     └─────▒──▓───┘          ▒ ▓    ░
                            ▒  ▓   ▓           ▒ ▓    ░
                      Conversion   Virtual     ▒ ▓    ░
                          result   kanji       ▒ ▓    ░
                            ▒  ▓   key         ▒ ▓    ░
                            ▒  ▓   interface   ▒ ▓    ░
                            ▒  ▓   ▓           ▒ ▓    ░
                            Conversion request and    ░
                            ▒  ▓ results back  ▒ ▓    ░
                            ▒  ▓   ▓           ▒ ▓    ░
                         ║          Applications          ║
                         ║                                ║


Utilizing OS/2 Multithread Techniques in Presentation Manager Applications

Charles Petzold☼

Microsoft(R) Windows is a multitasking operating environment──but not really.
As most Windows programmers know, Windows is actually a "nonpreemptive
multitasking" environment. It does not perform the preemptive time-
slicing we normally associate with a multitasking system. Instead, Windows
multitasks programs based on the presence of "messages" (which often
represent keyboard and mouse input) in the programs' message queues.

When a Windows program calls the GetMessage function to retrieve the next
message from its message queue, and the message queue is empty, Windows
suspends the program. Windows then switches to another program with a
nonempty message queue. This causes that other program to return from its
own GetMessage call to process the message. Only one Windows program is
running at any time. The rest are suspended in the GetMessage function.

Windows programmers are well aware of the problems associated with this
form of nonpreemptive multitasking. If a Windows program requires a long
period of time to process a message, then other programs running under
Windows are effectively halted for the duration. Windows programmers must
use some special techniques (which we'll look at in this article) when
doing lengthy processing to prevent the program from suspending the rest of
the system.

Microsoft OS/2 Presentation Manager is a windowing environment with a
message-based architecture much like that of Microsoft Windows. But unlike
Windows, the OS/2 Presentation Manager runs under a true priority-based
preemptive multitasking operating system.

At first, the preemptive multitasking of the OS/2 systems would seemingly
eliminate the problems associated with the nonpreemptive nature of
Windows. You might conclude that Presentation Manager programs can spend
as much time as they need processing a message without worrying about
suspending other programs. But this is not so. The problems that arise
when doing lengthy processing in Windows result more from the message-based
architecture rather than from nonpreemptive multitasking. In that
sense, the Presentation Manager is the same as Windows.

The real difference is that the Presentation Manager provides more and
better solutions to the problem of lengthy processing. That's what we'll
explore here.

The "Big Job" Problem

Presentation Manager programs can usually process most keyboard and mouse
input very quickly. In a word processing program, for example, a
character typed from the keyboard need only be inserted into the document
and displayed on the screen. But many programs must also carry out commands
that require more lengthy processing. My term for this lengthy
processing is the "big job."

In a Presentation Manager spreadsheet program, the big job is a
recalculation or the execution of a long macro. In a database program, the
big job is a file sort or indexing. In a word processing program, it's a
pagination or spelling check. In a CAD program, it's redrawing the
screen. In a communications program, it's reading the serial port when an
incoming character is not immediately available. In most any
Presentation Manager program, printing is a big job.

A big job is anything that takes more than 1/10 second. This is based on
the recommendation in the Presentation Manager documentation that
programs take no more than 1/10 second to process a message. Although
this is a guideline rather than a hard-and-fast rule, I'll refer to it here
as the "1/10 second rule."

To explore the big job problem, let's write a Presentation Manager
program that does a big job-specifically, a calculation called the
"savage" benchmark that is often used to test floating-point speed. The
program will allow repeating the savage calculation 10, 100, 1000, or 10,000
times depending on a menu selection.

I'll use the "alternate" floating-point library in this program so that the
calculation isn't affected by the presence of an 80287 math coprocessor.
The program will have menu options to start the calculation running and-if
possible-to abort it before it's finished. The 10,000 repetitions of the
savage benchmark calculation takes about 3 minutes on an 8-MHz IBM(R) PC/AT(R
Even 10 repetitions violate the 1/10 second rule.

Five different versions of this program will be examined, with each
successive version more sophisticated in its approach to multitasking under
the Presentation Manager.

The Naive Approach

The simplest approach to a big job is shown in the BIGJOB1 program in
Figures 1 and 2. Most of the code in BIGJOB1's client window procedure,
which is called ClientWndProc, handles WM_COMMAND messages from the
program's menu. When you select an option from the Repetitions menu, BIGJOB
unchecks the currently selected option and checks the option you choose.
When you select Start from the menu, BIGJOB1 disables the Start option,
enables the Abort option, and begins the calculation. After it's finished
with the big job, the program reenables the Start option and then exits the
window procedure.

Because BIGJOB1 does the entire calculation in response to a WM_COMMAND
message from the menu, it obviously violates the 1/10 second rule. BIGJOB1
is a bad program. I show it only to illustrate how such programs clog up the
rest of the Presentation Manager.

While BIGJOB1 is doing the big job, you cannot switch to another program
running under the Presentation Manager. The system seemingly ignores all
keyboard and mouse input until the calculation is finished. Although the
Abort option is present on BIGJOB1's menu, you can't use the keyboard or
mouse to select that option. Once you begin the big job, you have to wait
until it's finished to be able to do anything else.

At first, this seems disturbing. Isn't the Presentation Manager supposed to
be multitasking? And if so, why does one program apparently cause the whole
system to grind to a halt? What is happening here is a predictable result of
the message-based architecture of the Presentation Manager.

Using Messages

Programs that run under the Presentation Manager are message driven. In the
normal case, a program spends most of its time suspended in WinGetMsg (the
Presentation Manager equivalent of the Windows GetMessage function)
waiting for a message.

Each window created by a program has a window procedure that processes
messages to the window. At least one of these window procedures──the
window procedure for the client window──is located in the Presentation
Manager program. The other window procedures (such as those for the frame
window, the title bar window, and the menu window) can be found in the
Presentation Manager PMWIN.DLL dynamic-link library.

Some of the messages for a window are stored in the program's message
queue. These messages are called "queued" messages and are sometimes said to
be "posted" to the queue. The queued messages are retrieved from the message
queue when a program calls WinGetMsg and are dispatched to the window
procedure by WinDispatchMsg. Other messages are sent directly to the window
procedure, bypassing the program's message queue.

Regardless of whether a message is posted to a message queue or sent
directly to a window procedure, and regardless of whether a window
procedure is contained in the program or in a dynamic-link library,
messages to windows created by a thread of execution are always processed
in that thread. A particular thread of execution can only do one thing at
a time. A thread cannot be multitasked with itself. This seems obvious, yet
the message-based architecture of the Presentation Manager sometimes
obscures this simple fact.

When you select Start from BIGJOB1's menu, the program begins the big job.
You then try to use the Alt-Tab key combination to switch to another
program. The window that must process these keystrokes is BIGJOB1's frame
window. But the window procedure for the frame window must run in the same
thread as the client window, and the client window is busy doing the big
job. This means that the Alt-Tab keyboard message cannot be processed
until the calculation is finished, and until BIGJOB1 exits ClientWndProc and
then calls WinGetMsg to retrieve the message from the queue. This explains
why the Presentation Manager seems to be ignoring keyboard input while
BIGJOB1 is calculating.

Serialization of Input

There may, however, be a way around this. As you know, you can also use the
mouse to make another window active. Maybe that will work.

To test this possibility, you must first position the mouse pointer on top
of another program's window and then use the keyboard to select Start from
BIGJOB1's menu. While it is calculating, you press the mouse button and...
nothing happens.

This again is initially disturbing. Because the Presentation Manager is a
true multitasking system, the other program ought to be able to read that
mouse click even while BIGJOB1 is calculating. The Presentation Manager
should also be able to change the active window from BIGJOB1 to the other

This does not happen for a couple of reasons. First, the Presentation
Manager serializes all keyboard and mouse input in a system message queue.
Keyboard and mouse input is passed one message at a time to an
application's message queue based on which window has the input focus (in
the case of keyboard messages) and which window is underneath the mouse

The serialization of mouse and keyboard input in a system message queue is
required to correctly handle "type-ahead" and "mouse-ahead" input from the
user. One of the keystrokes or mouse clicks in the system message queue
could have the effect of changing the active window and the focus window.
Subsequent keyboard input must then go to that new window. This won't work
if mouse and keyboard input is not passed to applications in the same
order that it enters the system message queue. Therefore, a keyboard or
mouse message cannot be passed to a particular application until all
previous keyboard and mouse messages have been processed.

In this particular case, another application cannot read a mouse message
until BIGJOB1 processes all its keyboard input. The keyboard input that it
hasn't processed is the release of the key that caused the menu to send the
WM_COMMAND message that started the calculation.

Thus, because BIGJOB1 renders itself resistent to keyboard or mouse input,
it also prevents all other programs running under the Presentation Manager
from getting keyboard or mouse input.

Even if another program could read a mouse click, the Presentation Manager
cannot change the input focus from BIGJOB1 to another program while BIGJOB1
is busy doing the calculation. In order to change the input focus, the
Presentation Manager must send a WM_SETFOCUS message to the window losing
the input focus. That WM_SETFOCUS message is blocked because the window that
must receive the message is part of BIGJOB1's thread, and BIGJOB1 is busy
doing the big job.

Messages are not like hardware interrupts. Although a window procedure can
be sent a message as a result of calling WinDefWindowProc, and a window
procedure can be sent a message as a result of calling some other
Presentation Manager functions, these are examples of recursion in window
procedures. Messages do not preemptively interrupt a thread and start
execution someplace else in the same thread.

Now that we've seen how BIGJOB1 effectively disables keyboard and mouse
input in the Presentation Manager, the reason for the 1/10 second rule
should be obvious. Presentation Manager programs must continually interact
with the system, retrieving and processing their messages promptly.

Not Exactly Like Windows

As bad as BIGJOB1 is, the Presentation Manager can still multitask programs
while BIGJOB1 is running. This is a difference between the Presentation
Manager and Windows.

If you have a Windows version of BIGJOB1 running under Windows along with
the Windows CLOCK program, CLOCK will stop dead while BIGJOB1 is
calculating. Under Windows, only one program can be running at any time.

However, when you run BIGJOB1 in the Presentation Manager along with a
Presentation Manager version of CLOCK (like the one included in my book,
Programming the OS/2 Presentation Manager, Microsoft Press, 1988) you'll
find that CLOCK continues to run while BIGJOB1 is calculating. CLOCK and
BIGJOB1 run concurrently.

CLOCK works by setting a timer and then processing a WM_TIMER message every
second. A WM_TIMER message does not have to be serialized with the
keyboard and mouse input. CLOCK can continue to receive these messages
even if BIGJOB1 has clogged up keyboard and mouse input.

Using a Timer

Once you acknowledge that BIGJOB1 is a very bad Presentation Manager
program, you are stuck with the problem of structuring it so that it works
correctly. The use of a timer in CLOCK suggests one possible remedy: you can
divide a big job up into little pieces, set the Presentation Manager
timer, and do each little piece on receipt of a WM_TIMER message. This is a
solution that works both in the Presentation Manager and in Windows.

The BIGJOB2 program uses a timer to do the big job. The files BIGJOB2,
BIGJOB2.C, and BIGJOB2.DEF are shown in Figure 3. Compiling BIGJOB2 also
requires the BIGJOB.H and BIGJOB.RC files shown in Figure 1.

When you select the Start option from BIGJOB2's menu, BIGJOB2 calls
WinStartTimer to start the timer, disables the Start option, enables the
Abort option, and initializes a few variables. The savage calculation is
done once for each WM_TIMER message. Thus, for 1000 repetitions, the big
job is finished after 1000 WM_TIMER messages.

WM_TIMER messages are low-priority queued messages. If the message queue
contains keyboard or mouse messages, these messages will be retrieved from
the queue and processed before a WM_TIMER message. Thus, BIGJOB2 continues
to read keyboard and mouse input and can allow the user to select Abort from
BIGJOB2's menu, move or resize BIGJOB2's window, or to move to another
program. The entire system-including BIGJOB2-continues to function normally
all the time that BIGJOB2 is doing the calculation.

Timer Problems

The timer approach is not too bad for BIGJOB2, but it's easy to imagine
cases where the timer would be inadequate.

A program using the timer for a big job must continually reenter and leave
the processing loop with every WM_TIMER message. This is easy to structure
when a single loop is involved, but it becomes a nightmare for more complex
jobs with lots of nested loops.

The timer also slows down the big job. It is not possible to receive
WM_TIMER messages at a rate faster than that of the hardware clock. Under
OS/2, this means the program receives a WM_TIMER message only once every
31.25 milliseconds. But BIGJOB2 spends less than 20 milliseconds (on an 8-
MHz IBM PC/AT) processing each WM_TIMER message. Because the calculation is
paced by the timer, the calculation won't finish any faster on a 20-MHz
80386 machine.

Clearly, as a general solution to the big job program, the timer approach
must be rejected.

The Peek Message

Another solution that is very common in Windows is the use of the
PeekMessage function (called WinPeekMsg in the Presentation Manager). The
PeekMessage function is very similar to GetMessage. Under Windows, the
PeekMessage function first checks if there are messages waiting in the
program's message queue. If so, PeekMessage fills the message structure
with the next message from the queue and returns a non-zero value.

If the message queue of the program calling PeekMessage is empty, then
Windows switches to a program with a nonempty message queue to allow that
program to process its own messages. (This is often called "yielding" to
other programs.) When no messages remain in any program's message queues,
then PeekMessage returns control to the original program and returns a zero

PeekMessage allows all Windows programs to process outstanding messages
but then returns control to the program calling PeekMessage after all this
message processing is completed. Using PeekMessage allows a program to do a
big job while periodically allowing itself and other programs to process
their messages. Several Windows programs, including SPOOLER and TERMINAL,
use PeekMessage in this way in order to simulate multitasking. Programs
that print frequently use PeekMessage to allow a user to cancel a print job
from a dialog box.

The WinPeekMsg function in the Presentation Manager is very similar to
PeekMessage. However, in the Presentation Manager, WinPeekMsg is used by a
program to process its own messages rather than to yield to other programs.
Multitasking occurs normally in the Presentation Manager if a program does
not clog up the processing of keyboard and mouse input. The WinPeekMsg
function provides a good solution to the big job problem. BIGJOB3 in
Figure 4 shows how this is done.

Like BIGJOB2, BIGJOB3 does the full calculation in response to a WM_COMMAND
message. However, within the calculation loop, BIGJOB3 calls WinPeekMsg to
determine if any messages are in its message queue. If so, it removes them
with WinGetMsg and dispatches them to a window procedure with
WinDispatchMsg, just as in the normal message loop in main.

One of these messages could be another WM_COMMAND message generated when the
user selects Abort from the menu. BIGJOB3 uses the bContinueCalc variable to
indicate when the calculation has been aborted.

You'll notice that some special processing is required for the WM_QUIT
message. This message is posted to the message queue by the Presentation
Manager as a default response when the user selects Close from the system
menu. The WM_QUIT message should not be removed from the queue within the
window procedure. Instead, BIGJOB3 exits the window procedure so that the
WM_QUIT message can be retrieved from the message queue in the main

Although message peeking usually works well in Windows and Presentation
Manager programs, it's always a little messy in practice. The PeekMessage
and WinPeekMsg functions must be called frequently enough to give the
system a good response time, yet there's an inordinate amount of code
involved. If the big job must be aborted, it's sometimes difficult to get
out of a calculation loop in a structured manner. In short, code involving
WinPeekMsg often looks like a kludge.

A Second Thread

The solutions in BIGJOB2 and BIGJOB3 are applicable in both Windows and the
Presentation Manager. Now let's do something that is impossible in Windows
but a natural in the Presentation Manager: create a second thread of
execution to do the big job.

When a program contains multiple threads of execution, the multiple threads
run concurrently. All threads in a process share the program's
resources──such as open files, memory, semaphores, and queues──but each
thread has its own CPU state, dispatching priority, and stack.

Within a program, a second thread of execution looks like a function. All
local automatic variables in the thread function are private to the thread
because they are stored on the thread's stack. Local static variables in the
thread function can be shared by all threads based on that same function.

When programming for the Presentation Manager, threads fall into two
categories: a thread is either a "message queue thread" or a "nonmessage
queue thread." A thread becomes a message queue thread when it calls
WinCreateMsgQueue. It reverts back to being a nonmessage queue thread when
it calls WinDestroyMsgQueue.

A Presentation Manager program always creates a message queue in at least
one thread. A thread must create a message queue before it can create
windows. The message queue is used to store messages for all of the windows
created in the thread. However, other threads in a Presentation Manager
program do not need to create message queues if they do not create

Thread Restrictions

In a Presentation Manager program, nonmessage queue threads have some
advantages over message queue threads──and some disadvantages.

The good news is that a nonmessage queue thread is not bound by the 1/10
second rule. Because the thread does not receive or process messages, it
needn't worry about clogging up the processing of messages in message queue
threads. Thus, a nonmessage queue thread is often ideal for doing big jobs.

The bad news is that nonmessage queue threads are restricted in the
Presentation Manager functions they may call. Basically, a thread without a
message queue cannot create windows, cannot send a message to a window
procedure in a message queue thread, and cannot call any function that
causes a message to be sent to a window procedure.

Some of these restrictions are obvious: a nonmessage queue thread cannot
create a window because it has no queue to store messages to that window.
However, a nonmessage queue thread can call some functions that affect
windows created in message queue threads. For example, a nonmessage queue
thread can obtain a presentation space handle for a window created in a
message queue thread and can paint something on the surface of that window.

Nonmessage queue threads cannot send messages to message queue threads. The
WinSendMsg function is not allowed. Nor can they call functions that send
messages. For example, WinDestroyWindow cannot be called from a nonmessage
queue thread because it sends a window procedure a WM_DESTROY message. The
functions that are forbidden to nonmessage queue threads are listed in the
Presentation Manager documentation.

Although a nonmessage queue thread cannot send a message using WinSendMsg,
the thread can post a message by calling WinPostMsg. This latter function
places the message in a thread's message queue and returns immediately.


Threads within a single process must often communicate with each other in
various ways. The execution of threads must be coordinated so that the
threads don't step on each other's toes. This requires some handshaking.
Often, threads must also signal each other and pass data among themselves.

A message queue thread generally communicates with a nonmessage queue
thread by using semaphores. A nonmessage queue thread communicates with
a message queue thread by using posted messages. Both threads can also
access common global variables.

The BIGJOB4 program in Figure 5, which uses a nonmessage queue thread to do
a big job, shows this signaling and communication.

The global variable lSemTrigger is a RAM semaphore. This semaphore is set
during the WM_CREATE message in ClientWndProc. The window procedure then
creates a second thread by calling the OS/2 DosCreateThread function using
SecondThread as the thread function.

SecondThread waits for lSemTrigger to be cleared. ClientWndProc clears the
semaphore when the user selects Start from BIGJOB4's menu. SecondThread then
does the calculation. When it's finished, it posts a WM_CALC_DONE message to
the window procedure. (This is a "user-defined" message defined within
BIGJOB4.C.) The elapsed time of the calculation is passed to the window
procedure in the lParam1 variable that accompanies this message.

Aborting the Calculation

BIGJOB4 also allows the user to abort the calculation. When Abort is
selected from the menu, the window procedure sets the bContinueCalc global
variable to FALSE and disables the Abort menu option.

SecondThread checks this bContinueCalc variable during the calculation and
breaks from the loop when the variable is no longer non-zero. The thread
then sets the semaphore and posts a WM_CALC_ABORTED to the window. On
receipt of this message, the client window enables the Start menu option
again. The semaphore is already set, so SecondThread can't proceed with a
new calculation until Start is chosen again. This is an example of some
simple handshaking between the two threads.

Note that the semaphore is used only for blocking and unblocking the
nonmessage queue thread. A message queue thread should not be made to wait
on a semaphore because that may violate the 1/10 second rule. If absolutely
necessary, a nonmessage queue thread could suspend a message queue thread
for very short periods of time by calling DosSuspendThread or
DosEnterCritSec. This is sometimes helpful when both threads access global
variables. (It's not necessary in BIGJOB4 when the threads access
bContinueCalc because this variable is altered or accessed in one machine
code instruction.)

The message queue thread in the BIGJOB4 program calls DosSuspendThread to
suspend SecondThread after exiting the message loop in main before
destroying the window and message queue. SecondThread could be in the middle
of a calculation at this time. Suspending the thread prevents it from
posting a message to the message queue after the message queue is

Thinking Threads

A nonmessage queue thread is nearly essential in Presentation Manager
programs that must read input other than the keyboard and the mouse. An
obvious example of this is a communications program. Such a program would
have a client window in the message queue thread that processes keyboard
messages, writes the characters to the communications port using DosWrite,
and (if local echo is in effect) also writes the characters to the surface
of the window.

The nonmessage queue thread reads the communications port with the DosRead
function. Used most efficiently, this function does not return unless a
character has been read from the serial port. A message queue thread should
not call DosRead to get input from the serial port because it may violate
the 1/10 second rule. When the nonmessage queue thread reads a character, it
can post a user-defined message to the window. The client window processes
the message by displaying the character to the window.

A Presentation Manager program using queues for interprocess communication
should also create a nonmessage queue thread for reading the queue. The
nonmessage queue thread calls the DosReadQueue function with the "no wait"
flag set to 0, thus blocking the thread until there is something in the

In BIGJOB4, the second thread is created once and unblocked whenever it has
to do the big job. If a program has numerous big jobs handled in lots of
different thread functions, it would be best not to create all these
threads initially, but to create a thread each time it is needed. When a
thread is finished with its big job, it can post a message to the client
window and terminate using the DosExit call with a first parameter of 0.

Object Windows

BIGJOB4 shows how a program can handle big jobs in a nonmessage queue
thread. A Presentation Manager program can also do big jobs in a second
message queue thread that creates "object windows."

Object windows are created in a message queue thread just like normal
windows. They have a window procedure that processes messages to the object
window, again just like normal windows. However, object windows do not
appear on the screen and do not get keyboard and mouse input. In fact,
object windows basically receive only two messages: WM_CREATE and
WM_DESTROY. The WM_CREATE message is sent during the WinCreateWindow call
that creates the object window, and the WM_DESTROY message is sent during
the WinDestroyWindow call that destroys the window.

(In the pre-release version of the Presentation Manager I have, object
windows also receive a WM_ADJUSTWINDOWPOS message concurrently with the
WinCreateWindow call.)

You normally use object windows specifically to receive messages from other
windows, perhaps to implement an object-oriented programming language. But
object windows also help out in processing big jobs. If a thread creates
only object windows, then there are only a very limited number of messages
the window procedure can receive. This greatly simplifies the processing
of these messages in the object window's window procedure.

There are several advantages to using object windows for big jobs. The
object window is not restricted to a subset of Presentation Manager
functions. You can thus use messages for all communications between the
client window and the object window. If you've come to regard message-driven
architecture as inherently superior to traditional top-down architecture,
you may prefer this. The use of object windows in separate threads is more
in tune with the architecture of the Presentation Manager than are simple
nonmessage queue threads.

The use of an object window in a second message queue thread is shown in the
BIGJOB5 program in Figure 6. The BIGJOB5.C file defines six messages that
the client window and object window use for communicating with each other.

BIGJOB5 generally creates the second thread after creating the program's
main window. BIGJOB5's SecondThread function looks a lot like the main
function. Although it does not call WinInitialize, it registers a window
class and creates a window. The WinCreateWindow call to create an object
window is very simple: the parent window parameter is set to HWND_OBJECT,
the second parameter is set to the name of the window class, and all other
parameters are set to 0 or NULL.

SecondThread then sends a WM_OBJECT_CREATED message to ClientWndProc to
notify it that the object window has been created and is ready for work.
SecondThread then enters a normal message loop.

When you select Start from BIGJOB5's menu, the client window posts the
message WM_START_CALC to the object window. Similarly, selecting Abort
from BIGJOB5's menu causes the client window to post a WM_ABORT_CALC
message. Note as well that ClientWndProc processes the normal WM_CLOSE
message (generated by selecting Close from the system menu) by posting a
WM_QUIT message to the object window.

ObjectWndProc begins the big job in response to the WM_START_CALC message.
While doing the calculation, it can be interrupted only by one of two
possible messages posted to its message queue. These messages are
WM_ABORT_CALC and WM_QUIT. In either case, the calculation can be
unconditionally aborted.

In the calculation loop, ObjectWndProc calls the WinQueryQueueStatus
function and checks the QS_POSTMSG bit of the return value. (It should be
necessary only to check that the return value is non-zero, but in the
version of the Presentation Manager I used, this function was a little
buggy.) This check of WinQueryQueueStatus is almost as simple as checking
bContinueCalc in BIGJOB4, but not nearly as complex as the code involved
with using WinPeekMsg in BIGJOB3.

ObjectWndProc notifies ClientWndProc that it has finished or aborted a
calculation by posting the messages WM_CALC_DONE and WM_CALC_ABORTED
respectively. ClientWndProc uses these messages to enable its menu for
further calculations.

Ending the program in an orderly manner (that is, where both main and
SecondThread destroy their own windows and message queues) is a little
tricky. ClientWndProc processes a WM_CLOSE message by posting a WM_QUIT
message to the object window. This causes ObjectWndProc to abort the big job
if it's doing one at the time. When this WM_QUIT message is retrieved
from SecondThread's message queue, WinGetMsg returns 0. SecondThread
destroys its window, its message queue, and posts a WM_OBJECT_DESTROYED
message to ClientWndProc. On receipt of this message, ClientWndProc posts
itself a WM_QUIT message and terminates normally.

The use of an object window for big jobs is certainly more complex than the
use of a nonmessage queue thread, so you may not like it. Moreover, the
object window approach is not good for some big jobs. For example, when a
second thread must use DosRead to get input from a communications port or
DosReadQueue to get input from a queue, the thread is blocked within the
OS/2 function and obviously cannot check the message queue status using

No More "Please Wait"

We started out looking at BIGJOB1, a program that did the job it was meant
to do, but did it in a way that was not advantageous for the user. Our
immediate rejection of this program and our search for better ways of doing
big jobs indicates some major changes in our conception of proper behavior
in application programs.

In a traditional single-tasking nonwindowed environment, of course you have
to wait while the database program is sorting a file. When you start a file
sort, it's time to take a coffee break.

In a traditional multitasking operating environment, you may be able to let
the database program sort in the background, and you can work on another
program while waiting for the sort to finish.

In a multitasking window environment like the Presentation Manager,
however, we are satisfied only when the user can continue to interact with a
program even when it's doing a big job. Obviously, the complexities
involved with structuring a program in this way require some extra work on
the part of the programmer. But that makes the program better to use.

Just as we can no longer tolerate programs that require the user to memorize
scores of commands, we can no longer tolerate programs that throw up a
"Please Wait" message and require a user to wait until the program finishes
its big job.

Figure 1:

BIGJOB.RC resource script file

#include <os2.h>
#include "bigjob.h"

     SUBMENU "~Repetitions",  IDM_REPS
          MENUITEM "     10", IDM_10,    MIA_CHECKED
          MENUITEM "    100", IDM_100
          MENUITEM "   1000", IDM_1000
          MENUITEM "  10000", IDM_10000
     SUBMENU "~Action",       IDM_ACTION
          MENUITEM "~Start",  IDM_START

BIGJOB.H header file

#define ID_RESOURCE 1

#define IDM_REPS    1
#define IDM_ACTION  2

#define IDM_10      10
#define IDM_100     100
#define IDM_1000    1000
#define IDM_10000   10000

#define IDM_START   20
#define IDM_ABORT   21

     /* Definitions, functions, and variables for BIGJOBx.C */

#ifndef RC_INVOKED       /* This stuff not needed for .RC file */

#define STATUS_READY     0
#define STATUS_WORKING   1
#define STATUS_DONE      2


HAB  hab ;

double Savage (double A)
     return tan (atan (exp (log (sqrt (A * A))))) + 1.0 ;

VOID CheckMenuItem (HWND hwnd, SHORT iMenuItem, BOOL bCheck)
     HWND hwndParent = WinQueryWindow (hwnd, QW_PARENT, FALSE) ;
     HWND hwndMenu   = WinWindowFromID (hwndParent, FID_MENU) ;

     WinSendMsg (hwndMenu, MM_SETITEMATTR, MAKEULONG (iMenuItem, TRUE),
                 MAKEULONG (MIA_CHECKED, bCheck ? MIA_CHECKED : 0)) ;

VOID EnableMenuItem (HWND hwnd, SHORT iMenuItem, BOOL bEnable)
     HWND hwndParent = WinQueryWindow (hwnd, QW_PARENT, FALSE) ;
     HWND hwndMenu   = WinWindowFromID (hwndParent, FID_MENU) ;

     WinSendMsg (hwndMenu, MM_SETITEMATTR, MAKEULONG (iMenuItem, TRUE),
                 MAKEULONG (MIA_DISABLED, bEnable ? 0 : MIA_DISABLED)) ;

VOID PaintWindow (HWND hwnd, SHORT iStatus, SHORT iRep, LONG lTime)
     static CHAR *szMessage [3] = { "Ready", "Working ...",
                                    "%d repetitions in %ld msec." } ;
     CHAR        szBuffer [60] ;
     HPS         hps ;
     WRECT       wrc ;

     hps = WinBeginPaint (hwnd, NULL, NULL) ;
     GpiErase (hps) ;

     WinQueryWindowRect (hwnd, &wrc) ;

     sprintf (szBuffer, szMessage [iStatus], iRep, lTime) ;

     WinDrawText (hps, -1, szBuffer, &wrc, DT_CENTER | DT_VCENTER) ;
     WinEndPaint (hps) ;


Figure 2:

BIGJOB1 Make File

bigjob1.obj : bigjob1.c bigjob.h
     cl -c -FPa -G2sw -W3 -Zp bigjob1.c

bigjob.res : bigjob.rc bigjob.h
     rc -r bigjob.rc

bigjob1.exe : bigjob1.obj bigjob1.def bigjob.res
     link bigjob1, /align:16, /map, /nod slibc slibcp slibfa os2,
     bigjob1 rc bigjob.res bigjob1.exe

BIGJOB1.DEF Module Definition File

NAME           BIGJOB1
DESCRIPTION    'BIGJOB Demo Program No. 1(C) Charles Petzold, 1988'
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        ClientWndProc

BIGJOB1.C - Naive Approach to Lengthy Processing Job

#define INCL_WIN

#include <os2.h>
#include <math.h>
#include <stdio.h>
#include "bigjob.h"

INT main (VOID)
     static CHAR szClassName [] = "BigJob1" ;
     HMQ         hmq ;
     HWND        hwndFrame, hwndClient ;
     QMSG        qmsg ;

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

     WinRegisterClass (hab, szClassName, ClientWndProc,
                            CS_SYNCPAINT | CS_SIZEREDRAW, 0, NULL) ;

     hwndFrame = WinCreateStdWindow (HWND_DESKTOP,
                               | FS_SYSMENU    | FS_MINMAX
                               | FS_MENU,
                    szClassName, "BIGJOB Demo No. 1",
                    0L, NULL, ID_RESOURCE, &hwndClient) ;

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

     WinDestroyWindow (hwndFrame) ;
     WinDestroyMsgQueue (hmq) ;
     WinTerminate (hab) ;

     return 0 ;

ULONG EXPENTRY ClientWndProc (HWND hwnd, USHORT msg, ULONG mp1,
                              ULONG mp2)
     static SHORT iCalcRep, iCurrentRep = IDM_10 ;
     static SHORT iStatus = STATUS_READY ;
     static ULONG lElapsedTime ;
     double       A ;
     SHORT        i ;

     switch (msg)
          case WM_COMMAND:

               switch (LOUSHORT (mp1))
                    case IDM_10:
                    case IDM_100:
                    case IDM_1000:
                    case IDM_10000:
                         CheckMenuItem (hwnd, iCurrentRep, FALSE) ;
                         iCurrentRep = LOUSHORT (mp1) ;
                         CheckMenuItem (hwnd, iCurrentRep, TRUE) ;
                         break ;

                    case IDM_START:
                         EnableMenuItem (hwnd, IDM_START, FALSE) ;
                         EnableMenuItem (hwnd, IDM_ABORT, TRUE) ;

                         iStatus = STATUS_WORKING ;
                         WinInvalidateRect (hwnd, NULL) ;

                         iCalcRep = iCurrentRep ;
                         lElapsedTime = WinGetCurrentTime (hab) ;

                         for (A = 1.0, i = 0 ; i < iCalcRep ; i++)
                              A = Savage (A) ;

                         lElapsedTime = WinGetCurrentTime (hab) -
                                        lElapsedTime ;

                         iStatus = STATUS_DONE ;
                         WinInvalidateRect (hwnd, NULL) ;

                         EnableMenuItem (hwnd, IDM_START, TRUE) ;
                         EnableMenuItem (hwnd, IDM_ABORT, FALSE) ;
                         break ;

                    case IDM_ABORT:

                              /* Not much we can do here */

                         break ;

                         break ;
               break ;

          case WM_PAINT:
               PaintWindow (hwnd, iStatus, iCalcRep, lElapsedTime) ;
               break ;

               return WinDefWindowProc (hwnd, msg, mp1, mp2) ;
     return 0L ;

Figure 3:

BIGJOB2 Make File

bigjob2.obj : bigjob2.c bigjob.h
     cl -c -FPa -G2sw -W3 -Zp bigjob2.c

bigjob.res : bigjob.rc bigjob.h
     rc -r bigjob.rc

bigjob2.exe : bigjob2.obj bigjob2.def bigjob.res
     link bigjob2, /align:16, /map, /nod slibc slibcp slibfa os2,
     bigjob2 rc bigjob.res bigjob2.exe

BIGJOB2.DEF Module Definition File

NAME           BIGJOB2
DESCRIPTION    'BIGJOB Demo Program No. 2(C) Charles Petzold, 1988'
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        ClientWndProc

BIGJOB2.C - Timer Approach to Lengthy Processing Job

#define INCL_WIN

#include <os2.h>
#include <math.h>
#include <stdio.h>
#include "bigjob.h"

#define ID_TIMER 1

INT main (VOID)
     static CHAR szClassName [] = "BigJob2" ;
     HMQ         hmq ;
     HWND        hwndFrame, hwndClient ;
     QMSG        qmsg ;

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

     WinRegisterClass (hab, szClassName, ClientWndProc,
                            CS_SYNCPAINT | CS_SIZEREDRAW, 0, NULL) ;

     hwndFrame = WinCreateStdWindow (HWND_DESKTOP,
                               | FS_SYSMENU    | FS_MINMAX
                               | FS_MENU,
                    szClassName, "BigJob Demo No. 2",
                    0L, NULL, ID_RESOURCE, &hwndClient) ;

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

     WinDestroyWindow (hwndFrame) ;
     WinDestroyMsgQueue (hmq) ;
     WinTerminate (hab) ;

     return 0 ;

ULONG EXPENTRY ClientWndProc (HWND hwnd, USHORT msg, ULONG mpl,
                              ULONG mp2)
     static double A ;
     static SHORT  i, iCalcRep, iCurrentRep = IDM_10 ;
     static SHORT  iStatus = STATUS_READY ;
     static ULONG  lElapsedTime;

     switch (msg)
          case WM_COMMAND:

               switch (LOUSHORT (mp1))
                    case IDM_10:
                    case IDM_100:
                    case IDM_1000:
                    case IDM_10000:
                         CheckMenuItem (hwnd, iCurrentRep, FALSE) ;
                         iCurrentRep = LOUSHORT (mp1) ;
                         CheckMenuItem (hwnd, iCurrentRep, TRUE) ;
                         break ;

                    case IDM_START:

                         if (!WinStartTimer (hab, hwnd, ID_TIMER, 1))
                              WinAlarm (HWND_DESKTOP, WA_ERROR) ;
                              break ;

                         EnableMenuItem (hwnd, IDM_START, FALSE) ;
                         EnableMenuItem (hwnd, IDM_ABORT, TRUE) ;

                         iStatus = STATUS_WORKING ;
                         WinInvalidateRect (hwnd, NULL) ;

                         iCalcRep = iCurrentRep ;
                         lElapsedTime = WinGetCurrentTime (hab) ;

                         A = 1.0 ;
                         i = 0 ;

                         break ;

                    case IDM_ABORT:
                         WinStopTimer (hab, hwnd, ID_TIMER) ;

                         iStatus = STATUS_READY ;
                         WinInvalidateRect (hwnd, NULL) ;

                         EnableMenuItem (hwnd, IDM_START, TRUE) ;
                         EnableMenuItem (hwnd, IDM_ABORT, FALSE) ;
                         break ;

                         break ;
               break ;

          case WM_TIMER:

               A = Savage (A) ;

               if (++i == iCalcRep)
                    lElapsedTime = WinGetCurrentTime (hab) -
                                        lElapsedTime ;

                    WinStopTimer (hab, hwnd, ID_TIMER) ;

                    iStatus = STATUS_DONE ;
                    WinInvalidateRect (hwnd, NULL) ;

                    EnableMenuItem (hwnd, IDM_START, TRUE) ;
                    EnableMenuItem (hwnd, IDM_ABORT, FALSE) ;
               break ;

          case WM_PAINT:
               PaintWindow (hwnd, iStatus, iCalcRep, lElapsedTime) ;
               break ;

               return WinDefWindowProc (hwnd, msg, mp1, mp2) ;
     return 0L ;

Figure 4:

BIGJOB3 Make File

bigjob3.obj : bigjob3.c bigjob.h
     cl -c -FPa -G2sw -W3 -Zp bigjob3.c

bigjob.res : bigjob.rc bigjob.h
     rc -r bigjob.rc

bigjob3.exe : bigjob3.obj bigjob3.def bigjob.res
     link bigjob3, /align:16, /map, /nod slibc slibcp slibfa os2,
     bigjob3 rc bigjob.res bigjob3.exe

BIGJOB3.DEF Module Definition File

NAME           BIGJOB3
DESCRIPTION    'BigJob Demo Program No. 3(C) Charles Petzold, 1988'
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        ClientWndProc

BIGJOB3.C - PeekMessage Approach to Lengthy Processing Job

#define INCL_WIN

#include <os2.h>
#include <math.h>
#include <stdio.h>
#include "bigjob.h"

INT main (VOID)
     static CHAR szClassName [] = "BigJob3" ;
     HMQ         hmq ;
     HWND        hwndFrame, hwndClient ;
     QMSG        qmsg ;

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

     WinRegisterClass (hab, szClassName, ClientWndProc,
                            CS_SIZEREDRAW, 0, NULL) ;

     hwndFrame = WinCreateStdWindow (HWND_DESKTOP,
                               | FS_SYSMENU    | FS_MINMAX
                               | FS_MENU,
                    szClassName, "BigJob Demo No. 3",
                    0L, NULL, ID_RESOURCE, &hwndClient) ;

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

     WinDestroyWindow (hwndFrame) ;
     WinDestroyMsgQueue (hmq) ;
     WinTerminate (hab) ;

     return 0 ;

ULONG EXPENTRY ClientWndProc (HWND hwnd, USHORT msg,
                              ULONG mp1,ULONG mp2)
     static BOOL   bContinueCalc = FALSE ;
     static SHORT  iStatus = STATUS_READY ;
     static SHORT  iCalcRep, iCurrentRep = IDM_10 ;
     static ULONG  lElapsedTime ;
     double        A ;
     SHORT         i ;
     QMSG          qmsg ;

     switch (msg)
          case WM_COMMAND:

               switch (LOUSHORT (mp1))
                    case IDM_10:
                    case IDM_100:
                    case IDM_1000:
                    case IDM_10000:
                         CheckMenuItem (hwnd, iCurrentRep, FALSE) ;
                         iCurrentRep = LOUSHORT (mp1) ;
                         CheckMenuItem (hwnd, iCurrentRep, TRUE) ;
                         break ;

                    case IDM_START:
                         EnableMenuItem (hwnd, IDM_START, FALSE) ;
                         EnableMenuItem (hwnd, IDM_ABORT, TRUE) ;

                         iStatus = STATUS_WORKING ;
                         WinInvalidateRect (hwnd, NULL) ;

                         iCalcRep = iCurrentRep ;
                         bContinueCalc = TRUE ;
                         lElapsedTime = WinGetCurrentTime (hab) ;

                         qmsg.msg = WM_NULL ;

                         for (A = 1.0, i = 0 ; i < iCalcRep ; i++)
                              A = Savage (A) ;

                              while (WinPeekMsg (hab, &qmsg, NULL, 0, 0,
                                   if (qmsg.msg == WM_QUIT)
                                        break ;

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

                                   if (!bContinueCalc)
                                        break ;

                              if (!bContinueCalc || qmsg.msg == WM_QUIT)
                                   break ;
                         lElapsedTime = WinGetCurrentTime (hab) -
                                                  lElapsedTime ;

                         if (!bContinueCalc || qmsg.msg == WM_QUIT)
                              iStatus = STATUS_READY ;
                              iStatus = STATUS_DONE ;

                         WinInvalidateRect (hwnd, NULL) ;

                         EnableMenuItem (hwnd, IDM_START, TRUE) ;
                         EnableMenuItem (hwnd, IDM_ABORT, FALSE) ;
                         break ;

                    case IDM_ABORT:
                         bContinueCalc = FALSE ;
                         break ;

                         break ;
               break ;

          case WM_PAINT:
               PaintWindow (hwnd, iStatus, iCalcRep, lElapsedTime) ;
               break ;

               return WinDefWindowProc (hwnd, msg, mp1, mp2) ;
     return 0L ;

Figure 5:

BIGJOB4 Make File

bigjob4.obj : bigjob4.c bigjob.h
     cl -c -FPa -G2sw -W3 -Zp bigjob4.c

bigjob.res : bigjob.rc bigjob.h
     rc -r bigjob.rc

bigjob4.exe : bigjob4.obj bigjob4.def bigjob.res
     link bigjob4, /align:16, /map, /nod slibc slibcp slibfa os2,
     bigjob4 rc bigjob.res bigjob4.exe

BIGJOB4.DEF Module Definition File

NAME           BIGJOB4
DESCRIPTION    'BigJob Demo Program No. 4(C) Charles Petzold, 1988'
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        ClientWndProc

BIGJOB4.DEF ── Second Thread Approach to Lengthy Processing Job

#define INCL_WIN
#define INCL_DOS

#include <os2.h>
#include <math.h>
#include <stdio.h>
#include "bigjob.h"

#define WM_CALC_DONE     (WM_USER + 0)
#define WM_CALC_ABORTED  (WM_USER + 1)

VOID FAR SecondThread (VOID) ;

BOOL  bContinueCalc = FALSE ;
HWND  hwndClient ;
SHORT iCalcRep ;
LONG  lSemTrigger ;
TID   idThread ;
UCHAR cThreadStack [4096] ;

INT main (VOID)
     static CHAR szClassName [] = "BigJob4" ;
     HMQ         hmq ;
     HWND        hwndFrame ;
     QMSG        qmsg ;

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

     WinRegisterClass (hab, szClassName, ClientWndProc,
                            CS_SIZEREDRAW, 0, NULL) ;

     hwndFrame = WinCreateStdWindow (HWND_DESKTOP,
                               | FS_SYSMENU    | FS_MINMAX
                               | FS_MENU,
                    szClassName, "BigJob Demo No. 4",
                    0L, NULL, ID_RESOURCE, &hwndClient) ;

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

     DosSuspendThread (idThread) ;

     WinDestroyWindow (hwndFrame) ;
     WinDestroyMsgQueue (hmq) ;
     WinTerminate (hab) ;

     return 0 ;

ULONG EXPENTRY ClientWndProc (HWND hwnd, USHORT msg, ULONG mp1,
                              ULONG mp2)
     static SHORT iCurrentRep = IDM_10 ;
     static SHORT iStatus = STATUS_READY ;
     static ULONG lElapsedTime ;

     switch (msg)
          case WM_CREATE:

               DosSemSet (&lSemTrigger) ;

               if (DosCreateThread (SecondThread, &idThread,
                                    cThreadStack + sizeof

                    WinAlarm (HWND_DESKTOP, WA_ERROR) ;

               break ;

          case WM_COMMAND:

               switch (LOUSHORT (mp1))
                    case IDM_10:
                    case IDM_100:
                    case IDM_1000:
                    case IDM_10000:
                         CheckMenuItem (hwnd, iCurrentRep,
                                        FALSE) ;
                         iCurrentRep = LOUSHORT (mp1) ;
                         CheckMenuItem (hwnd, iCurrentRep,
                                        TRUE) ;
                         break ;

                    case IDM_START:
                         iStatus = STATUS_WORKING ;
                         WinInvalidateRect (hwnd, NULL) ;

                         iCalcRep = iCurrentRep ;
                         bContinueCalc = TRUE ;
                         DosSemClear (&lSemTrigger) ;

                         EnableMenuItem (hwnd, IDM_START,
                                         FALSE) ;
                         EnableMenuItem (hwnd, IDM_ABORT,
                         break ;

                    case IDM_ABORT:
                         bContinueCalc = FALSE ;

                         EnableMenuItem (hwnd, IDM_ABORT,
                                         FALSE) ;
                         break ;

                         break ;
               break ;

          case WM_CALC_DONE:

               iStatus = STATUS_DONE ;
               lElapsedTime = mp1 ;
               WinInvalidateRect (hwnd, NULL) ;

               EnableMenuItem (hwnd, IDM_START, TRUE) ;
               EnableMenuItem (hwnd, IDM_ABORT, FALSE) ;
               break ;

          case WM_CALC_ABORTED:

               iStatus = STATUS_READY ;
               WinInvalidateRect (hwnd, NULL) ;

               EnableMenuItem (hwnd, IDM_START, TRUE) ;
               break ;

          case WM_PAINT:
               PaintWindow (hwnd, iStatus, iCalcRep, lElapsedTime) ;
               break ;

               return WinDefWindowProc (hwnd, msg, mp1, mp2) ;
     return 0L ;

VOID FAR SecondThread ()
     double A ;
     int    i ;
     LONG   lTime ;

     while (1)
          DosSemWait (&lSemTrigger, -1L) ;

          lTime = WinGetCurrentTime (hab) ;

          for (A = 1.0, i = 0 ; i < iCalcRep ; i++)
               if (!bContinueCalc)
                    break ;

               A = Savage (A) ;

          lTime = WinGetCurrentTime (hab) - lTime ;

          DosSemSet (&lSemTrigger) ;

          if (bContinueCalc)
               WinPostMsg (hwndClient, WM_CALC_DONE, lTime, 0L) ;
               WinPostMsg (hwndClient, WM_CALC_ABORTED, 0L, 0L) ;

Figure 6:

BIGJOB5 Make File

bigjob5.obj : bigjob5.c bigjob.h
     cl -c -FPa -G2sw -W3 -Zp bigjob5.c

bigjob.res : bigjob.rc bigjob.h
     rc -r bigjob.rc

bigjob5.exe : bigjob5.obj bigjob5.def bigjob.res
     link bigjob5, /align:16, /map, /nod slibc slibcp slibfa os2,
     bigjob5 rc bigjob.res bigjob5.exe

BIGJOB5.DEF Module Definition File

NAME           BIGJOB5
DESCRIPTION    'BigJob Demo Program No. 5(C) Charles Petzold, 1988'
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        ClientWndProc

BIGJOB5.C - Object Window Approach to Lengthy Processing Job

#define INCL_WIN
#define INCL_DOS

#include <os2.h>
#include <math.h>
#include <stdio.h>
#include "bigjob.h"

#define WM_OBJECT_CREATED     (WM_USER + 0)
#define WM_START_CALC         (WM_USER + 1)
#define WM_ABORT_CALC         (WM_USER + 2)
#define WM_CALC_DONE          (WM_USER + 3)
#define WM_CALC_ABORTED       (WM_USER + 4)

VOID  FAR SecondThread (VOID) ;

HWND  hwndClient, hwndObject ;
UCHAR cThreadStack [8192] ;

INT main (VOID)
     static CHAR szClassName [] = "BigJob5" ;
     HMQ         hmq ;
     HWND        hwndFrame ;
     QMSG        qmsg ;
     TID         idThread ;

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

     WinRegisterClass (hab, szClassName, ClientWndProc,
                            CS_SIZEREDRAW, 0, NULL) ;

     hwndFrame = WinCreateStdWindow (HWND_DESKTOP,
                               | FS_SYSMENU    | FS_MINMAX
                               | FS_MENU,
                    szClassName, "BigJob Demo No. 5",
                    0L, NULL, ID_RESOURCE, &hwndClient) ;

     EnableMenuItem (hwndClient, IDM_START, FALSE) ;

     if (DosCreateThread (SecondThread, &idThread,
                          cThreadStack + sizeof cThreadStack))

          WinAlarm (HWND_DESKTOP, WA_ERROR) ;

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

     WinDestroyWindow (hwndFrame) ;
     WinDestroyMsgQueue (hmq) ;
     WinTerminate (hab) ;

     return 0 ;

ULONG EXPENTRY ClientWndProc (HWND hwnd, USHORT msg, ULONG mp1,
                              ULONG mp2)
     static SHORT iCalcRep, iCurrentRep = IDM_10 ;
     static SHORT iStatus = STATUS_READY ;
     static ULONG lElapsedTime ;

     switch (msg)
          case WM_OBJECT_CREATED:

               EnableMenuItem (hwnd, IDM_START, TRUE) ;
               break ;

          case WM_COMMAND:

               switch (LOUSHORT (mp1))
                    case IDM_10:
                    case IDM_100:
                    case IDM_1000:
                    case IDM_10000:
                         CheckMenuItem (hwnd, iCurrentRep, FALSE) ;
                         iCurrentRep = LOUSHORT (mp1) ;
                         CheckMenuItem (hwnd, iCurrentRep, TRUE) ;
                         break ;

                    case IDM_START:
                         EnableMenuItem (hwnd, IDM_START, FALSE) ;
                         EnableMenuItem (hwnd, IDM_ABORT, TRUE) ;

                         iStatus = STATUS_WORKING ;
                         WinInvalidateRect (hwnd, NULL) ;

                         iCalcRep = iCurrentRep ;
                         WinPostMsg (hwndObject, WM_START_CALC,
                                   MAKEULONG (iCalcRep, 0), 0L) ;
                         break ;

                    case IDM_ABORT:
                         WinPostMsg (hwndObject, WM_ABORT_CALC,
                                     0L, 0L) ;

                         EnableMenuItem (hwnd, IDM_ABORT, FALSE) ;
                         break ;

                         break ;
               break ;

          case WM_CALC_DONE:

               iStatus = STATUS_DONE ;
               lElapsedTime = mp1 ;
               WinInvalidateRect (hwnd, NULL) ;

               EnableMenuItem (hwnd, IDM_START, TRUE) ;
               EnableMenuItem (hwnd, IDM_ABORT, FALSE) ;
               break ;

          case WM_CALC_ABORTED:

               iStatus = STATUS_READY ;
               WinInvalidateRect (hwnd, NULL) ;

               EnableMenuItem (hwnd, IDM_START, TRUE) ;
               break ;

          case WM_PAINT:

               PaintWindow (hwnd, iStatus, iCalcRep, lElapsedTime) ;
               break ;

          case WM_CLOSE:

               if (hwndObject)
                    WinPostMsg (hwndObject, WM_QUIT, 0L, 0L) ;
                    WinPostMsg (hwnd, WM_QUIT, 0L, 0L) ;
               break ;

          case WM_OBJECT_DESTROYED:

               WinPostMsg (hwnd, WM_QUIT, 0L, 0L) ;
               break ;

               return WinDefWindowProc (hwnd, msg, mp1, mp2) ;
     return 0L ;

VOID FAR SecondThread ()
     static CHAR szClassName [] = "BigJob5.Object" ;
     HMQ         hmq ;
     QMSG        qmsg ;

     hmq = WinCreateMsgQueue (hab, 0) ;

     WinRegisterClass (hab, szClassName, ObjectWndProc, 0L, 0, NULL) ;

     hwndObject = WinCreateWindow (HWND_OBJECT, szClassName,
                    NULL, 0L, 0, 0, 0, 0, NULL, NULL, 0, NULL, NULL) ;

     WinPostMsg (hwndClient, WM_OBJECT_CREATED, 0L, 0L) ;

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

     WinDestroyWindow (hwndObject) ;
     WinDestroyMsgQueue (hmq) ;

     WinPostMsg (hwndClient, WM_OBJECT_DESTROYED, 0L, 0L) ;

     DosExit (0, 0) ;

ULONG EXPENTRY ObjectWndProc (HWND hwnd, USHORT msg, ULONG mp1,
                              ULONG mp2)
     double A ;
     SHORT  i, iCalcRep ;
     LONG   lQueueStatus, lTime ;

     switch (msg)
          case WM_START_CALC:

               iCalcRep = LOUSHORT (mp1) ;
               lTime = WinGetCurrentTime (hab) ;

               for (A = 1.0, i = 0 ; i < iCalcRep ; i++)
                    lQueueStatus = WinQueryQueueStatus (HWND_DESKTOP) ;

                    if (lQueueStatus & QS_POSTMSG)
                         break ;

                    A = Savage (A) ;

               if (lQueueStatus & QS_POSTMSG)
                    break ;

               lTime = WinGetCurrentTime (hab) - lTime ;

               WinPostMsg (hwndClient, WM_CALC_DONE, lTime, 0L) ;
               break ;

          case WM_ABORT_CALC:

               WinPostMsg (hwndClient, WM_CALC_ABORTED, 0L, 0L) ;
               break ;

               return WinDefWindowProc (hwnd, msg, mp1, mp2) ;
     return 0L ;


OS/2 LAN Manager Provides a Platform for Server-Based Network Applications

Alan Kesler☼

The introduction of personal computer──based networks has provided
workgroups with systems that are capable of sharing expensive peripherals
and supporting applications. This article provides a historical perspective
of application development on PC network platforms, examines the impact of
the OS/2 systems on PC network applications, and introduces the programmatic
interfaces and capabilities provided by the Microsoft(R) OS/2 LAN Manager.

Historical Perspective

Applications that operate on PC networks can be categorized in one of
three ways. Standalone applications see the network as nothing more than a
shared printer and shared disk. They could just as easily be used on a
standalone personal computer. Network aware applications recognize the
existence of the network and use some of the network communication and
data-sharing capabilities. Multiuser databases like dBASE III PLUS(TM) or
Paradox(R) are the best examples of applications that are network aware.
Many network aware applications may also be used in a standalone

Finally, there are network intrinsic applications. These
applications──electronic mail, multiuser document editing, and distributed
database applications──don't make sense in a single personal computer or a
standalone configuration. What sets these applications apart from the
standalone and network aware applications is their use of distributed
computing on the PC LAN; with standalone and network aware applications,
all computing occurs in the network workstation. Only the program file and
data are downloaded from the network server.

This style of network-based computing has some disadvantages, such as
underutilization of computing power sitting idle elsewhere on the
network. Moreover, data-intensive applications will suffer performance
problems when all computing occurs in the workstation. With back-end
database servers, applications designed on a server and requester model
can offer much greater performance to many more users. Finally,
security is threatened by database applications that place large amounts
of unnecessary data on the network, since all data must be sent to the
network workstation in order to be processed. With distributed
applications, user permission and data access can be verified at the
server while a request is being processed. Only the requested information
need be placed on the network and sent to the requesting workstation.

Network intrinsic applications are set apart from both standalone and
network aware applications through their use of distributed computing. In
a distributed computing model the network application is partitioned
into a workstation or front-end process and a server-based or back-end
process. The workstation sends transactions to, and receives responses
from, the server process. One example is a store-and-forward electronic
mail system for a PC network. Users send electronic mail messages from
their workstations using a message editor. The message is then sent and
accepted by a mail server that runs in a network server on the LAN. The
mail server stores and forwards the message along the delivery path until
it is requested for delivery by the recipient.

Few Distributed Apps

The most frequently used applications on a network are standalone and
network aware applications. There have been few network intrinsic or
distributed network applications. Reasons for the lack of distributed
network applications include the lack of a sophisticated operating
system for the PC network platform, proprietary server environments, and
the complexity of programming.

During the 1980s, we've seen computing platforms downsized from
mainframes and minicomputers to desktop personal computers that provide
similar or even more raw computing power than their larger predecessors.
Unlike the downsizing of computing platforms from the mainframe to the
minicomputer, we have not seen a large-scale migration of crucial data
processing applications to the desktop personal computer or PC network.
The hardware platform has been in place with more powerful 80386-based
workstations, but the software has, for the most part, not followed. This
is because PC operating systems and PC LAN system software lacked the
power and sophistication of a multitasking operating system required for
many mission critical data processing tasks.

Many believe that with IBM and Microsoft's introducing Operating System/2,
applications once unable to operate on PC network systems will now
migrate down to the PC network platform. IBM's Systems Application
Architecture (SAA) is rooted in the concept of application and data
portability across IBM's large, medium, and Personal System/2(TM) systems.

Proprietary Environments

The network system software that functions in a network file server is
designed to provide high-performance, multiuser access to shared network
resources such as disks and printers. Network system software vendors
like 3Com, Novell, and IBM each implemented a proprietary means of
making DOS a multitasking operating system, since network server software
is by nature multitasking. The proprietary software environments, some
of which were openly documented by the vendor, were all based on
different application programming interfaces (APIs). An application
developer who wanted to create a distributed network application for the
leading network system software providers needed to implement the server
portion of the application several times, once for each leading network
system vendor. Nearly all application developers elected not to write to
these proprietary interfaces for the reasons of maintenance,
duplicate development effort, and inevitable support problems. Few
distributed applications were developed to these proprietary interfaces,
and most distributed applications were developed directly by the network
system software providers who knew the proprietary environment and`were
capable of supporting the software interfaces.

The introduction of Operating System/2 changes the development
environment dramatically. Now, leading network system software vendors
will support OS/2 as an industry-standard multitasking kernel within the
network server. Developers of network distributed applications have
the advantage of an industry-standard operating system that has more
than 250 application programming interfaces. Also, those who create
applications that use OS/2 as the operating system for the workstation
portion of the application have the added advantage of writing to the
same APIs for both the workstation and the server software.


Historically, developers of network applications have had an opportunity
to choose from several APIs for data-sharing and interprocess
communication services. These include APIs supported by DOS 3.x and the
Microsoft redirector, network BIOS or NetBIOS APIs, and APIs directly in
the transport layer of network protocols.

DOS 3.x and redirector APIs provide basic file-sharing and record-locking
capabilities. Network aware applications such as dBASE III PLUS are
written to these APIs. This class of application involves intensive data

Transport-specific APIs enable a developer to create communications-
oriented applications. Applications such as electronic mail, local and
remote network bridging, and message routing have the advantage of using
the specific capabilities of a given network transport. Unfortunately,
applications developed to a specific interface are not portable if
another network transport layer is used. For example, an application
that interfaces directly to TCP/IP for its internetworking support cannot
function on an IBM or ISO protocol without first being modified to
interface with the specific functions of those respective network
transport protocols.

NetBIOS, a communication-intensive protocol introduced with IBM's PC Net
in 1985, supports session-level interaction between workstations and
servers. As the name implies, NetBIOS is a low-level protocol that
requires the application developer to write directly to a network BIOS.
Perhaps the greatest advantage of the NetBIOS interface is that it is
supported or emulated by all leading network system software vendors. An
application developer who writes to the NetBIOS API can be reasonably sure
that the application will run on many network systems and therefore
garner a large market share. However, NetBIOS requires the application
developer to understand a detailed communication protocol at very low
levels in the system, therefore making NetBIOS applications difficult to
develop. Also, error conditions that result from NetBIOS transactions
(send, receive, etc.) need special programming over and above that
required by the underlying operating system. Applications developed to
the NetBIOS protocol include electronic mail and transaction-oriented
database applications.

The OS/2 LAN Manager

The Microsoft OS/2 LAN Manager is a network system software product that
provides file, print, resource, security, and network management services
to a work group using a local area network. A joint development project
of Microsoft and 3Com, the OS/2 LAN Manager is based on an OS/2 network
kernel, supports both DOS and OS/2 network workstations, has sophisticated
network management features, and provides an open system with complete
application programmer interfaces, including programming documentation and
development tools.

The Microsoft OS/2 LAN Manager includes both workstation and server
software. The workstation software includes user interface and requester
functions for DOS and OS/2 network workstations. The server operates on
top of the OS/2 kernel. Thus, as an application, the OS/2 LAN Manager is
hosted by OS/2 and is an OS/2 application in the network server. Because
it functions as an OS/2 application in the server, it can take advantage
of the features of OS/2, including the ability to use the 16Mb of
protected-mode memory provided by OS/2. The protected-mode memory allows
OS/2 LAN Manager to coexist with any other application written to OS/2
APIs. Applications written to OS/2 can operate side-by-side in the same
computer that is acting as a network server for a PC LAN. This is perhaps
the most important benefit gained from the use of the OS/2 kernel.

The OS/2 LAN Manager is based on OS/2, unlike network system software that
is based on proprietary implementations of multitasking DOS. Any
application developer will be able to create an application that uses
any of the more than 250 APIs within OS/2 and the application can run
side-by-side with the OS/2 LAN Manager. This frees developers from writing
to proprietary server interfaces. It is important to note that IBM has
announced a network system software product called OS/2 LAN Server, which
also is hosted by OS/2.


Applications written to take advantage of the security, network
management, or any other LAN Manager feature are portable to many
different network environments. This is because the OS/2 LAN Manager
itself is designed to be a transport protocol──independent system. The
diagram in Figure 1 describes the framework of multiple network transport
layers supported beneath OS/2 LAN Manager. For reference, the Microsoft
OS/2 LAN Manager architecture is mapped against the Open Systems
Interconnection (OSI) model.

Network system software like 3Com's 3+Open(TM) product line that incorporates
the Microsoft OS/2 LAN Manager will be supported on multiple network
transport layers. Network applications written to take advantage of
OS/2 LAN Manager APIs will have an opportunity for a greater share of the
market because the Microsoft OS/2 LAN Manager will support specific needs
of more environments by giving the customer the choice of network
transport. As shown in Figure 1, the OS/2 LAN Manager has several
protocol-independent interfaces including the NetBIOS and hardware
adapter driver (Media Access Control) levels. Future developments will
yield a network transport programming interface and an IBM(R) LLC (Logical
Link Control) 802.2 interface.


The Microsoft OS/2 LAN Manager supports equivalent calls to today's
application programmer interfaces (NetBIOS, DOS redirector calls) in both
an OS/2 and DOS environment. Today's DOS and NetBIOS APIs are supported
and are compatible with the installed base of network applications
written to DOS and NetBIOS. These include both normal DOS calls and
Interrupt 5CH calls from the OS/2 compatibility mode.

An Open System

The OS/2 LAN Manager is an open system. Application programmer interfaces
are provided for all important system functions. The system is hosted by
OS/2, making it fully compatible with the more than 250 APIs provided by
the kernel, in addition to the APIs shown in Figure 2.


Highly integrated with the OS/2 kernel over which it operates, the
Microsoft OS/2 LAN Manager extends the capabilities of OS/2 "pipes" or
interprocess communication across the network, allowing an application
to use remote procedure calls to processes running in the same computer
or a different one elsewhere on the network. This extension or
redirection of OS/2 interprocess communication across the network is
often referred to as "named pipes" or redirected pipes. It is this ability
to redirect interprocess communication across the network that is the
foundation of distributed and network intrinsic applications (see
Figure 3).

Of course, named pipes are not required to build an application that
uses PC LAN interprocess communication and remote procedure calls. What
makes the OS/2 LAN Manager implementation of named pipes different is
the ease of implementation for the developer when compared with similar
implementations based on NetBIOS or other program-to-program communications.
Where NetBIOS and other program-to-program protocols often require specific,
in-depth knowledge of networking issues, named pipes provide a high-level
interface that offers developers of DOS-based applications an opportunity to
quickly and easily create network intrinsic applications.

Remote Procedure Calls

Before providing an in-depth comparison of the use of named pipes versus
NetBIOS for remote procedure calls on a PC network, it helps to review the
types of transactions required to perform this task. The example below is
generalized to include the procedures necessary for a secure,
transaction-oriented, comprehensive commercial application. Included in
the application are user authentication, error logging, access control, and
access logging.

In general, the requesting workstation and responding server will have to
perform the following tasks:

  ■  user asks for a session

  ■  user sends username

  ■  server sends a challenge that is used by the workstation to provide
     the correct password validation back to the server

  ■  user sends password

  ■  server validates password, username

  ■  user sends remote procedure call

  ■  server checks for access control (can the user perform the remote
     procedure call)

  ■  server performs proper error response and logging if
     access/permission denied

  ■  server performs remote procedure call

  ■  server logs access to server process used by remote procedure call

  ■  server closes session

  ■  server gets next request

Named Pipes vs. NetBIOS

Named pipes are transaction-oriented and provide a much higher-level
interface to the application developer. The calling convention of named
pipes is much less complicated than that required with a NetBIOS solution.
A single named pipe function call equates to many NetBIOS calls. To
demonstrate the relative simplicity of the named pipe API in relation to
NetBIOS APIs, a side-by-side comparison is provided in Figure 4.

It is important to note that the named pipe example is presented in C
pseudocode and more closely approximates real C language code. The NetBIOS
example is presented with a high-level procedural description of steps
that would need significant embellishment to evolve to the detail of the C
language pseudocode. This was done because the NetBIOS example would be
too lengthy if provided in detailed C pseudocode. Even with a pseudocode
example, the differences in complexity of the calling convention between
named pipes and NetBIOS is obvious.

A remote procedure call requires two basic pieces: the workstation portion
that initiates the call and a server portion that responds to and acts
on the call request.

The function call necessary to request a remote procedure call from an
OS/2 LAN Manager workstation to an OS/2 LAN Manager server is provided in
Figure 4. For clarity, some of the arguments (like buffer lengths, for
example) have been excluded from the C pseudocode example.

You will notice that the DosCallNmPipe function takes the name of a
server, the name of a pipe, application name and request, and receive
buffer as arguments. The complete remote procedure call request is
performed with a single function call. All user validation, auditing,
error checking, and reporting is performed automatically by the OS/2
LAN Manager server, since a named pipe can be shared by an OS/2 LAN
Manager server. The OS/2 LAN Manager security system, network management,
audit trail, resource accounting, and user validation are a side benefit
of using named pipes.

To provide a similar capability using a NetBIOS API on an OS/2 LAN
Manager system would require workstation code with the functional
characteristics shown in Figure 4.

Notice that with the NetBIOS example, not only is the remote procedure
call more complex, but special-purpose code must be developed to validate
user access, report on resource usage, and handle special case error
conditions. Named pipes are treated as any other shared server resource,
and thus special-purpose code is not necessary as the error conditions,
validation, and reporting are handled automatically by the Microsoft
OS/2 LAN Manager. It is also important to note that the NetBIOS example
does not include special code to handle error conditions that would
require clean-up operations.

Named pipes can be either stateless, like DosCallNmPipe, or allow remote
procedure calls, like DosTransactNmPipe. Use of the DosCallNmPipe
procedure call provides for stateless remote procedure calls (sessions
only maintained during a transaction) and thus are less prone to error and
don't require special clean-up. Use of DosTransactNmPipe allows a series
of RPCs on the same session with the network server.

In addition to error conditions that result from session management, hard
errors, such as Abort, Retry, Ignore, Fail, occur. Since named pipes are
an extension of OS/2 pipes, built-in OS/2 error recovery is
automatically used should a hard error occur. With NetBIOS, hard errors
must be anticipated and dealt with within the application code itself.

Remote Procedure: Server Portion

Remote procedure calls that are initiated using named pipes from the
workstation must be supported by remote procedure management code on the
network server. The pseudocode in Figure 5 shows the APIs required by
the network server portion of the distributed application, as well as
the pseudocode necessary to provide similar capabilities if NetBIOS is
used rather than named pipes.

Note that in the NetBIOS example, all password and user verification,
reporting, auditing, and resource management must be supported directly by
the application code running in the network server.

The system-level capabilities - portability, stateless operation,
multiple RPC calls, auditing, security, administration, error handling,
and higher performance - are built into the Microsoft OS/2 LAN Manager and
are therefore automatically available to any application that uses named
pipes for interprocess communication across the network.


OS/2 LAN Manager is hosted by the OS/2 kernel; therefore, applications
written to OS/2 APIs can be ported to perform remote procedure calls and
function in a distributed configuration on the PC LAN with only minor
modifications to the application code. This is not true if the application
was ported from OS/2 to use NetBIOS as an interprocess communication API.
It is also important to note that applications written to OS/2 can run
without modification in an OS/2 LAN Manager network server. Of course,
this is not the case with all network system software. Many vendors who
provide PC network systems for business work groups don't provide an
effective OS/2 server environment for OS/2 applications installed in the

Stateless Operation

A stateless distributed application maintains a session with the server
only during the length of a single procedure call. Each procedure call to
the server from the workstation creates a session, performs the remote
procedure call, and closes the session. With named pipes this is
performed using DosCallNmPipe. To perform the remote procedure call
using NetBIOS, the NetBIOS application must perform a call, send,
receive, and hang up. The NetBIOS example requires the session to live
longer than a simple request/receive transaction. This is particularly
important with error management. The longer a session is open, the
more likely an error may occur. If you don't have a stateless design,
special case code has to be provided to perform a clean-up from
potential errors.

Multiple RPC Calls

Named pipes also include procedure calls that are useful when the
application transacts multiple remote procedure calls on the same session
to the server. For some applications, this is the desired means for remote
procedure calling. Applications that perform normal DOS reads, DOS
writes, or DosTransactNmPipe calls to a named pipe are operating in such a
multicall mode, which may offer performance advantages because the
session is maintained across multiple transactions and doesn't have to be
reestablished for every transaction between the requester and the


Many distributed applications require sophisticated audit trail facilities
that track who performed the remote procedure call, from which
workstation, at what time, and how long the remote procedure call took.
Using a NetBIOS-based distributed application, all of the functions
required for auditing would have to be specially developed as part of the
distributed application. With the Microsoft OS/2 LAN Manager, the
network administrator can flag one or more specific named pipes that exist
on the server and mark them to be audited; this causes a record to be
entered in the OS/2 LAN Manager audit trail each time a named pipe is
opened or closed. The record generated in the audit trail can be used as
the foundation of a bill-back system. It includes the date and time of
access, who accessed the named pipe, the type of access (open/close), and
the duration of the access.


The Microsoft OS/2 LAN Manager incorporates a sophisticated user
security system that includes encrypted user log-on validation, access
control list, error reporting, and real-time event notification of
attempted security breaches should access to a shared resource be
attempted by an unauthorized user. All of these capabilities are
available to a distributed application that uses named pipes as the
means for interprocess communication across the network. Access to named
pipes is controlled by the server's standard user logon, password
encryption, group membership, and access permission mechanism. The
capabilities are provided without requiring any additional programming by
the application developer.

Treated by the server as a shared resource, the named pipe can be placed
in the server's resource list and given access permission using the OS/2
LAN Manager administrative software. Once logged as a shared server
resource, the OS/2 LAN Manager will report on the status of the resource
just as it would any other shared resource on the network. Attempts to use
the named pipe resource will be logged in the system audit trail. Should
excessive unauthorized access to the resource be attempted, a real-time
message will be sent to advise a predetermined network administrator of
the attempted security breach. Finally, network errors resulting from the
use of the named pipe will be automatically logged in the system's error
file and can be reviewed by the network administrator.


The OS/2 LAN Manager has a window-oriented administrative user
interface that supports real-time administration of shared network
resources. If distributed applications are designed to use named pipes as
the means for interprocess communication, the LAN Manager administrative
interface can be used to view and manage access to the redirect pipes
without any development specifically embedded in the distributed
application. OS/2 LAN Manager provides management of named pipes,
including the ability to view real-time user access to the named pipe.
Data includes local machine name, user name, resource name, total session
time, and session idle time.

In addition, OS/2 LAN Manager provides the ability to force a user to
disconnect from a named pipe and automatically disconnect workstation and
server sessions based on user activity and an administrator defined
timeout parameters. If a named pipe is open between a workstation and
server (there is an open handle), the session will not timeout. A built-in
pop-up message system can be used directly by the distributed application to
inform users with a real-time message. The message system can also be used
by the network administrator to send messages to individual users.

Error Handling

Pipe management code is part of the operating system kernel within OS/2.
It adheres fully to the operating system's method of error reporting,
including use of standard OS/2 error codes and issuance of hard error pop-
ups where appropriate with abort/retry/ignore/fail error handling. Because
NetBIOS session capability is not a supported interprocess communication
within the operating system, distributed applications written to NetBIOS
must include a significant amount of specific error-handling code.

Named pipes also provide advantages for the server portion of the remote
procedure code. If a named pipe fails during processing, the operating
system performs all the cleanup. But if any NetBIOS send fails, the
application will have to cancel any asynchronous receive requests that
have been posted and hang up the session. Each of these clean-up tasks
could generate an error as well, from which the application must be
programmed to recover.


The Microsoft OS/2 LAN Manager named pipe API supports function calls
such as DosTransactNmPipe and DosCallNmPipe that do entire transactions in
a single system call. By contrast, multiple NetBIOS calls would be
necessary to perform the same function as the single named pipe call.
Because named pipes call the operating system less often, distributed
applications based on named pipes for remote procedure calls and
interprocess communication are likely to yield high-performance results.


Many of the advantages achieved by the developer who uses named pipes for
PC network interprocess communication have been outlined. There are,
however, costs to the developer who uses OS/2 LAN Manager named pipes,
which deserve serious consideration from any developer who is planning
to create a distributed network application.

Currently, named pipes have been announced as a fully supported API on
systems that incorporate the Microsoft OS/2 LAN Manager. 3Com's 3+Open
product line is an example of one such system. Although it is reasonable
to assume that other vendors who provide network system software product
lines will incorporate the Microsoft OS/2 LAN Manager product, and that
these systems will also support named pipes, there are many more installed
network systems that support NetBIOS and will continue to do so. The
developer of a network application will, at least initially, choose
between capability and installed base market share. For many developers,
the advantages of named pipes and the market position of companies who
have announced Microsoft OS/2 LAN Manager-based systems is
encouragement enough to forgo NetBIOS and develop to the named pipe
interface. Even if a developer of network applications already knows and
understands the NetBIOS API, using named pipes will require additional

Microsoft has taken direct steps toward reducing the need for further
education with its Microsoft University classes. These classes deal with
the fundamentals of developing OS/2 applications as well as applications
development for the Microsoft OS/2 LAN Manager. Microsoft is also
sponsoring several OS/2 LAN Manager developers' conferences this year.
The high-level interface and simplicity of named pipes, the classes at
Microsoft University, and the developers' conference will benefit
developers of any network application.

Application Platform

Although the success of network systems incorporating the OS/2 LAN Manager
won't be known until they are introduced about mid-year 1988, the Microsoft
OS/2 LAN Manager provides a rich platform for application development.

The strong points of the Microsoft OS/2 LAN Manager currently available to
developers of network applications and end customers include:

  ■  use of an industry-standard multitasking operating system, OS/2

  ■  high performance and reliability

  ■  open system architecture

  ■  support for OS/2 APIs as well as NetBIOS and DOS redirector calls

  ■  support for OS/2 pipes and extension of them across the network using
     named OS/2 pipes

  ■  rich system-level function that lets applications benefit from the
     administrative functions already built into the system


The advent of IBM OS/2 and the use of industry standard multitasking
operating systems as the kernel to network system software provides an
opportunity for the development of applications that reside in the network
server and provide distributed computing power to the PC network. Network
applications that once had to be developed to use proprietary, vendor-
specific operating system interfaces will now be developed to industry-
standard OS/2 operating system interfaces. In addition, Microsoft has
simplified network interprocess communication with the OS/2 named pipes
capability. OS/2 LAN Manager APIs can significantly reduce the overhead of
developing a network intrinsic application. People in the industry believe
that the OS/2 network servers and these new APIs will result in many new
applications, such as database servers, mail servers, name/directory
servers, document library services, accounting (using a distributed database
server as the back end), and communication gateways.

The industry direction by Microsoft, IBM, and 3Com of providing and
marketing open system server platforms based on Operating System/2 as an
industry-standard operating system kernel will benefit both the developer
and end customer of PC network systems with more powerful and functional
application software.

Figure 1:  The Microsoft OS/2 LAN Manager has a protocol-independent design.

│   ╔═════════════╗      ╔═══════════════════════════════════╗            │█
│   ║ Application ║      ║ Microsoft OS/2 LAN Manager Server ║            │█
│   ╟─────────────╢      ║     and Workstation Software      ║            │█
│   ║ Presentation║      ╚═══════════════════════════════════╝            │█
│   ╚═════════════╝                                                       │█
│                                                                         │█
│   ╔═════════════╗      ╔════════╗   ╔════════╗   ╔════════╗-NetBIOS     │█
│   ║   Session   ║      ║  IBM   ║   ║        ║   ║        ║             │█
│   ╟─────────────╢      ║NetBIOS/║   ║ TCP/IP ║   ║  ISO   ║             │█
│   ║  Transport  ║      ║ Token  ║   ║        ║   ║        ║             │█
│   ╟─────────────╢      ║  Ring  ║   ║        ║   ║        ║             │█
│   ║   Network   ║      ╚════════╝   ╚════════╝   ╚════════╝             │█
│   ╚═════════════╝                                                       │█
│                                                                         │█
│   ╔═════════════╗      ╔═══════════════════════════════════╗-802.2 LLC  │█
│   ║  Data Link  ║      ║       Adapter Interface and       ║            │█
│   ╟─────────────╢      ║          Protocol Manager         ║            │█
│   ║   Physical  ║      ╚═══════════════════════════════════╝-Media      │█
│   ╚═════════════╝                                            Access     │█
│                                                              Control    │█

Figure 2:  System functions supported by an application programming
           interface include, but are not limited to, those shown here.

Network Interface
Media Access Control (network adapter
driver interface)
Protocol manager (to support multiple,
 simultaneous network protocols)

Network Management/Administration
Audit trail
Error log
Network error reporting
Network statistics

Input/Output Devices and Pipes
Named pipes (redirected pipes)
Character devices
Mail slots

Network Access
Use/un-use network resources
Access control list security
Server/workstation configuration
Resource sharing
Network console
Net Run (OS/2 compute server)
Logon scripts
User profiles

Figure 3:  OS/2 pipes are used to redirect interprocess communication within
           a single computer. Named pipes redirect interprocess communication
           and make it possible for applications to transparently use
           computing resources in other workstations or servers on the

  ╔══════════════╗                  ┌──────Interface──────┐
  ║ ░░░░░░░░░░░  ║█                 │                     │
  ║ ░░░░░░░░░░░░ ║█             OS/2 Pipe             OS/2 Pipe
  ║ ░░░░░░░░░░░░ ║█                 │                     │
  ║ ░░░░░░░░░░↔↨ ║█                 ▼                     ▼
╔═╝──────────────╚═╗            Database ◄──OS/2 Pipe──► Report
║ ----‼‼‼‼‼‼‼‼‼‼‼  ║█           Manager                  Writer
╚══════════════════╝█  ─────────────────────────────────────────────────────
  ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀                  ╔══════╗
    Standalone PC                      ║│     ║█ Server
                                       ║│▄▄▄▄▄║█            ╔═══════════╗
                                       ║┌───┬┐║█            ║ ░░░░░░░░  ║█
                                       ║│   ││║█            ║ ░░░░░░░░░ ║█
  ┌──────────Name         Database     ║│   ││║█            ║ ░░░░░░░░░ ║█
User     (Redirected)────►Manager      ║│   ││║█◄PC Network►║ ░░░░░░░↔↨ ║█
Interface  OS/2 Pipe        ↑          ║└───┴┘║█          ╔═╝───────────╚═╗
  │                      OS/2 Pipe     ║||||||║█          ║ ----‼‼‼‼‼‼‼‼  ║█
  └──────────Name      ┌─►  ↓          ║||||||║█          ╚═══════════════╝█
         (Redirected)──┘  Report       ╚══════╝█            ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
           OS/2 Pipe      Writer         ▀▀▀▀▀▀▀              Workstation

Figure 4:  A pseudocode comparison of a LAN manager API call (shown on top)
           and the equivalent NetBIOS calls (shown below).

LAN Manager API Call

DosCallNmPipe("\\servername\pipe\appname", RpcRequBuf, RpcRespBuf)


Begin Procedure
  Get username from the user
  Get password from the user
  Establish session with server
  Send username to server
  Receive server challenge (used for password encryption)
  Do the encryption challenge using server challenge and user password
    Result of logic is challenge response
  Send challenge response with remote procedure request to server
  Receive remote procedure call response
  Close session with server
End Procedure

Figure 5:  The LAN Manager itself handles a great deal of the administrative
           details required through NetBIOS.

LAN Manager API Call

DosMakeNmPipe(\"pipe\appname", handle)
loop /* for each remote procedure call */
  DosRead(handle, RpcReqBuf)
  Do the remote procedure call
  DosWrite(handle, RpcRespBuf)


  Listen for user session
  Establish user session
  Receive username
  Do encryption logic to generate challenge
  Send challenge
  Receive challenge response and remote procedure call request
  Do encryption logic to validate user challenge response
    Open and match username in application specific access control file
    If access control supports group memberships, then verify membership
  If validation fails,
    Generate an audit trail record that access was denied
    Send an access denied response
    Close session
  Do the remote procedure call
  Send the remote procedure call response
  Close the session
  Calculate time duration of remote procedure call handling and
       generate audit trail record of service provided for this user


Writing OS/2 Bimodal Device Drivers: An Examination of the DevHIp API

Ray Duncan☼

Device drivers are the modules of an operating system that issue commands
directly to peripheral devices and cause data to be transferred between
those devices and RAM. Drivers are thus the bottom, most hardware-
dependent layer of the system software. They shield the operating system
kernel from dealing with hardware I/O port addresses and the peculiarities
and characteristics, such as number of tracks or sectors per track, of a
particular peripheral device, just as the kernel in turn shields
applications programs from the details of file and memory management.

By convention, Microsoft(R) OS/2 device drivers reside in individual disk
files with the extension .SYS. The essential OS/2 drivers for the keyboard,
display, and disk device are selected on the basis of the hardware
environment and automatically linked to the operating system during booting.
Other optional device drivers can be loaded by DEVICE= statements in the
CONFIG.SYS file on the boot disk and are referred to as installable drivers.
However, all OS/2 systems drivers have the same physical and logical
structure and an identical relationship to the OS/2 kernel.

OS/2 device drivers are in many ways similar to MS-DOS(R) device drivers,
but they also have unique capabilities and requirements, including:

  ■  fully interrupt-driven operation

  ■  bimodal (real mode and protected mode) operation

  ■  support for multitasking and overlapped I/O

  ■  the ability to deinstall after the driver is fully initialized

  ■  support for device monitors (applications that can register with
     the driver to filter its data stream)

  ■  support for ROM BIOS service calls by real-mode applications

The experienced MS-DOS programmer must be particularly wary of
terminology that sounds familiar even though the associated driver function
is very different. A full-fledged OS/2 driver for any given device is at
least an order of magnitude more complex than its MS-DOS equivalent.

Device Driver Types

OS/2 device drivers are categorized into two groups: character device
drivers and block device drivers. Whether a given driver is a character
device or block device driver determines how the associated device is
viewed by OS/2 and what functions the driver itself must support.

Character device drivers control peripheral devices that perform input
and output a character (or byte) at a time, such as a terminal or a
printer. A single character device driver ordinarily supports a single
hardware unit, although a given file can contain more than one driver.
Each character device has a one- to eight-character logical name, and can
be opened by using this name for input or output by an application
program as though it were a file. The logical name is strictly a means of
identification of the driver to OS/2 and has no physical equivalent on
the device.

Block device drivers control peripheral devices that transfer data in
chunks rather than a byte at a time. Block devices are usually randomly
addressable, such as floppy disk or hard disk drives, but they can also be
sequential in nature, such as a magnetic tape drive. A block driver can
support more than one physical unit and can also map two or more logical
units onto a single physical unit, such as a partitioned hard disk.

OS/2 assigns single-letter drive identifiers (A:, B:, and so on), instead
of logical names, to block devices. The first letter assigned to a given
block device driver is determined solely by that driver's position in the
chain of all drivers, that is, by the number of units supported by the
block drivers that have been loaded before it. The total number of
letters assigned to the driver is determined by the number of logical
units that the driver supports.

Device Driver Structure

OS/2 device drivers are written as small-model Macro Assembler programs
with one code and one data segment and are linked as a segmented or "new"
EXE file. Figure 1 is a diagram of the physical structure of a device
driver file; the data segment always lies in the first part of the file,
with a structure called the device driver header at its beginning. The
header contains information about the driver that the OS/2 kernel can
inspect when it needs to satisfy an I/O request by an applications program
(see Figures 2 and 3); all of the device drivers in the system are
chained together through pointers that are stored in the device driver

A driver has two major components: the strategy routine and the interrupt
routine. Although they have the same names as the two most important
sections of an MS-DOS installable driver, they function in a very different

The Strategy Routine

The OS/2 kernel will initiate an I/O operation by calling the driver's
strategy routine (whose offset is found in the device driver header),
passing the address of an information packet called a request header in
registers ES:BX. The first 13 bytes of the request header have a fixed
format and are followed by data that varies according to the operation
being requested (see Figure 4).

The most crucial field of the request header is the command code, which
selects a particular driver subfunction (see Figure 5). Many of the
subfunctions can be carried out immediately at the time of the strategy
call; in such cases, the driver simply places any necessary information to
be returned to the kernel into the request header and sets the done bit in
the status word.

However, some subfunctions, such as read and write, involve an
unpredictable delay and cannot be completely carried out during the
strategy call without an adverse impact on overall system performance.
The request headers for such subfunctions are chained onto the driver
queue or work list, the actual I/O operation is started if the physical
device happens to be idle, and then the strategy routine returns control
to the kernel so that other work within the system can go forward.
Incidentally, block device drivers are expected to sort their pending
work list so as to minimize the average transfer time.

The driver strategy routine normally executes at the highest privilege
level (Ring 0), with the exception of the initialization subfunction,
which executes at Ring 2 (the same as an application with I/O privilege).
The strategy routine must be fully reentrant and capable of processing
multiple requests simultaneously.

The Interrupt Routine

The driver interrupt routine is entered from the kernel as a result of a
hardware interrupt from the physical device, when an I/O operation is
either finished or has been aborted due to a hardware error. The
interrupt routine queries the peripheral controller to determine the
outcome of the I/O operation, sets the done bit and the appropriate
status and error information in the request header, removes the request
header from the work list, and notifies the kernel that the operation is

The interrupt routine then examines the driver's work list to determine
whether other I/O operations are pending for the device. If additional
request headers are waiting, the interrupt routine initiates the
operation that is at the head of the queue before returning control to
the kernel by clearing the carry flag and issuing a FAR RET. In cases
where several devices (and their drivers) share the same interrupt level,
the kernel will call each driver's interrupt routine in turn until one
indicates its acceptance of the interrupt by clearing the carry flag; the
other interrupt routines must return with the carry flag set.

Since hardware interrupts are by nature asynchronous and unpredictable, an
OS/2 device driver must be capable of properly servicing an interrupt in
either protected mode or real mode. In order to assist the driver, OS/2
always provides special bimodal pointers to request headers-pointers that
are valid in either real mode or protected mode. This is accomplished by
reserving an area of physical memory to hold request headers and a set of
selector values such that the selector for each header is exactly the
same as its segment (paragraph) address. As an extra aid to the driver,
OS/2 locks down memory segments that are the source or destination of a
data transfer and converts their segment:offset or selector:offset
addresses into linear 32-bit physical addresses before passing the read or
write request to the driver.

Unique Aspects

Three particularly interesting features of OS/2 device drivers have no
counterpart under MS-DOS: the necessity to provide ROM BIOS-equivalent
services for real-mode applications, support for device monitors, and
the ability of a driver to deinstall itself on demand.

The ROM BIOS requirement stems from the propensity of many popular MS-DOS
application programs to call the ROM BIOS directly rather than perform
their I/O in a well-behaved manner through documented MS-DOS services.
The most frequent reason for such ROM BIOS calls is to improve performance
or because MS-DOS does not offer the necessary functionality (EGA palette
programming, for example). Since the ROM BIOS code is mostly incompatible
with the constraints of a protected-mode multitasking environment──it is
not reentrant and it frequently masks off interrupts, among other
problems──an OS/2 driver must capture the interrupt vectors that invoke
ROM BIOS services for its device. It can then either interlock the ROM
BIOS routines to prevent interference with the execution of protected-
mode applications or substitute a new set of routines that provide
equivalent services.

Device monitors are an OS/2 innovation that allow keyboard enhancers,
macro-expanders, print spoolers, and similar utilities to be written and
installed in a hardware-independent manner. An applications program
that wishes to filter the data stream of a particular character device
driver can issue an OS/2 Application Program Interface (API) function call
to register as a monitor for that device. Then the OS/2 kernel acts as
the intermediary and asks the driver if it wishes to accept the monitor
registration. If the driver assents, it will cooperate with the kernel to
pass each character it reads from or writes to the device through a
buffer belonging to the monitor program, which can add, delete, or
substitute characters at its discretion.

Finally, extension of the device driver definition with the DeInstall
command code provides efficient use of memory and a more controlled
environment for character drivers. A character device driver, under MS-
DOS, can be superseded by simply installing another driver with the same
logical device name; the first driver loaded has no way to prevent being
superseded, and the memory it occupied is simply lost. Under OS/2, if a
driver is loaded that has the same logical name as a previously loaded
driver, the kernel first sends a request header with the DeInstall command
code to the previous driver. The first driver can then release its
interrupt vectors and memory and return a success status, after which
the new driver is allowed to initialize itself. Alternatively, if the
first driver refuses the DeInstall command by returning an error code,
the kernel aborts the installation of the new driver with the same name.

Device Driver Helpers

Under MS-DOS, device drivers must be self-contained. They can use a very
limited subset of the Interrupt 21H function calls during their
initialization (character output, get MS-DOS version, get or set
interrupt vector), but once the system is fully loaded and running, device
drivers are not allowed to invoke any MS-DOS system services at all. MS-
DOS, as a single-tasking operating system, assumes that only one I/O
request will be in progress at any given time. If a driver that is in the
process of carrying out an I/O request from the kernel in turn requests a
different operation from the ker-nel, the context of the original
request is destroyed, and the system will crash.

OS/2, on the other hand, was designed specifically for multitasking, and
most of its internal routines are fully reentrant. As a result, OS/2
provides a battery of services to device drivers, called device driver
helpers, or DevHlps, much as it does to application programs, although the
nature of these services is of course much different. Moving
functionality that all drivers need into the kernel makes drivers
smaller, simpler, and easier to debug and ensures that critical driver
activities are carried out uniformly and efficiently.

Unlike the kernel's API, in which parameters are passed on the stack and
each function has a distinct, named entry point, the DevHlp interface uses
a register-based parameter-passing convention and a common entry point.
The driver is provided with a bimodal pointer to the DevHlp entry point in
the request packet it receives from the kernel during its initialization.

When the DevHlp entry point is called, a particular DevHlp function is
selected by the value in register DL. Additional parameters and addresses
are passed in other general registers as demanded by the particular function
being requested. The status of a DevHlp function is usually returned to the
driver in the zero or carry flags, error codes in register AX, and addresses
in registers DS:SI or ES:DI (see Figure 6 for an example of a
typical DevHlp call).

Also see:
Summary of the way Device Drivers Queue and Process I/O Requests

DevHlps and Driver Modes

Whenever an OS/2 device driver has control of the CPU, it executes in one
of four different modes or contexts: kernel mode, interrupt mode, user
mode, and init mode. Understanding these modes and their implications is
essential to the task of writing an OS/2 driver, since the DevHlp
services available to a driver are determined by its current mode.

A driver is in kernel mode when its strategy routine is called by the
kernel, usually as the immediate result of an I/O request by an application.
The driver executes at the highest privilege level (Ring 0). Nearly every
DevHlp service is available to the driver in kernel mode.

The driver executes in interrupt mode as the result of a hardware
interrupt. The OS/2 kernel fields the interrupt, saves all registers, and
enters the driver's interrupt routine via a far call. The driver executes
at the highest privilege level (Ring 0). A somewhat restricted set of
DevHlp services is available to a driver in interrupt mode.

The driver executes in user mode when it is entered as a result of a
software interrupt by an application executing in the real-mode session
(sometimes called the DOS 3.x compatibility box). The only drivers
that must concern themselves with this mode are those that provide
equivalents to the original IBM(R) PC ROM BIOS services in a manner
consistent with the multitasking and memory protection requirements of
OS/2, such as the keyboard, video, or floppy disk driver. Since a driver
only runs in user mode when the CPU is in real mode, the concept of
privilege levels is irrelevant, but you can think of the driver as
executing at the highest privilege level since no protection mechanisms
are operative. Relatively few of the DevHlp services are available to
the driver in user mode.

A driver runs in init mode only once: when its strategy routine is called
with a request packet containing the initialization command code,
immediately after the driver is loaded into memory during the system boot
process. In init mode, the driver runs as though it were an application
program IOPL segment, that is, at the next-to-lowest privilege level
(Ring 2). A limited set of dynamic-link API calls, mostly concerned with
file management (DosOpen, DosRead, DosWrite, and so on), as well as a
subset of the DevHlp services (mainly those associated with hardware and
memory management), are available to a driver in init mode.

Task time and interrupt time are two other terms in the OS/2 device driver
documentation regarding driver execution context. A driver is executing
at task time when it is called synchronously as the direct result of
some application program API request or real-mode software interrupt;
this includes both the user mode and kernel mode contexts described
above. Interrupt time refers to driver routines being invoked
asynchronously as the result of an external hardware event; consequently,
the identity of the currently executing process and the state of the CPU
(real mode or protected mode) is unpredictable.

Categories of DevHlps

The roughly 40 DevHlp functions fall into nine categories (see
Figure 7): process management, memory management, hardware management,
request packet management, character buffer management, timer management,
monitor management, semaphore management, and ABIOS communication
(IBM PS/2(TM) only).

Some of these categories of services are used by every OS/2 device driver,
because they allow the driver to perform overlapped, interrupt-driven
I/O operations in a well-behaved manner, or they provide access to memory
addresses that the driver would otherwise be unable to reach. The others
are present only for convenience or to assist in the construction of
unusual or highly complex drivers. A more detailed look at the most
commonly used groups of DevHlp functions also brings up some interesting
points about the operation of OS/2 device drivers and their relationship
to the kernel.

Process Management

The basic technique by which a driver performs overlapped or asynchronous
I/O is very simple. When the strategy routine of the driver is called, it
sets up the I/O operation (or adds it to the list of pending operations,
if one is already in progress) and simply returns to the OS/2 kernel
with the done bit cleared in the request header status word. The kernel
then suspends the thread that initiated the I/O request. (For more
information about OS/2 threads and processes, see "OS/2 Multitasking:
Exploiting the Protected Mode of the 80286," MSJ, Vol. 2 No. 2, p. 27.)

When the I/O operation is eventually completed, the driver's interrupt
routine updates the request packet associated with the operation with the
appropriate status word and any other necessary information and then
calls the DevHlp routine DevDone. This signals the kernel to awaken the
thread that owns the I/O request, which can then perform any necessary
post-I/O processing, such as translating the driver's status information
into appropriate values to be returned to an application program.

A driver can also use the DevHlps Block and Run to suspend a thread while
an I/O operation is in progress, particularly if there is a lengthy
amount of code in the driver that must be executed after the operation is
complete. Putting such code in the strategy routine is preferable to
putting it in the interrupt routine, since other lower-priority
interrupts are locked out by the hardware while the interrupt routine is
executing. The strategy routine sets up the I/O operation and then calls
Block with a 32-bit identifier of its own choosing. When the Interrupt
routine notes that the I/O operation is complete, it calls Run with the
same 32-bit identifier. This will cause the kernel to wake up the original
thread within the strategy routine, which can proceed with any necessary
post-I/O processing and then exit back to the kernel with the request
header's done bit set. The biggest problem with this approach is ensuring
that the 32-bit value used in a pair of Block and Run calls is unique
within the system, since Run wakes up all threads that have blocked with
that particular 32-bit identifier.

The DevHlp services Yield and TCYield are provided for drivers that need
to transfer long strings of data from one memory location to another, such
as a RAMdisk, or whose devices do not support interrupts. A call to Yield
simply tells the kernel's scheduler to give any threads with the same or
higher priority a chance to run before returning control to the
currently executing thread, while TCYield allows only those threads
that have a time-critical priority to take a turn. A driver can check the
value of YieldFlag or TCYieldFlag (two variables whose addresses are
returned by GetDOSVar, another DevHlp) to determine whether any other
threads are waiting for the CPU and bypass the call to Yield or TCYield if
no other thread is eligible to execute. Such a check along with a possible
Yield should be made at least every 3 milliseconds to avoid interference
with the proper operation of other drivers.

Hardware Management

Since the strategy and interrupt routines of device drivers execute at
the highest privilege level (Ring 0), they have a free hand with the
machine if they want it. As a result, two different drivers that access
the same hardware addresses, such as a serial mouse driver and a
communications driver that are both configured for COM2, can theoretically
come into conflict. However, OS/2 has DevHlps to arbitrate interrupt
vector and 8259 Programmable Interrupt Controller (PIC) usage for
cooperating drivers. Well-behaved drivers will use these DevHlps to gain
access to the hardware resources they need.

The DevHlp services SetIRQ and UnsetIRQ assist drivers in capturing or
releasing interrupt vectors. When a driver calls SetIRQ to hook its
interrupt handler to a given interrupt request (IRQ) level, it also
specifies whether it is willing to share the level with another driver.
If it specifies that it wants to own the interrupt exclusively, subsequent
requests for the same IRQ level by other drivers will fail; if another
driver has already signed up for the interrupt, the request by the current
driver will fail.

When a hardware interrupt occurs, a dispatcher in the OS/2 kernel
initially receives control, saves all registers, then enters the driver's
interrupt handler via a far call. When several drivers share an
interrupt level, the dispatcher calls each driver's handler in turn.
The handler must access its device and determine whether that device
caused the interrupt. If the driver finds that its own device did not
trigger the interrupt, it returns to the kernel with the carry flag set.
On the other hand, if the handler finds that it owns the interrupt, it
must service its device, possibly start another I/O operation if any are
waiting, call the DevHlp service EOI to dismiss the interrupt at the 8259
PIC, and then return to the kernel with the carry flag clear. Note that,
although drivers can directly access their own device's ports,
manipulation of the PIC should be reserved for the kernel to isolate
drivers from the interrupt architecture.

Memory Management

A driver specifies the amount of storage to reserve for its code and data
segments in the information it returns to the kernel at load time. Later
on, if the driver needs more memory for tables, buffers, or other data
structures, it can use the DevHlp services AllocPhys and FreePhys to
dynamically allocate and release such memory. The memory that is assigned to
the driver by AllocPhys is not movable or swappable, and the driver can
specify whether it wants the memory located above or below the 1Mb boundary.

One of the trickiest aspects of an OS/2 driver's operation is memory
addressing. Since a driver must be able to execute and handle interrupts
equivalently in either protected mode or real mode, it might encounter
three types of addresses: protected-mode selector:offset pairs, real-
mode segment:offset pairs, and the linear or physical 32-bit addresses
used by DMA channels and some devices. The operating system's virtual
memory manager, which may shuffle segments around to collect unused
fragments together or swap segments out to disk, must also be taken into

The OS/2 kernel takes several measures to shield drivers from most of the
potential problems with memory addressing. First, whenever the kernel
requests an explicit transfer operation from a driver with a request
packet containing the read or write command codes, it passes the driver a
physical 32-bit address that has already been "locked," that is, the
virtual memory manager has been notified that the memory in question
should not be moved or swapped. When the kernel passes the driver an
address of a structure (such as a request packet) or a procedure (such as
the DevHlp entry point) that the driver will need to access directly, it
arranges things so that the address is valid in either real mode or
protected mode, that is, the protected-mode selector for the structure or
procedure is the same as its real-mode segment. Such an address is called
a bimodal pointer.

Next, the kernel provides six DevHelp functions which assist a driver by
converting physical 32-bit addresses to virtual (segment:offset or
selector:offset) addresses and back again: AllocateGDTSelector,
PhysToGDTSelector, PhysToUVirt, UnPhysToVirt, and VirtToPhys. The first
three are used to access static structures (such as memory allocated by
AllocPhys or an adapter's memory-mapped I/O locations). The remainder are
used when a driver needs temporary access to data that is transient or
belongs to a process. The PhysToVirt service is particularly interesting,
because it permits a driver which is executing in real mode to address
memory locations above the 1Mb boundary.

If the driver is running on a PC/AT(R) or compatible and has to reach
extended memory in real mode, PhysToVirt uses the undocumented LOADALL
instruction to set the CPU's "shadow" addressing registers, which are
normally loaded invisibly from a descriptor table when a segment register
is loaded, so as to allow the memory access. On a PS/2, PhysToVirt takes
advantage of hardware support for faster mode switching and simply
switches the CPU into protected mode, providing the driver with a selector
to the memory. When the driver is finished with the virtual address it
obtained from PhysToVirt, it calls the DevHlp UnPhysToVirt so that the
selector can be reused, at which time the kernel will also restore the
original CPU mode if necessary.

Finally, for cases in which a memory address is passed to a driver in an
IOCTL data packet or by some other channel of communication, the OS/2
kernel provides the DevHlp services Lock, Unlock, and VerifyAccess. The
driver first calls VerifyAccess with the selector, offset, and length of
the memory involved in the requested operation to make sure that the
application is really entitled to access the memory. If VerifyAccess
succeeds, the driver calls the Lock function to immobilize the segment
and VirtToPhys to obtain the physical address, and then proceeds with the
I/O operation, calling Unlock afterwards to put the segment back under
control of the system's memory manager.

Monitor Management

Under MS-DOS, terminate-and-stay-resident (TSR) utilities have been
tremendous commercial successes. Such utilities intercept keystrokes at
the hardware or BIOS level while other applications are running and take
some special action when a particular key, called a hot key, is detected.
In order to make such utilities possible in protected mode, OS/2 allows
applications to register as a monitor for a specific character device,
if supported by the device driver. The raw data stream from the device is
then passed through a pair of buffers belonging to the application before
it is transferred from the driver to the kernel; the application can
delete, add, or remap characters in the data stream as it chooses or
simply watch for its hot key.

As you might expect, a driver's support for monitors involves a rather
complicated interplay of calls from the application to the kernel, the
kernel to the driver, and the driver to the kernel, and requires a high
degree of cooperation among the three to preserve the integrity of the
system. A special kernel module, called the monitor dispatcher,
arbitrates among the three components and takes care of moving data from
the driver through the buffers of one or more monitors and then back to
the driver.

An application registers as a monitor for a character device by calling
DosMonOpen with the logical name of the device. It then calls DosMonReg
with the monitor handle returned by DosMonOpen and the addresses of two
buffers that will be used for input and output of data packets from the
driver. If registration succeeds, the application becomes an integral
part of the data stream and must use the functions DosMonRead and
DosMonWrite to pass the character data through its buffers in a timely
fashion. The application uses the function DosMonClose to terminate its
connection with the device driver, after which it is removed from the
data stream.

From the driver's point of view, it informs the kernel that it is prepared
to support monitors by issuing the DevHlp call MonitorCreate, which
establishes an initial "empty chain" of device monitors and assigns a
monitor handle to it. The driver may choose to create the monitor chain
during its initialization or wait until there is a need for it. The
driver is informed that an application is attempting to register as a
monitor by the receipt of a special IOCTL request packet (Category 0AH
Function 40H); it then calls the DevHlp service Register, which causes the
kernel's monitor dispatcher to insert the application's buffers into the
monitor chain at the appropriate point.

Once the monitor chain is created, the driver must pass each character it
receives from the device through the chain by calling the DevHlp service
MonWrite. A notification routine within the driver is called by the
kernel's monitor dispatcher when data emerges from the end of the monitor
chain. The driver passes the data onward to the kernel in the usual
manner. It is then distributed to applications via the API function

When the driver is informed by the kernel that an application has issued
DosMonClose, the driver calls the DevHlp service DeRegister with the
application's PID, resulting in the monitor dispatcher removing the
application's buffers from the monitor chain. When the driver notes that
all monitor connections are closed, it can call the DevHlp MonitorCreate
again with a parameter that causes the monitor chain to be destroyed.

Other Services

The DevHlp services listed under semaphore management in Figure 7 allow a
driver to communicate directly with an application via a system
semaphore. Of course, a driver cannot create or open a system semaphore
using the normal API calls, but if an application passes the handle for a
system semaphore to the driver in an IOCTL packet, the driver can use the
DevHlp service SemHandle at task time to obtain a new handle for the
semaphore for use at interrupt time. The DevHlp functions SemRequest and
SemClear, which the driver can call to obtain or release ownership of
the semaphore, are the equivalents of the API functions DosSemRequest
and DosSemClear.

The Timer DevHlps assist a driver in performing polled I/O without
degrading the system for devices that do not support interrupts or permit
a driver to set up a watchdog timer to detect lost interrupts or I/O
operations that never complete. The function SetTimer is called with the
address of a routine within the driver that should be entered on each
timer tick; this is the equivalent of chaining onto Interrupt 1CH (the
ROM BIOS timer tick vector) under MS-DOS. The DevHlp service TickCount is
a more general version of SetTimer; it also allows the number of ticks
between entries to the driver's timer tick handler to be specified. The
driver can remove its timer tick handler from the system's list of all
such handlers by calling ResetTimer.

The other device helper services which have not already been listed play
minor roles, and are present mostly for convenience. For instance, the
DevHlps listed under Request Packet Management in Figure 7 assist the
driver in maintaining and sorting a linked list of request headers for
pending I/O operations, but do not provide any functionality that the
driver couldn't do for itself with a little extra code. Similarly, the
DevHlps listed under Character Buffer Management provide simple support
for a ring buffer of characters that are received or transmitted from the

A Skeleton Device Driver

As an illustration of the essential elements of an OS/2 device driver,
TEMPLATE.ASM (see Figure 8) has the source code for a skeleton character
driver called TEMPLATE.SYS. This driver performs no useful function; it
simply returns a success code for all request packets that the kernel
passes to it. However, TEMPLATE demonstrates the segment ordering and
naming conventions that should be used in an installable driver, the use
of API calls at initialization time, and a method by which initialization
code and data can be discarded to minimize a driver's memory
requirements. TEMPLATE can serve as a starting point for your own
installable device drivers.

In order to assemble and link TEMPLATE.SYS, you use the source file
TEMPLATE.ASM, the module definition file TEMPLATE.DEF (see Figure 9),
the Microsoft Macro Assembler, and the Microsoft Segmented Object Linker.
Enter the commands shown in Figure 10.

If the assembly and link are successful, you can copy the file
TEMPLATE.SYS to the root directory of your boot disk, add
DEVICE=TEMPLATE.SYS to the CONFIG.SYS file, and then restart the system
to see the TEMPLATE driver's sign-on message.


OS/2 device drivers are architecturally and functionally far more complex
than their MS-DOS ancestors, principally because they need to support
bimodal operation and fully interrupt-driven I/O. This article has
provided a brief overview of OS/2 device driver structure and

Figure 1:  Structure of an OS/2 installable device driver file. OS/2 drivers
           are linked as small-model EXE files, containing one code and one
           data segment. By convention, driver files always have the
           extension .SYS. Note that code and data used only by the driver's
           initialization routine can be positioned at the segments and
           discarded after driver initialization is completed to minimize the
           driver's memory overhead.

            Start  ╔═════════════════════════════════╗ ──────┐
            of file║         EXE file header         ║       │
                   ╠═════════════════════════════════╣       │
                   ║       Device driver header      ║    Driver
                   ╠═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═╣     data
                   ║                                 ║    segment
                   ║       Resident driver data      ║       │
                   ║                                 ║       │
                   ╠═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═╣ ══════╡
                   ║ Initialization data (discarded) ║       │
                   ║                                 ║       │
                   ╠═════════════════════════════════╣    Driver
                   ║      Resident driver code       ║     code
                   ║                                 ║    segment
            End    ╠═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═╣       │
            of file║ Initialization code (discarded) ║       │
                   ╚═════════════════════════════════╝ ──────┘

Figure 2:  OS/2 device driver header.

In character device drivers, the name or units field contains the logical
name of the device, left-justified and padded with spaces; in block device
drivers, the first byte of the field contains the number of logical units
supported by the driver (filled in by OS/2), followed by 7 bytes of reserved

       │    link to next driver header, offset    │
       │   link to next driver header, selector   │
       │          device attribute word           │
       │       offset, strategy entry point       │
       │                 reserved                 │
       │                                          │
       │         name or units (8 bytes)          │
       │                                          │
       │                                          │
       │            reserved (8 bytes)            │
       │                                          │

Figure 3:  The device attribute word in the device driver header, which
           describes the characteristics and capabilities of the associated

Bit(s)    Meaning

15        = 1 if character device
          = 0 if block device

14        = 0 (reserved)

13        = 1 if non-IBM-compatible format (block devices)
          = 0 if IBM-compatible format (that is, FAT ID byte is valid)

12        = 1 if device name is not to be protected by "sharer" module
             (character devices only)
Bit(s)    Meaning
             (character devices only)
          = 0 if sharer controls contention for the device

11        = 1 if Device Open/Close functions (character devices) or
             Removable Media function (block devices) supported
          = 0 if Device Open/Close/Removable Media not supported

10        = 0 (reserved)

7-9       = function level, 001 to indicate OS/2 device driver

4-6       = 0 (reserved)

3         = 1 if current CLOCK device

2         = 1 if current NUL device

1         = 1 if current standard output device (stdout)

0         = 1 if current standard input device (stdin)
Bit(s)    Meaning
0         = 1 if current standard input device (stdin)

Figure 4:  A prototype OS/2 device driver request header.

The OS/2 kernel requexts a driver operation by passing the bimodal address
of a request header containing operation-specific parameters, and the driver
communicates the results of the operation back to the kernel by placing
status and other information in the same request header. In order to support
multiple concurrent I/O requests, the driver maintains a private queue of
request headers for its pending operations. This is done with the aid of the
kernel DevHlp functions.

       │         length of request header         │
       │         block device unit number         │
       │    command code (driver subfunction)     │
       │             returned status              │
       │                                          │
       │         reserved area (4 bytes)          │
       │                                          │
       │         queue linkage (4 bytes)          │
       │                                          │
       │                                          │
       │  command-specific data (length varies)   │
       │                                          │

Figure 5:  Command Codes and Their Associated Driver Functions

Code     Subfunction Name   Subfunction Action
Code     Subfunction Name   Subfunction Action

0        Init               Initialize driver (called at driver load time).
                            Tests for existence and operability of
                            peripheral device register interrupt handler and
                            returns size of resident code and data segments
                            to kernel. Block drivers also return number of
                            units supported and an array of pointers to BIOS
                            Parameter Blocks (BPBs).

1        Media Check        Returns a code indicating whether the media has
                            been changed.

2        Build BPB          Returns a pointer to the BIOS Parameter Block
                            for the indicated disk type. The BPB for a disk
                            describes its physical characteristics (sector
                            size, number of tracks, tracks per sector, and
                            so on).

3        Reserved
Code     Subfunction Name   Subfunction Action
3        Reserved

4        Read               Transfers data from the device to memory.

5        NonDestructive     Returns the next character from the device, if
         Read               one is available, without removing it from the
                            input buffer.

6        Input Status       Returns a flag indicating whether characters are
                            waiting in the device's input buffer.

7        Input Flush        Discards all data waiting in the device's input
                            buffer and cancels pending read requests.

8        Write              Transfers data from memory to the device.

9        Write with Verify  Transfers data from memory to the device,
                            performing a read-after-write verification if
Code     Subfunction Name   Subfunction Action

10       Output Status      Returns a flag indicating whether characters are
                            waiting in the device's output buffer.

11       Output Flush       Discards all data waiting in the device's output
                            buffer and cancels pending write requests.

12       Reserved

13       Device Open        Initializes a character device for subsequent
                            I/O if necessary.

14       Device Close       Performs any necessary post-I/O processing (such
                            as transmitting a form feed to a list device).

15       Removable Media    Returns a code indicating whether the media is

Code     Subfunction Name   Subfunction Action

16       Generic IOCTL      General-purpose mechanism for communication
                            between the driver and application programs.

17       Reset Media        Called by the kernel to acknowledge and clear
                            the "uncertain media" error condition previously
                            returned by the driver.

18       Get Logical Drive  Returns the drive code for the logical unit
         Map                currently mapped to the specified physical unit.

19       Set Logical Drive  Notifies the driver which logical unit will be
         Map                used for the next access to the specified
                            physical unit.

20       DeInstall Driver   Signals the driver to release its interrupt
                            vectors and memory. Called when a character
                            device driver is superseded by the loading of
                            another driver with the same logical device
Code     Subfunction Name   Subfunction Action
                            another driver with the same logical device

21       Reserved

22       Partitionable      Returns the number of partitionable fixed disks
         Fixed Disk         supported by the driver.

23       Get Fixed Disk     Returns the identities of the logical disks, if
         Map                any, supported by driver on the specified hard

24-26    Reserved

Figure 6:  Example of a DevHlp call. This code allocates 64Kb of physical
           memory above the 1Mb boundary for use by the driver. When the
           driver wishes to access the memory block, it must use PhysToVirt
           or PhysToUVirt to convert the physical address stored in 'bufptr'
           into an appropriate virtual address for the current CPU mode.

devhlp  dd      ?       ; contains bimodal pointer
                        ; to DevHlp common entry point
                        ; (from Init routine)

bufptr  dd      ?       ; receives 32-bit physical
                        ; address of allocated block

                        ; allocate 64Kb of memory
                        ; above 1Mb for use by driver

                        ; AX:BX = size in bytes
        mov     ax,1    ; AX = high word
        mov     bx,0    ; BX = low word
        mov     dh,0    ; 0 = above 1Mb boundary
        mov     dl,18h  ; DevHlp 18H = AllocPhys

        call    devhlp  ; indirect call to DevHlp
                        ; common entry point

        jc      error   ; jump if allocation failed

                        ; save 32-bit physical
                        ; address of memory block
        mov     word ptr bufptr,bx
        mov     word ptr bufptr+2,ax

Figure 7:  DevHlp services by category, their function numbers, and the
           driver contents in which they may be used (K = kernel mode, I =
           interrupt mode, U = user mode, N = initialization mode).

DevHlp               DevHlp             DevHlp
Name                 Code   K  I  U  N  Action
DevHlp               DevHlp             DevHlp
Name                 Code   K  I  U  N  Action

Process Management

DevDone              01H    K  I        Signals that the specified operation
                                        is complete. Kernel unblocks any
                                        threads waiting for the operation.

Block                04H    K     U     Blocks the calling thread until a
                                        timeout occurs of thread is
                                        unblocked by Run.

Run                  05H    K  I  U     Activates a thread that was
                                        previously suspended by a call to

Yield                02H    K           Surrenders the CPU to any other
                                        threads of the same or higher
                                        priority that are ready to execute.

DevHlp               DevHlp             DevHlp
Name                 Code   K  I  U  N  Action

TCYield              03H    K           Similar to Yield, but allows other
                                        threads to execute only if they have
                                        a time-critical priority.

Memory Management

AllocPhys            18H    K        N  Allocates a block of fixed (not
                                        swappable or movable) memory. Caller
                                        can specify whether the block should
                                        be above or below the 1Mb boundary.

FreePhys             19H    K        N  Releases a block of memory
                                        previously allocated with AllocPhys.

Lock                 13H    K        N  Locks a memory segment, that is,
                                        flags the segment as nonmovable and

DevHlp               DevHlp             DevHlp
Name                 Code   K  I  U  N  Action

Unlock               14H    K  I     N  Unlocks a memory segment.

Verify Access        27H    K           Verifies that a user process has
                                        access to a segment.

PhysToVirt           15H    K  I     N  Converts a 32-bit physical (linear)
                                        address to a virtual address.

UnPhysToVirt         32H    K  I     N  Signals that the virtual addresses
                                        previously obtained with PhysToVirt
                                        can be reused.

PhysToUVirt          17H    K           Converts a 32-bit physical address
                                        to a virtual address accessed
                                        through the current Local Descriptor
                                        Table (LDT).

VirtToPhys           16H    K        N  Converts a virtual address
DevHlp               DevHlp             DevHlp
Name                 Code   K  I  U  N  Action
VirtToPhys           16H    K        N  Converts a virtual address
                                        (segment:offset or selector:offset)
                                        to a 32-bit physical address.

AllocateGDTSelector  2DH    K        N  Allocate one or more GDT selectors
                                        for use by the driver.

PhysToGDTSelector    2EH    K        N  Map a physical address and length
                                        onto a GDT selector.

RealToProt           2FH       I        Switch the CPU into protected mode.

ProtToReal           30H       I        Switch the CPU into real mode.

Hardware Management

SetIRQ               1BH    K        N  Sets the interrupt vector for the
                                        indicated interrupt request (IRQ)
                                        level to point to the driver's
DevHlp               DevHlp             DevHlp
Name                 Code   K  I  U  N  Action
                                        level to point to the driver's
                                        interrupt handler.

UnSetIRQ             1CH    K  I     N  Releases the interrupt vector for
                                        the specifies IRQ level.

EOI                  31H       I     N  Issues an end of interrupt to the
                                        appropriate 8259 PIC for the
                                        specified IRQ level.

Request Packet Management

PushReqPacket        09H    K           Adds a request packet to the
                                        driver's linked list of packets.

SortReqPacket        0CH    K           Inserts a request packet into the
                                        driver's linked list of packets,
                                        sorted by sector number.

DevHlp               DevHlp             DevHlp
Name                 Code   K  I  U  N  Action

PullReqPacket        0AH    K  I        Removes the oldest request packet
                                        from the driver's linked list.

AllocReqPacket       0DH    K           Allocates a block of memory large
                                        enough to hold the largest possible
                                        request packet and returns a bimodal

FreeReqPacket        0EH    K           Releases a request packet previously
                                        allocated with AllocReqPacket.

Character Buffer Management

QueueInit            0FH    K  I  U  N  Establishes a ring buffer for
                                        storage of characters and
                                        initializes its pointers.

QueueRead            12H    K  I  U     Returns and removes a character from
DevHlp               DevHlp             DevHlp
Name                 Code   K  I  U  N  Action
QueueRead            12H    K  I  U     Returns and removes a character from
                                        the specified buffer.

QueueWrite           11H    K  I  U     Puts a character into the specified

QueueFlush           10H    K  I  U     Resets the pointers to the specified

Timer Management

SetTimer             1DH    K        N  Adds a timer tick handler to be
                                        called on every timer tick to the
                                        system's list of such handlers.

TickCount            33H    K  I  U  N  Adds a timer tick handler to be
                                        called at specified intervals to the
                                        system's list of such handlers, or
                                        modifies the interval at which an
DevHlp               DevHlp             DevHlp
Name                 Code   K  I  U  N  Action
                                        modifies the interval at which an
                                        existing handler is called.

ResetTimer           1EH    K  I     N  Removes a timer tick handler from
                                        the system's list of such handlers.

Monitor Management

MonitorCreate        1FH    K        N  Creates or removes an empty monitor

Register             20H    K           Adds a process to a monitor chain.

MonWrite             22H    K  I  U     Passes a data record to a monitor
                                        chain for filtering.

MonFlush             23H    K           Removes all data from a monitor

DevHlp               DevHlp             DevHlp
Name                 Code   K  I  U  N  Action

DeRegister           21H    K           Removes a jprocess from a monitor

Semaphore Management

SemHandle            08H    K  I        Converts a process's handle for a
                                        system semaphore to a virtual
                                        address that the driver can use at
                                        interrupt time.

SemRequest           06H    K     U     Claims (sets) a semaphore. If the
                                        semaphore is already set, blocks the
                                        thread until the semaphore is

SemClear             07H    K  I  U     Clears a semaphore, restarting any
                                        threads that were blocking on the
DevHlp               DevHlp             DevHlp
Name                 Code   K  I  U  N  Action

Miscellaneous Device Helpers

SchedClockAddr       00H    K        N  Obtains the address of the kernel's
                                        routine to be called on each clock
                                        tick. Used only by the system's
                                        clock driver.

SendEvent            25H    K  I        Signals the kernal that a critical
                                        event has been detected, such as the
                                        Session Manager hot key or a Ctrl-

GetDOSVar            24H    K        N  Returns a bimodal pointer to a table
                                        of useful kernel addresses and

SetROMVector         1AH    K        N  Captures an interrupt vector
DevHlp               DevHlp             DevHlp
Name                 Code   K  I  U  N  Action
SetROMVector         1AH    K        N  Captures an interrupt vector
                                        normally used in real mode to invoke
                                        a ROM BIOS function, returning the
                                        previous contents of the vector for

ROMCritSection       26H          U     Protects ROM BIOS code from
                                        interrupts that might cause a
                                        cantext switch. Called by a driver
                                        in user mode.

ABIOS Communication Device Helpers (IBM PS/2 only)

ABIOSCall            36H    K  I  U  N  Invokes an Advanced BIOS (ABIOS)
                                        service on behalf of the driver.

ABIOSCommonEntry     37H    K  I  U  N  Transfers to an ABIOS common entry
                                        point on behalf of the driver.

DevHlp               DevHlp             DevHlp
Name                 Code   K  I  U  N  Action

GetLIDEntry          34H    K        N  Obtains a logical ID (LID) for a
                                        physical device for use with
                                        subsequent ABIOS calls.

FreeLIDEntry         35H    K        N  Releases a Logical ID (LID) for a
                                        physical device.

Figure 8:  Installable Driver Template

name    template
page    55,132
title   'TEMPLATE - installable driver template'

; TEMPLATE.ASM:  A program skeleton for an installable
;                OS/2 character device driver.
; The driver command code routines are stubs only and have
; no effect but to return a nonerror "Done" status.

maxcmd  equ     26              ; maximum allowed command code

stdin   equ     0               ; standard device handles
stdout  equ     1
stderr  equ     2

cr      equ     0dh             ; ASCII carriage return
lf      equ     0ah             ; ASCII line feed

        extrn   DOSWRITE:far

DGROUP  group   _DATA

_DATA   segment word public 'DATA'

                                ; device driver header...
header  dd      -1              ; link to next device driver
        dw      8880h           ; device attribute word
        dw      Strat           ; "Strategy" routine entry point
        dw      0               ; (reserved)
        db      'TEMPLATE'      ; logical device name
        db      8 dup (0)       ; (reserved)

devhlp  dd      ?               ; DevHlp entry point

wlen    dw      ?               ; receives DOSWRITE length
                                ; Strategy routine dispatch table
                                ; for request packet command code
dispch  dw      Init            ; 0  = initialize driver
        dw      MediaChk        ; 1  = media check on block device
        dw      BuildBPB        ; 2  = build BIOS parameter block
        dw      Error           ; 3  = not used
        dw      Read            ; 4  = read (input) from device
        dw      NdRead          ; 5  = nondestructive read
        dw      InpStat         ; 6  = return input status
        dw      InpFlush        ; 7  = flush device input buffers
        dw      Write           ; 8  = write (output) to device
        dw      WriteVfy        ; 9  = write with verify
        dw      OutStat         ; 10 = return output status
        dw      OutFlush        ; 11 = flush output buffers
        dw      Error           ; 12 = not used
        dw      DevOpen         ; 13 = device open
        dw      DevClose        ; 14 = device close
        dw      RemMedia        ; 15 = removable media
        dw      GenIOCTL        ; 16 = generic IOCTL
        dw      ResetMed        ; 17 = reset media
        dw      GetLogDrv       ; 18 = get logical drive
        dw      SetLogDrv       ; 19 = set logical drive
        dw      DeInstall       ; 20 = deinstall
        dw      PortAcc .       ; 21 = reserved
        dw      PartFD          ; 22 = partitionable fixed disks
        dw      FDMap           ; 23 = get fixed disk unit map
        dw      Error           ; 24 = not used
        dw      Error           ; 25 = not used
        dw      Error           ; 26 = not used

ident   db      cr,lf,lf
        db      'TEMPLATE Example OS/2 Device Driver'
        db      cr,lf

ident_len equ $-ident

_DATA   ends

_TEXT   segment word public 'CODE'

        assume  cs:_TEXT,ds:DGROUP,es:NOTHING

Strat   proc    far             ; device driver Strategy routine,
                                ; called by OS/2 kernel with
                                ; ES:BX = address of request packet

        mov     di,es:[bx+2]    ; get command code from packet
        and     di,0ffh
        cmp     di,maxcmd       ; supported by this driver?
        jle     Strat1          ; jump if command code OK

        call    Error           ; bad command code
        jmp     Strat2

Strat1: add     di,di           ; command code * 2
                                ; branch to appropriate routine
        call    word ptr [di+dispch]

Strat2:                         ; store subfunction status in
        mov     es:[bx+3],ax    ; AX into request packet

        ret                     ; back to OS/2 kernel

Strat   endp

Intr    proc  far               ; driver Interrupt handler

        clc                     ; signal that we owned interrupt


Intr    endp

; Command code routines are called by the Strategy routine
; via the Dispatch table with ES:BX pointing to the request
; header.  Each routine should return ES:BX unchanged,
; and AX = status to be placed in request packet:
;       0100H if 'done' and no error
;       0000H if thread should block pending interrupt
;       81xxH if 'done' and error detected (xx=error code)

MediaChk proc   near            ; function 1 = Media Check

        mov     ax,0100h        ; return 'done' status

MediaChk endp

BuildBPB proc   near            ; function 2 = Build BPB

        mov     ax,0100h        ; return 'done' status

BuildBPB endp

Read    proc    near            ; function 4 = Read (Input)

        mov     ax,0100h        ; return 'done' status

Read    endp

NdRead  proc    near            ; function 5 = Nondestructive Read

        mov     ax,0100h        ; return 'done' status

NdRead  endp

InpStat proc    near            ; function 6 = Input Status

        mov     ax,0100h        ; return 'done' status

InpStat endp

InpFlush proc   near            ; function 7 = Flush Input Buffers

        mov     ax,0100h        ; return 'done' status

InpFlush endp

Write   proc    near            ; function 8 = Write (Output)

        mov     ax,0100h        ; return 'done' status

Write   endp

WriteVfy proc   near            ; function 9 = Write with Verify

        mov     ax,0100h        ; return 'done' status

WriteVfy endp

OutStat proc    near            ; function 10 = Output Status

        mov     ax,0100h        ; return 'done' status

OutStat endp

OutFlush proc   near            ; function 11 = Flush Output Buffers

        mov     ax,0100h        ; return 'done' status

OutFlush endp

DevOpen proc    near            ; function 13 = Device Open

        mov     ax,0100h        ; return 'done' status

DevOpen endp

DevClose proc   near            ; function 14 = Device Close

        mov     ax,0100h        ; return 'done' status

DevClose endp

RemMedia proc   near            ; function 15 = Removable Media

        mov     ax,0100h        ; return 'done' status

RemMedia endp

GenIOCTL proc   near            ; function 16 = Generic IOCTL

        mov     ax,0100h        ; return 'done' status

GenIOCTL endp

ResetMed proc   near            ; function 17 = Reset Media

        mov     ax,0100h        ; return 'done' status

ResetMed endp

GetLogDrv proc  near            ; function 18 = Get Logical Drive

        mov     ax,0100h        ; return 'done' status

GetLogDrv endp

SetLogDrv proc  near            ; function 19 = Set Logical Drive

        mov     ax,0100h        ; return 'done' status

SetLogDrv endp

DeInstall proc  near            ; function 20 = DeInstall Driver

        mov     ax,0100h        ; return 'done' status

DeInstall endp

PartFD  proc    near            ; function 22 = Partitionable
                                ;               Fixed Disks
        mov     ax,0100h        ; return 'done' status

PartFD  endp

FDMap   proc    near            ; function 23 = Get Fixed Disk
                                ;               Logical Unit Map
        mov     ax,0100h        ; return 'done' status

FDMap   endp

Error   proc    near            ; bad command code in request header

        mov     ax,8103h        ; error bit + 'done' status
                                ; + "Unknown Command" code

Error   endp

Init    proc    near            ; function 0 = Initialize Driver

        mov     ax,es:[bx+14]   ; pick up DevHlp entry point
        mov     word ptr devhlp,ax
        mov     ax,es:[bx+16]
        mov     word ptr devhlp+2,ax

                                ; set offsets to end of code
                                ; and data segments
        mov     word ptr es:[bx+14],offset _TEXT:Init
        mov     word ptr es:[bx+16],offset DGROUP:ident

                                ; display sign-on message...
        push    stdout          ; handle for standard output
        push    ds              ; address of message
        push    offset DGROUP:ident
        push    ident_len       ; length of message
        push    ds              ; receives bytes written
        push    offset DGROUP:wlen
        call    DOSWRITE

        mov     ax,0100h        ; return 'done' status

Init    endp

_TEXT   ends


Figure 9:  Sample Module Definition File

; module definition file for
; TEMPLATE OS/2 character device driver



Summary of the way Device Drivers Queue and Process I/O Requests.

        ┌─────────────────┐                   ┌────────────────────┐
        │   OS/2 Kernal   │                   │ Hardware Interrupt │
        └────────┬────────┘                   └──────────┬─────────┘
    ┌────────────▼────────────┐             ┌────────────▼─────────────┐
    │ ES:BX ────► I/O Request │             │  OS/2 Interrupt Manager  │
    └────────────┬────────────┘             └────────────┬─────────────┘
    ┌────────────▼────────────┐             ┌────────────▼─────────────┐
    │ Device Strategy Routine │             │ Device Interrupt Routine │
    └────────────┬────────────┘             └────────────┬─────────────┘
    ┌────────────▼────────────┐             ┌────────────▼─────────────┐
    │Call DevHlp PushReqPacket│             │Service Hardware Interrupt│
    └──────┬──────────────────┘             └────────────┬─────────────┘
    ┌──────▼────────┐   ┌────┐              ┌────────────▼───────┐
    │Is Device Idle?├──►│ No ├┐             │Is Request Complete │  ┌────┐
    └──────┬────────┘   └────┘│        ┌────┤      or Error      ├─►│ No ├┐
        ┌──▼──┐               │        │    └────────────────────┘  └────┘│
        │ Yes │               │     ┌──▼──┐      ┌─────────────────┐      │
        └──┬──┘               │     │ Yes ├─────►│Set Packet Status│      │
           ▼                  │     └─────┘      └───────┬─────────┘      │
    Call StartNextRequest     │              ┌───────────▼─────────┐      │
           │◄─────────────────┘      ┌───────┤ Call DevHlp DevDone │      │
    ┌──────┴─────────┐               │       └─────────────────────┘      │
    │   Far Return   │               ▼              ┌────────────────┐    │
    └────────────────┘   Call StartNextRequest─────►│   Far Return   │◄───┘

                          │      Call DevHlp      │
                          │     Pull ReqPacket    │
                          │    Start Hardware     │
                          │ Processing of Request │
                             │   Far Return   │


Exploring the Structure and Contents of the MS-DOS Object Module Format☼

Richard Wilton

A programmer writing a compiler, an assembler, or a language translator
must know the details of object module format and processing. To take
advantage of LIB and LINK, a language translator must construct object
modules that conform to the format and usage conventions specified by

Although some MS-DOS language translators generate executable 8086-
family machine code directly from source code, most produce object code
instead. Typically, a translator processes each file of source code
individually and leaves the resulting object module in a separate file
bearing an .OBJ extension. The source-code files themselves remain

After all of a program's source-code modules have been translated, the
resulting object modules can be linked into a single executable program.
Because object modules often represent only a portion of a complete
program, each source-code module usually contains instructions that
indicate how its corresponding object code is to be combined with the
object code in other object modules when they are linked.

The object code contained in each object module consists of a binary image
of the program plus program structure information. This object code is
not directly executable. The binary image corresponds to the executable
code that will ultimately be loaded into memory for execution; it
contains both machine code and program data. The program structure
information includes descriptions of logical groupings defined in the
source code (such as named subroutines or segments) and symbolic
references to addresses in other object modules.

The program structure information is used by a linkage editor, or
linker, such as Microsoft LINK to edit the binary image of the program
contained in the object module. The linker combines the binary images
from one or more object modules into a complete executable program.

The linker's output is an EXE file-a file containing machine code that can
be loaded into RAM and executed (see Figure 1). The linker leaves intact
all of the object modules it processes.

Object code thus serves as an intermediate form for compiled programs.
This form offers two major advantages:

  ■  Modular intermediate code. The use of object modules eliminates the
     overhead of repeated compilation of an entire program whenever
     changes are made to parts of its source code. Instead, only those
     object modules affected by source-code revisions need be

  ■  Shareable format. Object module format is well defined, so object
     modules can be linked even if they were produced by different
     translators. Many high-level-language (HLL) compilers take advantage
     of this commonality of object-code format to support
     "interlanguage" linkage.

Object Module Contents

Object modules contain five basic types of information. Some of this
information exists explicitly in the source code (and is subsequently
passed on to the object module), but much is inferred by the program
translator from the structure of the source code and the way memory is
accessed by the 8086.

Binary image. As described earlier, the binary image comprises executable
code (such as opcodes and addresses) and program data. When object
modules are linked, the linker builds an executable program from the
binary image in each object module it processes. The binary image in each
object module is always associated with program structure information
that tells the linker how to combine it with related binary images in
other object modules.

External references. Since an object module generally represents only a
small portion of a larger program that will be constructed from several
object modules, it usually contains symbols that permit it to be linked
to the other modules. Such references to corresponding symbols in
other object modules are resolved when the modules are linked.

For example, consider the following short C program:

      puts("Hello, world\n");

This program calls the C function puts() to display a character string,
but puts() is not defined in the source code. Rather, the name puts is a
reference to a function that is external to the program's main()
routine. When the C compiler generates an object module for this program,
it will identify puts as an external reference. Later, the linker will
resolve the external reference by linking the object module containing the
puts() routine with the module containing the main() routine.

Address references. When a program is built from a group of object
modules, the actual values of many addresses cannot be computed until
the linker combines the binary image of executable code and the program
data from each of the program's constituent object modules. Object modules
contain information that tells the linker how to resolve the values of
such addresses, either symbolically (as in the case of external
references) or relatively, in terms of some other address (such as the
beginning of a block of executable code or program data).

Debugging information. An object module can also contain information that
relates addresses in the executable program to the corresponding source
code. After the linker performs its address fixups, it can use the object
module's debugging information to relate a line of source code in a
program module to the executable code that corresponds to it.

Miscellaneous information. Finally, an object module can contain comments,
lists of symbols defined in or referenced by the module, module
identification information, and information for use by an object
library manager or a linker (for example, the names of object libraries
to be searched by default).


When the linker generates an executable program, it organizes the
structural components of the program according to the information
contained in the object modules. The layout of the executable program can
be conceptually described as a run-time memory map after it has been
loaded into memory.

The basic structure of every executable program for the 8086 family of
microprocessors must conform to the segmented architecture of the
microprocessor. Thus, the run-time memory map of an executable program
is partitioned into segments, each of which can be addressed by using one
of the microprocessor's segment registers. This segmented structure of
8086-based programs is the basis for most of the following

Frames. The memory address space of the 8086 is conceptually divided into
a sequence of paragraph-aligned, overlapping 64Kb regions called frames.
Frame 0 in the 8086's address space is the 64Kb of memory starting at
physical address 00000H (0000:0000 in segment:offset notation), frame 1 is
the 64Kb of memory starting at 00010H (0001:0000), and so forth. A frame
number thus denotes the beginning of any paragraph-aligned 64Kb of
memory. For example, the location of a 64Kb buffer that starts at address
B800:0000 can be specified as frame 0B800H.

Logical segments. The run-time memory map for every 8086 program is
partitioned into one or more logical segments, which are groupings of
logically related portions of the program. Typically, an MS-DOS program
includes at least one code segment (that contains all of the program's
executable code), one or more data segments (that contain program data),
and one stack segment.

When a program is loaded into RAM to be executed, each logical segment in
the program can be addressed with a frame number──that is, a physical 8086
segment address. Before the MS-DOS loader transfers control to a program
in memory, it initializes the CS and SS registers with the segment
addresses of the program's executable code and stack segments. If an
MS-DOS program has a separate logical segment for program data, the
program itself usually stores this segment's address in the DS register.

Relocatable segments. In MS-DOS programs, most logical segments are
relocatable. The loader determines the physical addresses of a program's
relocatable segments when it places the program into memory to be
executed. However, this address determination poses a problem for the MS-
DOS loader, because a program may contain references to the address of a
relocatable segment even though the address value is not determined until
the program is loaded.

The problem is solved by indicating where such references occur within
the program's object modules. The linker then extracts this information
from the object modules and uses it to build a list of such address
references into a segment relocation table in the header of executable
files. After the loader copies a program into memory for execution, it
uses the segment relocation table to update, or fix up, the segment
address references within the program.

Consider the example in Figure 2, in which a program loads the starting
addresses of two data segments into the DS and ES segment registers.

The actual addresses of the _DATA and FAR_DATA segments are unknown when
the source code is assembled and the corresponding object module is
constructed. The assembler indicates this by including segment fixup
information, instead of actual segment addresses, in the program's
object module. When the object module is linked, the linker builds this
segment fixup information into the segment relocation table in the
header of the program's EXE file. Then, when the EXE file is loaded, the
MS-DOS loader uses the information in the EXE file's header to patch the
actual address values into the program.

Absolute segments. Sometimes a program needs to address a predetermined
segment of memory. In this case, the program's source code must declare
an absolute segment so that a reference to the corresponding frame number
can be built into the program's object module.

For example, a program might need to address a video display buffer
located at a specific physical address. The following assembler
directive declares the name of the segment and its frame number:

  VideoBufferSegSEGMENT at 0B800h

Segment alignment. When a program is loaded, the physical address of each
logical segment is constrained by the segment's alignment. A segment can
be page aligned (aligned on a 256-byte boundary), paragraph aligned
(aligned on a 16-byte paragraph boundary), word aligned (aligned on an
even-byte boundary), or byte aligned (not aligned on any particular
boundary). A specification of each segment's alignment is part of every
object module's program structure information.

High-level-language translators generally align segments according to
the type of data they contain. For example, executable code segments are
usually byte aligned; program data segments are usually word aligned. With
an assembler, segment alignment can be specified with the SEGMENT
directive and the assembler will build this information into the
program's object module.

Concatenated segments. The linker can concatenate logical segments from
different object modules when it builds the executable program. For
example, several object modules may each contain part of a program's
executable code. When the linker processes these object modules, it can
concatenate the executable code from the different object modules into
one range of contiguous addresses.

The order in which the linker concatenates logical segments in the
executable program is determined by the order in which the linker
processes its input files and by the program structure information in the
object modules. With a high-level-language translator, the translator
infers which segments can be concatenated from the structure of the source
code and proceeds to build appropriate segment concatenation information
into the object modules it generates. With an assembler, the segment class
type can be used to indicate which segments can be concatenated.

Groups of segments. Segments with different names may also be grouped
together by the linker so that they can all be addressed within the same
64Kb frame, even though they are not concatenated. For example, it might
be desirable to group program data segments and a stack segment within the
same 64Kb frame so that program data items and data on the stack can be
addressed with the same 8086 segment register.

In high-level languages, it is up to the translator to incorporate
appropriate segment grouping information into the object modules it
generates. With an assembler, groups of segments can be declared with the
GROUP directive.

Fixups. Sometimes a compiler or an assembler encounters addresses whose
values cannot be determined from the source code. The addresses of
external symbols are an obvious example. The addresses of relocatable
segments and of labels within those segments are another example.

A fixup is a language translator's way of passing the buck about such
addresses to the linker. Typically, a translator builds a zero value in
the binary image at locations where it cannot store an actual address.
Accompanying each such location is fixup information, which allows the
linker to determine the correct address. The linker then completes the
fixup by calculating the correct address value and adding it to the value
in the corresponding location in the binary image.

The only fixups the linker cannot fully resolve are those that refer to
the segment address of a relocatable segment. Such addresses are not known
until the program is actually loaded, so the linker, in turn, passes the
responsibility to the MS-DOS loader by creating a segment relocation table
in the header of the executable file.

To process fixups properly, the linker needs three pieces of information:
the LOCATION of the value in the object module, the nature of the TARGET
(the address whose value is not yet known), and the FRAME in which the
address calculations are to take place. Object modules contain the
LOCATION, TARGET, and FRAME information the linker uses to calculate
the appropriate address for any given fixup.

Consider the program in Figure 3. The statement:

  start:  call    far ptr FarProc

contains a reference to an address in the logical segment FarSeg2. Because
the assembler does not know the address of FarSeg2, it places fixup
information about the address into the object module. The LOCATION to
be fixed up is 1 byte past the label start (the 4-byte pointer following
the call opcode 9AH). The TARGET is the address referenced in the call
instruction-that is, the label FarProc in the segment FarSeg2. The FRAME
to which the fixup relates is designated by the group FarGroup and is
inferred from the statement

  ASSUME  cs:FarGroup in the FarSeg2 segment.

There are several different ways for a language translator to identify a
fixup. For example, the LOCATION might be a single byte, a 16-bit offset,
or a 32-bit pointer, as in Figure 3. The TARGET might be a label whose
offset is relative either to the base (beginning) of a particular
segment or to the actual LOCATION. The FRAME might be a relocatable
segment, an absolute segment, or a group of segments.

Taken together, all the information in an object module that concerns
the alignment and grouping of segments can be regarded as a specification
of a program's run-time memory map. In effect, the object module
specifies what goes where in memory when a program is loaded. The
linker can then take the program structure information in the object
modules and generate a file containing an executable program with the
corresponding structure.


Although object modules contain the information that ultimately
determines the structure of an executable program, they bear little
structural resemblance to the resulting executable program. Each object
module is made up of a sequence of variable-length object records.
Different types of object records contain different types of program

Each object record begins with a 1-byte field that identifies its type.
This is followed by a 2-byte field containing the length (in bytes) of the
remainder of the record. Next comes the actual structural or program
information, represented in one or more fields of varied lengths.
Finally, each record ends with a 1-byte checksum.

The sequence in which object records appear in an object module is
important. Because the records vary in length, each object module must be
constructed linearly, from start to end. More important, however, is
the fact that some types of object records contain references to
preceding object records. Because the linker processes object records
sequentially, the position of each object record within an object module
depends primarily on the type of information each record contains.

Object Record Types

Microsoft(R) LINK currently recognizes 14 types of object records, each one
of which carries a specific type of information within the object
module. Each type of object record is assigned an identifying six-letter
abbreviation, but these abbreviations are used only in documentation,
not within an object module itself. As already mentioned, the first byte
of each object record contains a value that indicates its type. In a
hexadecimal dump of the contents of an object module, these identifying
bytes identify the start of each object record.

Figure 4 lists the types of object records supported by LINK. The value
of each record's identifying byte (in hexadecimal) is included, along
with the six-letter abbreviation and a brief functional description. The
functions of the 14 types of object records fall into six general

  ■  Binary data (executable code and program data) is contained in the
     LEDATA and LIDATA records.

  ■  Address binding and relocation information is contained in FIXUPP

  ■  The structure of the run-time memory map is indicated by the SEGDEF,
     GRPDEF, COMDEF, and TYPDEF records.

  ■  Symbol names are declared in LNAMES, EXTDEF, and PUBDEF records.

  ■  Debugging information is in the LINNUM record.

  ■  Finally, the structure of the object module itself is determined by
     the THEADR, COMENT, and MODEND records.

Object Record Order

The sequence in which the types of object records appear in an object
module is fairly flexible in some respects. Several record types are
optional, and if the type of information they carry is unnecessary, they
are omitted from an object module. In addition, most object record types
can occur more than once in the same object module. And, because object
records are variable in length, it is often possible to choose, as a
matter of convenience, between combining information into one large
record or breaking it down into several smaller records of the same

As stated previously, one important constraint on the order in which
object records appear is the need for some types of object records to
refer to information contained in other records. Because the linker
processes the records sequentially, object records containing such
information must precede the records that refer to it. For instance, two
types of object records, SEGDEF and GRPDEF, refer to the names contained
in an LNAMES record. Thus, an LNAMES record must appear before a SEGDEF
or GRPDEF record that refers to it so that the names in the LNAMES record
are known to the linker by the time it processes the SEGDEF or GRPDEF

References Between Object Records

Object records can refer to information in other records either
indirectly, by means of implicit references, or directly, by means of
indexed references to names or other records.

Implicit references. Some types of object records implicitly reference
another record in the same object module. The most important example of
such implicit referencing is in the FIXUPP record, which always contains
fixup information for the preceding LEDATA or LIDATA record in the
object module. Any time an LEDATA or LIDATA record contains a value that
needs to be fixed up, the next record in the object module is always a
FIXUPP record containing the actual fixup information.

Indexed references to names. An object record that refers to a symbolic
name, such as the name of a segment or an external routine, uses an index
into a list of names contained in a previous object record. The first name
in such a list has the index number 1, the second name has index number 2,
the third has index number 3, and so on. Altogether, a list of as many as
32,767 (7FFFH) names can be incorporated into an object module-generally
adequate for even the most verbose programmer. (LINK does, however,
impose its own version-specific limits.)

Indexed references to object records. An object record can also refer to a
previous object record by using the same type of index. In this case, the
index number refers to one of a list of object records of a particular
type. For example, a FIXUPP record might refer to a segment by referencing
one of several preceding SEGDEF records in the object module. In that
case, a value of 1 would indicate the first SEGDEF record in the object
module, a value of 2 would indicate the second, and so on.

The index-number field in an object record can be either 1 or 2 bytes
long. If the number is in the range 0-7FH, the high-order bit (bit 7) is 0
and the low-order 7 bits contain the index number, so the field is only 1
byte long (see Figure 5).

If the index number is in the range 80-7FFFH, the field is 2 bytes long.
The high-order bit of the first byte in the field is set to 1, and the
high-order byte of the index number (which must be in the range 07FH) fits
in the remaining 7 bits. The low-order byte of the index number is
specified in the second byte of the field (see Figure 6). The same
format is used whether an index refers to a list of names or to a previous
object record.

Microsoft 8086 Object Record Formats

Just as the design of the Intel(R) 8086 microprocessor reflects the design
of its 8-bit predecessors, 8086 object record formats are reminiscent
of the 8-bit software tradition. In 8-bit systems, disk space and RAM
were often at a premium. To minimize the space consumed by object
records, information is packed into bit fields within bytes and variable-
length fields are frequently used.

Microsoft LINK recognizes a major subset of Intel's original 8086 object
module specification (Intel Technical Specification 121748-001).
Intel also proposed a six-letter name for each type of object record and
symbolic names for fields. These names are documented in The MS-DOS
Encyclopedia and appear in the order shown earlier in Figure 4.

The Intel record types that are not recognized by LINK provide
information about an executable program that MS-DOS obtains in other
ways. For example, information about run-time overlays is supplied in
LINK's command line rather than being encoded in object records. (Because
they are ignored by LINK, they are not included in the Encyclopedia.)

All 8086 object records conform to the format shown in Figure 7.

The record type field is a 1-byte field containing the hexadecimal
number that identifies the type of object record (see Figure 4).

The record length is a 2-byte field that gives the length of the remainder
of the object record in bytes (not including the bytes in the record type
and record length fields). The record length is stored with the low-order
byte first.

The body field of the record varies in size and content, depending on the
record type.

The checksum is a 1-byte field that contains the negative sum (modulo 256)
of all other bytes in the record. In other words, the checksum byte is
calculated so that the low-order byte of the sum of all the bytes in the
record, including the checksum byte, equals zero.

Figure 1:  Generation of an executable (EXE) file

    ║      Source     ║
    ║       Code      ║
   │Language translator│
   │  or assembler     │
    ╔═════════════════╗    ┌──────────────┐     ╔═════════════════╗
    ║  Object module  ║◄═══╡Object module ╞════►║  Object library ║
    ║   (OBJ file)    ║    │librarian(LIB)│     ║    (LIB file)   ║
    ╚════════╦════════╝    └──────────────┘     ╚════════╦════════╝
                             Linker (LINK)
                         ║Executable binary║
                         ║ image (EXE file)║
                              MS-DOS loader
                             Program runs

Figure 2:  Obtaining Data Segment Starting Addresses

  mov     ax,seg _DATA
  mov     ds,ax           ; make _DATA segment
                          ; addressable through DS
  mov     ax,seg FAR_DATA
  mov     es,ax           ; make FAR_DATA segment
                          ; addressable through ES

Figure 3:  A sample "program" containing statements from which the
           assembler derives fixup information.

                                title       fixups

                   FarGroup     GROUP       FarSeg1, FarSeg2

0000               CodeSeg      SEGMENT     byte   public 'CODE'
                                ASSUME      cs:CodeSeg

0000 9A 0000 -- R  start:       call        far ptr FarProc

0005               CodeSeg      ENDS

0000               FarSeg1      SEGMENT     byte public   ; part of FarGroup

0000               FarSeg1      ENDS

0000               FarSeg2      SEGMENT     byte public
                                ASSUME      cs:FarGroup

0000               FarProc      PROC        far
0000 CB                         ret                       ; a FAR return

                   FarProc      ENDP

0001               FarSeg2      ENDS


Figure 4:  Types of 8086 Object Records Supported by Microsoft LINK

ID byte   Abbreviation   Description

80H       THEADR         Translator Header Record
88H       COMENT         Comment Record
8AH       MODEND         Module End Record
8CH       EXTDEF         External Names Definition Record
8EH       TYPDEF         Type Definition Record
90H       PUBDEF         Public Names Definition Record
94H       LINNUM         Line Number Record
96H       LNAMES         List of Names Record
98H       SEGDEF         Segment Definition Record
9AH       GRPDEF         Group Definition Record
9CH       FIXUPP         Fixup Record
A0H       LEDATA         Logical Enumerated Data Record
A2H       LIDATA         Logical Iterated Data Record
B0H       COMDEF         Communal Names Definition Record

Figure 5:  One-byte Index Number.

       │                                                    │█
       │    bit   7    6    5    4    3    2    1    0      │█
       │        ╔═══╦══════════════════════════════════╗    │█
       │        ║ 0 ║         Index Number             ║    │█
       │        ╚═══╩══════════════════════════════════╝    │█
       │                                                    │█

Figure 6:  One-byte Index Number.

bit  7   6   5   4   3   2   1   0   7   6   5   4   3   2   1   0
    ║ 1║     High-Order Byte      ║       Low-Order Byte          ║
               First byte                   Second byte

Figure 7:  8086 Object Record Format

             │                                               │█
             │    ╔══════╦═════╦═════╦════/═════╦═══════╗    │█
             │    ║Record║  Record   ║   Body   ║  Chk  ║    │█
             │    ║ type ║  length   ║          ║  sum  ║    │█
             │    ╚══════╩═════╩═════╩════/═════╩═══════╝    │█
             │                                               │█


A Guide to Program Editors, the Developer's Most Important Tool

Also see the following related articles:
  The Brief Editor
  A BRIEF Journey
  The ME Text Editor
  Inside ME
  A Guide to Developers

Tony Rizzo

The personal computing platform has evolved into a serious and important
programming environment. Whenever one thinks of a programming environment,
what usually comes to mind is the compiling or assembling of source code
and the tools associated with that process, namely compilers and

However, compilers and assemblers are the very last links in a long chain
of events and may represent only a small fraction of a program's
development cycle. Throughout this cycle, from concept to design to the
actual building of a program, runs a fundamental thread──the evolution of
the text that is the source code of a program. What the professional
programmer really spends most of his or her time doing is writing and
editing the source code by working with a text editor.

In the early days of personal computing, many programmers used word
processors. WordStar(R), for example, often doubled as a text editor,
providing the ability to create a "text" file by stripping any special
codes prior to actually writing out the file. It was the first real text
manipulation program available for the PC, and there are many
programmers who still refuse to consider any other alternative.

WordStar continues to influence even the newest compilers as evidenced
by the emergence of the integrated editor/compiler environment. These
compilers provide a WordStar-compatible (or keystroke-compatible)
integrated editor.

The PC software business has grown at a tremendous rate, and the
"garage/home-brewed" software of yesterday has in many cases given way to
sophisticated software teams working in highly competitive and fast-
paced environments. A major factor in a programmer's success is

New and faster compilers, assemblers, and debuggers are emerging all the
time. Compilers now offer sophisticated code optimization capabilities,
and tools such as CodeView(R) provide sophisticated debugging aid. But such
tools are of little use without source code. The key to enhancing
productivity is the ability to create well-designed source code in the
least possible time. Consequently, WordStar no longer meets the
strenuous demands of the professional PC programmer.

There is a wide variety of editors available to programmers, ranging in
price from as little as $40 to more than $300. Some editors are based on
mainframe and minicomputer predecessors. Some were specifically
developed to operate within the PC environment. Some label themselves as
intended strictly for the professional programmer. Some purport to be all
things to all people. Whatever their individual heritage or claim to fame
might be, there are enough options that a programmer might have
difficulty in choosing one editor.

The technological advancements of recent generations of PCs have brought
us faster, more sophisticated hardware capabilities, particularly in
graphics. Each editor offers a unique set of features──multiple windows,
EGA support, configurable keyboards, built-in macro languages, and the
ability to compile a program using the programmer's preferred compiler or
assembler on the fly from within the editor itself.

Selecting the "right" editor is a very personal decision. How many times
have you heard a fellow programmer complain about his editing tools?
Perhaps you've spent most of your professional career working on a UNIX(R)
system, only to discover that Emacs and VI don't quite make it in the PC
world. Perhaps you only use a mainframe-based editor because you feel safe
in knowing how it operates, even though its performance is clearly awful
on your AT. Or perhaps you are that WordStar person who'd rather fight
than switch.

In order to help with your decision, MSJ set out to do a comprehensive
review of all the commercially available and advertised program editors.

We settled on twelve analysis areas by which to judge each editor:

  ■  basic editor functions
  ■  user interface
  ■  macro capabilities
  ■  file handling
  ■  special features
  ■  support for unique language features
  ■  print capabilities
  ■  word processing features
  ■  documentation
  ■  setup/installation
  ■  help/tutorial
  ■  speed

Our first objective was to analyze basic editing and productivity
features such as invoking a compiler from within the editor, editing
multiple files simultaneously, and multiple windows. Our survey of
programmers' requirements also indicated the need for certain word
processing features to create quick documentation, write notes, memos,
and so on. The ability to wordwrap, justify, and center is also handy.

Speed is important, but we weren't as interested in the raw speed
required, for example, to go from the top to the bottom of a document as
we were in factors such as the number of keystrokes necessary to effect
such a move. Often the time spent or wasted in keyboard manipulations
plays a much larger role in one's perception of a product's quickness
than raw speed. The difference between 2 and 4 seconds is not always as
significant as having to use six keystrokes instead of two to do
something. Nevertheless, for those of you who value such measurements we
have included two. These speed measurements were all recorded on an
unmodified 8-MHz IBM(R) PC/AT(R) (model 339).

The chart☼ accompanying the reviews lists the capabilities of all twelve
editors. We have listed the editors in order from highest rating to
lowest. The editors that received the three highest ratings were chosen
for more detailed review.

In addition, we offered the developers the opportunity to comment on
some of the underlying design and technology issues they had to deal
with in order to create their editors. We also encouraged them to give us
a glimpse into their plans for the future.

What we were looking for in this search-and-discover mission was proof
that text editors have been keeping pace with other, more heavily advertised
systems tools. We think our results point to the best-engineered editors now
available on the market.

Editing tools may not seem as important to obtain as the latest and
greatest optimizing compiler. However, given the capabilities of this
new generation of editors, we strongly suggest that you seriously
reevaluate your current text editor. See how it stacks up against the best
editors now on the market. You may just discover that a big boost to your
productivity, as well as to your competitiveness as a programmer, awaits

Microsoft Systems Journal believes strongly in the success of well-
engineered products, whether they are Microsoft's or another vendor's. The
three text editors that received the highest ratings are highly
recommended──they clearly deserve your attention.

There was one big surprise among the top three editors. Magma Systems' ME
retails for all of $39.95. What can one say about such a discovery? The
reviewer has considerable programming experience and he was very impressed,
enough to consider it superior to other editors costing many times its

Microsoft feels that OS/2 is going to become the operating system of
choice for the majority of PCs over the next several years and is the
ideal platform for the development of the next generation of applications.
Is there an editor out there that will not only keep you competitive today
but also in the protected-mode operating system world of tomorrow? Read on
and find out.

The BRIEF Editor

Matt Trask

BRIEF(R), an acronym for "Basic Reconfigurable Interactive Editing
Facility," is a full-featured programmer's editor for DOS-based personal
computers. Marketed by Solution Systems(R), it is moderately priced at
$195. Even though it is not my usual editor, BRIEF is intuitive enough
and the on-line help is good enough that I was able to use it to write
this review.


The documentation consists of two wire-bound paperback manuals and a quick
reference booklet, enclosed in the typical IBM-style slip case so that
they can maintain their own shelf space. The User's Guide includes a
tutorial, an alphabetical reference to the editor's commands, and an
index. In addition there is a complete summary of messages that BRIEF can
generate and a chapter with some common questions and answers about
editor operation.

The Macro Language Guide is a complete tutorial and reference for
BRIEF's built-in macro capability. As in any language reference there is
a section that defines the syntax and structure of the language. Two
examples of real-world macros are given-a searching package and a restore
function. The section on the restore function is an informative and
sometimes humorous visit with one of the editor's authors, Dave Nanian,
as he demonstrates the process of developing a macro that can restore the
editor's state (such as open files, search pattern, search direction, and
windows) on startup to the point where it was previously exited. This
section is an excellent insight into program development methods for any
new programmer as Nanian goes through the whole cycle (idea, fleshing it
out, pseudo-code, detail work, getting stuck, maintenance) in the example.

The documentation includes a card that summarizes the side effects of the
installation procedure so that you will have an idea of what the setup
program will or will not do to your AUTOEXEC.BAT before you run it. The
quick reference booklet is somewhat superfluous as it is easier to use
the on-line help than to find and page through a printed reference.

Although the tutorial provided with BRIEF is not of the flashy on-line
computer-assisted-instruction type, it is thorough and more than adequate
to get a new user up and running in a short amount of time. The first
forty-odd pages of the User Guide are an interactive quick-start lesson
that goes through the few editor features needed for most purposes. This
tutorial covers startup, shutdown, editing, cursor motion, help, undo,
cutting, pasting, windows, and other editing functions. At the end of
this section there is a quiz to test your comprehension of the material
by solving a typical editing problem. The next five chapters cover BRIEF
in greater depth, getting into theory of operation and more advanced

Setup and Installation

BRIEF comes with a program called SETUP.EXE that configures many of the
editor's operating parameters. You need to run this program once before
using the editor, but the default values are adequate for getting started.
SETUP can be run again later to reconfigure the editor as you become more
familiar with the available features and wish to tune them to your

CONFIG.SY and AUTOEXEC.BAT are modified by the setup program to add
device drivers, if needed, and set environment variables that inform the
editor of your choices. The documentation for the AUTOEXEC changes is
adequate in case you wish to change these by hand instead of using
SETUP.EXE. In addition, the setup program generates and compiles a
startup macro that contains initialization commands for screen
colors, autosave, and other options that can be customized. You can edit
and recompile this macro to add any other actions you desire at startup.

Among the configurable parameters are display parameters (color
preferences, cursor shape, use of 43-line mode on EGA, snow test on CGA
adapters, and video page test), filename extensions (compiler control
commands, word processing features and autoindentation, including
normal, smart, and template), and safety features (enable autosave, set
autosave timeout, use of backup files, depth of undo stack, and enable
session save/restore). Other parameters include initial insert/overstrike
mode, empty spaces filled with tabs or spaces, whether to use Ctrl-Z at
end of file, keyboard autorepeat rate, default line length, case
sensitivity during searches, and use of regular expressions in

Context-sensitive help is available during setup. The setup help has a
message, "Press Alt-H if you need more assistance," that appears and
flashes intermittently after a period of inactivity on the keyboard. This
attention to detail in the user interface is indicative of the
thoroughness of the product.

The only complaints I have with the setup procedure are the assumptions
about drives and directories. SETUP asks which disk is to be used and then
creates a subdirectory called \BRIEF, rather than asking where the
editor's files should be kept. It assumes that the target drive is also
the boot drive and therefore creates and modifies AUTOEXEC.BAT and
CONFIG.SYS in the root directory of this drive rather than finding out
where the real copies of these files are.


The context-sensitive pop-up help system is excellent. Pressing Alt-H
brings up a help menu during text entry or editing. This menu is the
entry into a hierarchical help window subsystem that covers such topics
as blocks, keys and commands, and windows. After selecting one of the
menu choices, another press of Alt-H shows which key or key combination
performs that editor function.

If you are currently in the middle of an editor function such as Alt-E
(open and edit a new file), Alt-H goes directly to the help window that
describes that function. There is also an on-line quick reference
available from the help menu that lists all available editor functions
and their current key assignments. Another menu selection accepts a
keystroke and tells you what function is performed by that key.

Basic Editor Functions

BRIEF does almost everything imaginable in the areas of cursor motion,
block operations, delete, copy, search-and-replace, and so on, and does
them quite well. One powerful feature not usually found in PC editors is
the availability of UNIX-style regular expressions for searching, in
addition to simple string matching. For instance, the angle bracket in a
search for "<if" causes BRIEF to find all lines beginning with "if," and a
search for "{[Hh]i} world" will find "hi world" and "Hi world."

The ability to adjust the keyboard repeat rate at setup time affects the
cursor motion. I made the mistake of setting this at the maximum repeat
rate for efficiency, only to watch as a repeated backspace to correct an
error about 10 characters to the left gobbled up all the text on the line.
I was rescued by the undo (Alt-U) capability, which is configurable as a
stack that saves editor actions, not just deleted text as some editors do.
This is usually set up to save the 30 most recent actions, but you can
configure it to your liking.

Word processing is not a strong point of BRIEF, but it is certainly
adequate for writing documentation and memos. A macro package is provided
that can be attached to a file extension such as .DOC or .TXT and
automatically enable the available word processing features when a file
with this extension is edited. BRIEF's features include wordwrap, margins,
centering, and automatic paragraph reformat. If you want more, you can
enhance the supplied source code to the word processing macros. A truly
ambitious programmer could conceivably write a full-featured word
processor by using nothing more than BRIEF's macro language.

User Interface

The user interface in BRIEF is more visual than other, more terminal-
oriented editors. It takes advantage of special PC features such as color
and line drawing characters that are usually not available in non-PC

Since every key on the keyboard is reconfigurable, you can assign the
editing functions such as search or delete in any way that you please.
Macros can be assigned to individual keys or can be executed by the
Execute Command key (F10 in the default configuration). This lets you
configure BRIEF to look like another editor more to your liking, for
instance PC/Vi or WordStar. A package that configures BRIEF to PC/Vi's
user interface is available from the authors. The remember and playback
functions make it easy to record keystrokes on the fly for reuse if you
need to repeat a sequence of actions.

Screen layout is one area in which personal preference often borders on
fanaticism. BRIEF has all the things that I like on the screen, no more
and no less. Besides the name of the current document in the top border,
the status line at the bottom of the screen shows the current line and
column occupied by the cursor and the time of day. If you don't like a
border around the screen, you can disable it at setup time. You can modify
foreground, background, and message colors with SETUP to support whatever
is available on your system.

BRIEF uses tiled windows instead of the overlapping Macintosh-style
windows. I generally prefer the latter, but in an editing context, a
window is usually used when more than one file has to be visible, so this
is not really a limitation. A 43-line display mode is available when
running on an EGA but the tiny characters are a little tough on the eyes.
This large screen mode would be useful if you are in an extended editing
session that requires the use of windows, since you can make each window
larger or have more windows.

The cursor shape changes to indicate which mode (insert or overstrike) is
currently in effect. The space past the end of the last character on a
line is referred to as virtual space──the cursor is allowed to move there
but it changes shape to indicate that it is in virtual space rather than
on top of a space character.

Nonstandard enhanced displays such as the WYSE(TM) 700 and Hercules(R)
Graphics Plus are supported at their maximum resolution by device
drivers that are installed by SETUP.

Unique Language Features

BRIEF supports autoindenting and delimiter checking for all languages.
The C language support is quite extensive. "Smart" indenting uses a
knowledge of the language syntax to automatically indent and outdent
as control structures open and close. Template indenting provides for
automatic completion of C language keywords. The keyword completion
can be confusing; it is probably useful for a programmer who only uses


BRIEF is truly outstanding in its macro capabilities. The built-in LISP-
like macro language is a complete general-purpose programming language.
It covers all the features that you would expect in a language except port
I/O and system calls. Even this is not too much of a limitation; if you
want to write a program such as a word processor with BRIEF macros, the
built-in "dos" macro can execute external programs that perform these
operations. The exit codes from such programs are retrieved by BRIEF and
are returned to the calling macro. If the external program needs to
return data, a file can be used for communication.

Rudimentary timer functionality is performed by registering a macro for
idle time processing when the keyboard is inactive. This mechanism
operates BRIEF's autosave feature whenever there is a period of
keyboard inactivity. The amount of time delay before autosave starts is
a setup option.

Two sets of primitives are provided: language primitives such as if,
while, get_parm, and editing primitives such as inq_position for cursor
control and del_edge for window modification. These primitives are the
equivalent of the function library that is provided with most language
translators for linkage with your own programs. Some of the available
functions are atoi (convert an ASCII number in a string to an integer),
beep (sound the PC's speaker), color (set the foreground, background, and
message colors), compress (shorten multiple white spaces to single ones),
create_window (create an overlapping window such as that which the help
subsystem uses), keyboard_flush (discard any pending keystrokes),
remember (record a sequence of keystrokes to playback later), and
write_buffer (write the current file out to disk).

File Handling

Maximum file size is limited only by the available disk space. BRIEF
automatically pages a file to disk as needed to free up memory for
editing. Inserting the contents of one file into another by using the
multiple file editing commands and the block copy command is

Special Features

BRIEF supports an extensive list of translators for operation within the
editor, which includes automatic return to the source file on error with
the cursor positioned at the cause of the error. Some of the supported
translators are MASM, Lattice and Microsoft(R) C, dBASE(R), R-M COBOL, Lahey
FORTRAN, Microsoft QuickBASIC, and Oregon Pascal-2. Many other compilers
are supported, and examples show you how to integrate a compiler that is
not supported into this environment.

Multiple files can be opened at the same time with the Alt-B buffer
command, which pops up a list of all currently open files for direct
selection. Some or all of these files can be displayed in tiled windows
simultaneously. I don't find windows to be particularly helpful on a
system with a small screen such as an EGA. The Macintosh paradigm is only
useful in that it models the way work is done by showing a pile of windows
(tasks) currently available, but the current task is given a full-sized
screen (or close to it) to work on. If you split an 80-column screen into
left and right halves, enough work, such as code comments, is obscured
that it is nearly useless. On the other hand, if you use BRIEF with a WYSE
700 display that supports 50-lines-by-160-columns, four full screens can
be shown at a time.

BRIEF has two features of great value: a support conference on the BIX
network and autosave. The conference gives a user direct access to the
program's authors, an excellent supplement to the staff support offer by
Solution Systems, as well as a forum for macro exchange. The autosave
feature writes a copy of the current file out to disk with the extension
.ASV whenever the keyboard is idle and deletes this file on a normal exit
to DOS. If there is a power failure (or you're working at home and your
two year old discovers the Reset button), this file enables you to recover
with no lost data.


I am accustomed to using 286-and 386-based systems where program
performance is usually not a concern. One editor that I customarily use
practically requires these faster systems to be useful. I installed BRIEF
on an XT-type system to see if this was the case and found no discernible
difference. Of course, major compute-intensive operations such as a
large search-and-translate benefit from a faster CPU, as is true of most

The responsiveness of the keyboard and other parts of the user interface
is crisp, with no noticeable delays.

Print Capabilities

Other than the ability to mark and print a block, there is no special
support for printing in BRIEF. You can set right margins and use embedded
formfeed characters for pagination. However, printer features such as
fonts are not supported.

Vendor Support

Vendor support is very good. I had a lot of trouble getting through the
initial installation of BRIEF due to a bug in another product that was
installed by my AUTOEXEC.BAT. After a short description of the symptoms,
the very sharp support person said, "Oh, you must be running xxx.COM; it
has a bug in the use of the SI register."

Instead of threatening you with imprisonment in return for no guarantee of
product usefulness, the license agreement restricts you to using BRIEF
on only one keyboard at a time. Solution Systems also offers a service in
which you can have your name embedded into the code along with the serial
number so that you can keep track of who is using your program.

An OS/2 version of BRIEF is currently under development for release in
early 1988, and BRIEF's author tells me that the current UNIX version is
being ported to other popular UNIX environments that support Sys V.3.


There are a lot of things to like about BRIEF. It takes advantage of
available PC features such as 43-line mode on the EGA. Multiple windows
make it easy to keep a spec in view while writing new source code.
Compilation within the editor with automatic return of the cursor to the
error can save a lot of time waiting for printouts of compile-time
errors. One power failure is all that it takes to appreciate autosave-the
value is not in the saving of typing time, but in the time saved not
having to reconstruct the thought processes behind your most recent

The one feature that makes BRIEF worth the effort of learning to use a new
editor is its programmability. The restore macro is probably the best
advance in productivity since editors that can operate on multiple files.
The ability to return to your PC and invoke BRIEF such that it restores
all opened files, cursor positions, and search strings allows you to
casually walk away without worrying about the time it would take to
restore your mental state when it is time to start again. If Solution
Systems would just modify the restore macro to save multiple sessions for
different concurrent projects, an excellent program would be even better.

A BRIEF Journey

David Nanian and Michael Strickman

From the start, we envisioned that BRIEF would need to run under a number
of different operating systems. To make this task easier, and because it's
good programming practice, BRIEF was designed to be as modular as
possible. We made logical divisions along functional boundaries,
splitting the main program into a number of independently operating
subsystems that communicated through standard function interfaces.
While many of the data structures are shared between modules, all of the
substructures that are related to a particular module are handled by
that module alone.

The basic modules, or subsystems include: virtual memory, windows,
logical display (refresh), physical device, input handling, macro
interpretation, undo, buffer modification, and command dispatch.

Each of these subsystems can be replaced with functions tailored to a
particular target system. The only requirement is that the new code must
adhere to the original subsystem interface. In the case of operating
system subsystem replacement, this usually means including some
intermediate "glue" routines.

Most of BRIEF's subsystems were designed to act as independently as
possible. For example, the part of BRIEF that handles changes to files
doesn't know anything about windows, nor does it know how the file it is
manipulating is going to be displayed. It works purely at a stream level,
and expects that other subsystems will provide the file's stream data and
present the results to the user.

Similarly, the refreshing subsystem doesn't really know anything about
the physical representation of the windows that it's refreshing since
it's completely device-independent. The subsystem only assumes that it
is refreshing ASCII text (from the file subsystem) onto some sort of
rectangular area that can be addressed in x,y coordinates through the
physical device interface. The algorithm redisplays the minimum amount
of information necessary; at the physical device level these operations
might translate to bit BLTs on a graphics display, block moves on a
memory-mapped CRT, or scrolling operations on a terminal.

Substantial portions of the editor have been implemented in BRIEF's macro
language. We chose the macro language over either C or Assembler for
several reasons.

First, the macros themselves are virtual, both in size and in their
interface to BRIEF. Virtual size means that the virtual memory subsystem
reads portions of the macro as they're needed, so the entire macro does
not have to be in memory at one time. The virtual interface means that
the macros communicate with a "BRIEF" machine, rather than with any
particular operating system or hardware, so they can be ported without
change from environment to environment.

More importantly, BRIEF was designed to let the user completely customize
the editor. We could have made the source code for the entire package
available. Although this option would allow the most complete
reconfiguration of the program──essentially, reconfiguration without
limits──it also creates many problems. Configuration requires extensive
knowledge of BRIEF's internals, making "simple" configuration all but
impossible; technical support is more difficult; updating the package
is extremely time-consuming; moving the configured system to other
environments is almost impossible; and, our secrets would be available
to all of our competitors. Clearly, this was not the way to go.

Another option was to provide users with a standard "library" of editing
functions which would, in essence, be the BRIEF subsystems. A library
could be provided through a .LIB file, a .DLL under OS/2, or even a run-
time interface. While a library would have made it easier to update
users, the other problems still remained.

We decided to take our virtualization philosophy a step further. Rather
than forcing the user to deal with the specifics of the system BRIEF was
running on, we provided the user with a set of "virtual" interfaces that
allow complete configuration without worrying about the environment or
specific, low-level implementation details. By writing a large portion of
BRIEF in the macro language, we provide an opportunity for users to learn
from the coding that we've done, and to change that code to match their
own style.

BRIEF's modular design makes it easier for us to port BRIEF to other
environments. For example, we are currently porting BRIEF to OS/2. The
BRIEF port consists mainly of changes allowing it to conform to the
virtual memory capabilities of the operating system. Refreshing code,
window management, macros, and high-level input remain largely unchanged.
The facilities provided by BRIEF's device drivers come primarily from
the operating system, and hence that system-specific code has been
removed. Much of the OS/2 version of BRIEF is substantially less
complicated than the one that runs under DOS.

The only problem with the OS/2 port so far has been our dependence on the
small programming model and the DOS memory allocator. BRIEF makes
extensive use of DOS memory allocation blocks, and we have stayed with
the small model (with some far routines) to get the most out of PC and XT
computers. Under OS/2, memory allocation has changed and we can no
longer assume that allocated blocks returned from the operating system are
paragraph-aligned segments (which, under DOS, we could store as a 16-bit
pointer with an implied 0 offset). Although a similar scheme could have
been implemented under OS/2, the overhead and large number of selectors
required made it impractical. We have converted BRIEF to a large model
program, and a lot of time has been spent converting various pointer
references in C and assembly language.

Overall, the port to OS/2 has been greatly assisted by BRIEF's modular
design. We anticipate that porting the program to environments such as
the Presentation Manager, UNIX(R) (and possibly even the Macintosh(TM)) will
be easier because of this design.

The ME Text Editor

Greg Comeau

Editing text is an important part of program development and maintenance.
Like most other programmers, a good portion of my time is spent using a
text editor. The editor of choice must be powerful, customizable,
extensible, reliable, and of course fast.

The Magma Editor Version 1.31, or ME as Magma Systems calls it, stands out
among the text editors I looked at. It supports editing of multiple
files, full and split-screen windows (pop-up or pull-down), 43-line EGA
mode, regular expression searches, cut-and-paste block commands, a C-like
macro language, compiles from within the editor returning with the cursor
on any offending lines of source, and more. Just before deadline, I
received the latest version of ME, Version 2.0 (which as of this writing
is in beta testing), and it's loaded with new features.

ME comes with help files, a macro compiler, sample macros, and a utility
called CTAGS in addition to the editor itself. Installation was as simple
as inserting the distribution floppy and typing INSTALL. Magma puts
comments in the installation script so that you can modify it in case
you don't want to install the whole package.


The documentation for ME is not outstanding. Most commercial software
comes in a sturdy box containing high-quality documentation, usually in
a slip-case binder. ME arrived on a disk packaged in a disk mailer, which
is not exactly an overwhelming presentation.

After installation, I found out that you have to print your own copy of a
file named MANUAL if you want a hard copy of the documentation. The manual
is very disorganized and has no index, and the command reference is
located in a file called KEYCHART rather than in the manual itself. It was
very hard to find sections describing certain features.

However, the manual is complete, and all features of ME are described at
least once. Examples of all commands and functions are provided; more
complicated features had more than one example. Perhaps the best feature
of ME's documentation is that it makes it perfectly clear from the start
how to exit from the editor, something that some editors don't do.

Although I haven't seen it, Magma Systems promises that Version 2.0 will
have a new, bound and typeset manual.

On-line Help

Once ME is installed, you invoke it by typing ME followed by the filename.
Then, once the program is running, you can go into an on-line help
subsystem that is not very elaborate but does the job. The text for the
on-line help is on disk at all times, and you can change it to suit your
needs. Changing the text to the help system is as simple as editing or
creating a new text file by using ME and letting ME know where the file
is located at startup, usually in \ME\HELP. Version 2.0 will have an
expanded help subsystem that is more readable and easier to use.

The general behavior of ME is consistent and reliable. I found no
surprises and felt comfortable with it. The Ctrl-key combinations are
set up logically and if you don't like them they can be changed. Major
features such as buffering, windowing, and block operations are
complete and operate without hesitation.

Buffers and Windows

Much of ME's functionality is based on buffer and window operations. A
buffer is an area of memory that holds data; in an editor, buffers are
responsible for holding varying lines of text and are important tools for
a programmer's productivity. ME supports two different types of buffers;
the first is called a scratch (or pick) buffer, which is useful for moving
text on the screen from one area to another or between windows. ME can
handle up to ten scratch buffers at a time. So, for instance, you could
delete or mark a few lines from one part of the program you are editing
and place them somewhere else in the file. Another possibility is to add
the same group of lines to many files, say a copyright message or some
variable declarations, by loading each file, moving to the appropriate
place, and inserting the scratch buffer.

The second type of buffer can have files loaded into it and can be seen in
a window, a view into a buffer displayed on your screen. Each window is
limited to a certain number of lines ranging from 1 to 24, so you can have
up to 24 windows active at any one time. (You can have an unlimited
number of buffers if you use ME's macro language described below.) Since
things can get cluttered, ME allows windows to be exploded so as to fill
the whole screen. Other windows displayed at the time of the explosion do
not go away; they become temporarily invisible until you shrink the
window currently in use.

ME typically loads with only one window displaying the source file that
was given as its command-line argument. You can add extra windows by
specifying more files on the command line or by creating new windows
after a session has started with the split-window command. Once multiple
windows have been established, you can delete a window, increase or
decrease the number of lines, choose between or explode windows, and
change the color of windows.

Version 2.0 of ME will have pop-up windows for adding messages to a
customized user interface and vertical windows for comparing source


ME supports various flavors of macros, the simplest of which is the
keyboard macro. This macro allows you to record all your keystrokes so
that you don't have to worry about typing them over and over if you use
them repeatedly.

Keyboard macros can also be transformed into another type of macro,
called a named macro, if you find that you must abort the current edit
session or find yourself creating the same keyboard macro over and over.
Named macros allow you to create commands that the ME user interface does
not provide directly. For instance, ME can only display 24 windows at a
time; since there are 10 scratch buffers and one buffer per window, the
ME interface permits a maximum of 34 buffers. Using a macro of your own
creation you could add as many buffers as you'd like. For instance,

    int   bufid;
    bufid = create_buffer("tempbuf");

creates a buffer named "tempbuf" and attaches it to a new window. It can
then have code to process and perhaps save the data in that window; at
some point it must close the window and delete the buffer.

An excellent example of a macro is shown in Figure 1. It contains a sample
ME macro script to compile Microsoft C Version 4.0 programs and report any
errors found. Notice its C-like syntax and its use of variables. The
example uses most of ME's macro language constructs including constants,
strings, arrays, arithmetic operations, compiler directives (such as
#include), flow-control statements (for, while, if-then-else, and switch),
function calls, global variables, local variables, parameters, and built-
in functions. You could change this example to conform to different
languages or different compilers; this would usually require the
modification of just five or six lines. The ability to code macros in a C-
like language is a strong selling point for ME. Most other editors that
come with a macro language usually contain some sort of LISP subset for
this, and the result is many times more complicated to write, read, and
debug. Because of the popularity of the C language, most programmers
will find ME's macro language extremely easy to use.

ME also supports keyboard remapping: the program keeps track of key
values so that any keystroke can be bound to another key, an ME macro, a
user macro, or even another keyboard map. This is useful for generating
multiple-key command sequences, either for a new group of macros or for
emulating other editors. An interesting new feature in Version 2.0 will
be a "typeamatic" mode for keyboard repetition.

Version 2.0 of ME includes two notable changes to the macro language. One
is the ability to use strings as the targets of a switch/case statement,
making the macro language more powerful and efficient. Before Version
2.0, a user could only execute a switch statement that evaluated to a
numeric argument, and that ruled out all strings, including those
containing the ASCII values for the digits 0 through 9. That meant that
any selections performed with strings had to be done either via if
statements or by encoding a numeric value in an array that also had a
string counterpart and then using the array index to access the string.
Now you can switch directly on a string as well as specify strings as the
target constants for each respective case statement. This is a facility
that most high-level languages cannot handle.

The other major change to the macro language is that ME now allows calls
to C functions directly from the macro language, providing for a very
flexible interface. You can call all routines internal to ME, and now
you can write parts of your larger or slower macros in C. Also, if you
need to call some of the C run-time libraries, this lets you do so without
having to create new macros. Instead, you just add an entry to a table and
recompile ME. (This option is available only if you've purchased the
source code for ME, which costs $95. Magma includes a README file
suggesting the use of MSC 4.0 or higher for any compilations.)

Other additions to the language include labels, the goto statement, and
hexadecimal constants, all of which make the ME macro language much
richer without going overboard. The end result is that macro generation is
more readable, structured, and easier to perform when compared with most
of the other macro-based editors on the market, which force you to use a
terse and error-prone LISP-like language. LISP and its subsets are
powerful, but not very productive or familiar to many programmers.

Regular Expressions

Many concepts from the UNIX operating system and its associated toolkit
have made their way into mainstream computing in spite of their
terseness and complexity. One such concept is regular expressions, which
let you build very complicated pattern-matching commands using
metacharacters to facilitate text searches. For instance, let's say you
wanted to search for every occurrence of "abc" in the current window
and change it to "ABC." That's simple enough with most editors. Now let's
add a slight twist to the search: all the occurrences of "abc" must occur
at the very beginning of a line. Most editors would fail at such a simple

Looking through ME's list of metacharacters (see Figure 1 for a sample of
the on-line menu for Search and Substitute Commands) you may notice the "
" character. By using this special character in the previous request you
could now look for the pattern " abc" instead of "abc."

Another problem that comes up often is swapping two columns on a line or
table. This can be accomplished by using " \(?*\) \(?*\)" as the search
pattern and " \2 \1" as the replacement pattern. Such a feat often
requires writing a general-purpose conversion utility, but comes naturally
to ME.

Programmer-specific Features

For the programmer, ME has a few unique language features. One is its
support for autoindenting while in input mode. Another useful feature
is the match-brace command, which finds a closing curly brace or a closing
parenthesis if the cursor is positioned at an opening one (and vice
versa). Compile/next scenarios are supported with macros. And although
Magma doesn't supply any language-specific syntax templates or syntax-
based editing even on a simple level, you can certainly write macros to
handle this. It is surprising that templates are not provided for the C
language; when I mentioned this to the people at Magma, they said they
were working on it and will have it in the next release of ME.

Magma provides the CTAGS utility with every copy of ME. CTAGS was
originally created for BSD UNIX and allows you to create a file containing
a list of filenames and line numbers of every function you use in your
code. Later, in an editing session, you can automatically create a new
window containing the source file of any function in the list with the
cursor properly positioned on it merely by responding to the go-to-
function command sequence with the function name. Still, CTAGS is limited
in that the file it creates does not change as your source code changes.
It can be helpful in a large programming project though, especially if you
are unfamiliar with the project.

ME's command set has some unique features. The 8-bit-mode command is used
for entering ASCII characters that are generally classified as
unprintable or extended, that is, those with values less than 32 and
greater than 127. Most editors allow you to enter only one such character
at a time, and ME is no exception. However, ME also lets you enter a
sequence of extended commands by switching into 8-bit-mode. This is very
handy for generating terminal escape sequences or for emulating a strange
file format.

Another interesting point about ME is its source code option. Because it
is written in C, the curious can use the source code to learn various data
structure concepts by looking at a real-life application. Also, you can
convert your most heavily used macros into true built-in commands by
modifying some of ME's internals. Finally, some programmers may be
interested in contacting Magma for an OEM license so they can include ME
in their product line and/or applications as well as configuring it to
their own needs.

Magma is now working on an OS/2 version of ME, which will support all
features of Version 2.0 as well as OS/2-specific concepts. One such
planned feature is extending ME's autosave ability so that buffers get
saved by another task. That way your edit sessions will not be interrupted


ME does not support printing. If you need to print a copy of your source
code or documentation, you must write a macro to handle it since Magma
doesn't provide any. This was annoying since it meant getting intimate
with ME's macro language, but it does demonstrate how powerful and
adaptable ME can be. For instance, I wrote a macro that picked up the
current filename and printed out a listing of the program in the buffer
much like the UNIX pr command. It included line numbers, page breaks, and
a header identifying the file and the page number. One could even expand
on such a macro to accept a list of dot commands to perform such tasks as
centering, justifying, underlining, and boldfacing.

Vendor Support

Magma's technical support is excellent. When I called them for support,
I was always promptly greeted by a member of the staff, and my questions
were answered eagerly and completely. Need I say more? Further, if you
happen to be a member of BIX, you can communicate directly with Magma
Systems on-line.

Overall Impression

ME is one of the top programmer-oriented editors on the market. It
contains many concepts ideal for maximizing programmer productivity and
stands out in four areas.

The first area is its command set. High among its list of features are its
various macros accessible either on the fly or via its macro language.
This allows for custom editing and flexibility when dealing with
complicated or repetitious tasks. Also, its use of buffering and
windowing offers programmers access to multiple files, a necessity even
when working on small projects.

The next area of importance is ME's consistency. Its operation is reliable
in every aspect. In general, the user interface is well thought out and
its behavior is not erratic. Programmers do not need to spend time trying
to fight with their tools so that they can do their editing. Also, when
programmers do need to reconfigure part of ME's operation, it is a very
smooth transformation.

As for speed, ME performs very quickly. Most cursor operations occur
instantaneously, and ME is usually waiting for you instead of the other
way around. Similarly, non-window-oriented commands are also speedy,
including searches for regular expressions and macro functions written
by the user.

Finally, ME contains features that are usually found in more expensive
editors costing several hundred dollars, but its price tag is only
$39.95. I'm told that the new release, Version 2.0, will retail for
$89.95. It is surely the most economical editor allowing for such a
powerful environment for programmers and is surely one of the bargains
of 1988. Even at double the price, there's no question about its
price/performance ratio.

ME is fast, complete, reliable, and productive──exactly what a
programmer's editor should be.

CompuView's VEDIT PLUS

Randall Swan

VEDIT PLUS, from CompuView, is an excellent programming editor with
many powerful options and a high degree of customization. The program is
very quick to load, respond, and execute all commands.

The commands are organized in a logical manner, and the on-line help and
tutorials in the documentation considerably speed up the learning
process. VEDIT enables you to edit multiple files simultaneously in
separate screens or windows and to store text or macros in multiple text
registers, which saves a great deal of time.

VEDIT offers you a choice of command mode, in which you type specific
commands, or visual mode, in which you use predefined function and Ctrl-
key combinations to specify edit functions, thus providing much
flexibility. The editor can be extensively tuned to an individual
programmer's environment by using the command mode to write command
sequences and storing them as keyboard macros with specially designated
function keys and Ctrl-key combinations.


VEDIT PLUS has fairly substantial documentation──378 pages in a 6-by-
81/2-inch three-ring binder. It includes a tutorial, a user guide, a
programming guide, a section on installation, and a reference section.

The tutorial covers topics ranging from simple editing operations to more
complicated ones. There is also a 30-Minute Tutorial in the introduction
to get started quickly. However, specific keystrokes are conspicuously
absent from the documentation; they are described only in the on-line
help screens. Another problem with the documentation is that it was
written for all installations of VEDIT PLUS, including the CRT terminal
version and various memory-mapped machine-specific versions.


To set up VEDIT PLUS, the manual tells you to simply create a VEDIT
subdirectory and copy the one disk into it. Several things in the
installation instructions, however, are not fully explained.

An INSTALL.EXE program allows for extensive customization of VEDIT.
From a menu in INSTALL called Main Menu Tasks, you can modify the
keyboard layout, add keystroke macros, and change print, edit, file-
handling, and screen display parameters. You can also change edit switch
settings, visual settings, and the command mode interface. It is
possible to create numerous configurations by running INSTALL
VPLUS.COM "outfile".COM, each identified by a different sign-on message.

On-line Help

On-line help is easily accessible by pressing Alt-F1, though this
keystroke is explained only in the introductory tutorial. The first
screen displays all the edit functions with corresponding keystrokes, the
next screen displays the separate default and expert keyboard layouts,
and the third displays status line messages and editing tasks. You can
access on-line help by entering command mode and typing EH to see a list
of topics or H to display five screens of information: basic and text
register commands; extended, jump, operating system, and print commands;
miscellaneous and numeric register commands; keyboard control, command
modifiers, help aids, generic/indirect, numeric specifiers, and
miscellany; and numeric, relational, and logical operators and operator

You can skip the menu by typing H followed by a command to display a
description of that particular command, or EH followed by a task name to
display a description of commands needed for that particular task. You
can select an individual command or function from any of the help screens
and obtain a detailed description by typing its name.

After selecting any individual item, however, pressing any other keystroke
returns to the text rather than back to the help screen, which makes it
difficult to look up more than one item at a time. There is also no way to
exit back to the editor without either selecting a particular item or
paging through all the help screens by hitting Return. You can edit and
expand all three help files.

One small source of confusion is that the two preconfigured keyboard
layouts, default and expert, use different keys to define the same
functions. The expert layout offers more predefined functions. The
explanations in the help functions depend on which layout has been
installed. There are no pop-up or pull-down help menus, nor is the help
context-sensitive. You cannot access the help screens in the middle of a

Basic Editor Functions

Cursor movement is performed with the standard up, down, right-, and
left-arrow keys. The Goto function allows for cursor movement to the
beginning or end of the file, to a specific line number, or to previously
set markers in the text. In general, though, it is not possible to move
the cursor to an empty part of the screen.

You have a choice of three modes for cursor movement: mode 0 causes the
cursor to move left from the end of a long line to the end of a shorter
line; mode 1 (default) allows the cursor to move straight up and down
regardless of line length but moves the cursor left if new text is added
when the cursor is beyond the end of the line; and mode 2 allows straight
up and down movement but adds new text at the cursor position, inserting
spaces between the end of the line and the new text.

VEDIT doesn't allow you to define columns, but in other respects the block
functions are very flexible. There are 36 text registers, labeled 0 to 9
and A to Z, into which blocks of text can be moved or copied, either
overwriting or appending it (using a plus sign) to any text already in
the register. However, this operation is cumbersome since the command
must be invoked three times, with a different sequence of keystrokes each

The defined block is not highlighted, so the only way to see the
beginning of the block is to repeat the block function (F9) followed by S
for the Swap option. Inserting a block displaces any text on that line to
the right. There are no functions to move or copy text without defining a
block, but the RC command copies a specific line or range of characters
in the edit buffer to a specified register.

The search-and-replace functions offer a number of options. For example,
both the Find (F2) and Replace (Alt-F2) functions allow you to use a
previously defined search-and-replace string and start the search at the
beginning of the edit buffer.

The default setting for search-and-replace is not case-sensitive but can
be set to distinguish between upper- and lowercase. Pattern-matching
codes, such as |A for any letter, can also be used in search-and-replace

You can set wordwrap in Task 5 (Change Edit Parameters) of the INSTALL
program, using the EP command, or selecting the Word Wrap option from
the User function to specify a right margin, or 0 for no wordwrap. The
default value for wordwrap is 0 in the preconfigured editor version,
which is a little disconcerting when one is first using VEDIT.

Paragraphs are formatted by using Indent or Undent to specify the left
margin, setting wordwrap and justification, and then selecting Format
Paragraph. The indentation (tab or offset) of the first line of the
paragraph is preserved.

Automatic conversion of upper-to-lowercase and vice versa is done by
selecting the Uc/lc option of the Misc function to convert the character
at the cursor. CompuView provides a V-SPELL spelling checker for any
files created with VEDIT.

User Interface

All 10 of the function keys, most of the Alt-function-key combinations,
and some Ctrl-key combinations are defined in the two preconfigured
keyboard layouts, default and expert. The normal displayable characters
and the Enter key (Ctrl-M) cannot be redefined, but all of the function
keys, sequences of Ctrl- or Alt-letter combinations, or any escape
sequence can be redefined by running INSTALL and selecting Modify Keyboard

The default assignments can all be cleared or retained. Each edit function
name, followed by the current assignment, is prompted in sequence and can
be retained as is by pressing Enter or reassigned to a different function
key, Ctrl-key combination, or escape sequence.

The normal visual edit mode displays a status line at the top of the
screen that shows the current line number, column number, and filename.
The status line is replaced by the options of any edit function that is
invoked or an error message. Switching into command mode puts "Command" in
the status line and a "Command:" prompt at the bottom of the screen.

You can set the number of lines for paging and lines displayed (three by
default) above and below the cursor as the screen scrolls, the column for
the horizontal scroll margin, and the continuation character (the hyphen
by default) used for automatic wordwrap when the horizontal scroll margin
is reached to different values by running INSTALL.

Reverse video on monochrome systems and the colors on CGA or EGA systems
can be set to new defaults by running Task 10 in INSTALL (Change Screen
Dependent Parameters) or by using the YEA command to change both the
foreground and background colors of the current window.

VEDIT supports any screen size up to 70-lines-by-250-columns. You can set
the size by running INSTALL and changing the number of screen lines and
length of displayed line in Task 10 and the screen line length in Task 11
(Additional Memory Mapped Installation Features) as well as setting
Using High Speed EGA Color Board to Yes.


VEDIT offers such standard page layout capabilities as margins, paragraph
formatting, and justification. CompuView also markets a more
sophisticated page formatter program called V-PRINT (not included with
the editor) that can create headings, tables of contents, and indexes, do
complete paragraph formatting, and interpret page formatter commands in
VEDIT text files.

You can print text by issuing a PR command, which puts the filename and
page number at the top of each page, or by loading the PRINT.VDM macro,
which performs simple print formatting. The only way to print line
numbers, headers, and footers is by modifying the PRINT.VDM macro.

Line numbering can be automatically added to the file by creating a
simple command macro to set the starting line number, then creating a loop
to insert the line number at the beginning of the line, add a specified
increment, and find the next line. VEDIT PLUS does not support special
features, such as different fonts.

Unique Language Features

Language-specific editing features are found under the Misc menu.
Positioning the cursor on the braces, brackets, angle brackets, or
parentheses and selecting the Match Parentheses option moves the cursor
forward or backward to the matching pair, showing any syntax errors.

The automatic indenting mode can be set with the ES command to enable Edit
Switch 3 or by choosing the User function and selecting Auto-Indent. Each
new line will automatically indent the same amount as the previous

Macro Capabilities

VEDIT macros can be stored as command macros in a text register and
executed with the Macro function or the RL command and the M command
followed by the register number to load and execute the macro in the
specified register. You can build keystroke macros by running Task 3 of
the INSTALL program or by using the Define function (Ctrl-D) and executing
the macro by pressing the defined function or Ctrl-key combination.

Text register macros are temporary unless you save the macro or add the
sequence of commands, including a command to copy to a specific register
to the VEDIT.INI file.

A keystroke macro can be any combination of a sequence of command mode
commands, visual mode edit functions, or keystrokes. Text register macros,
however, can only be sequences of one or more commands. The command mode
used with macros is, in effect, a special macro language, since a macro
can combine any series of commands into a single function, like a small

Several macros are provided with the editor and defined in the
preconfigured default keyboard layout, and there are even more with the
expert layout. The documentation also has several examples of command
macros. You can define any number of additional or replacement macros and
assign them to any function key or Ctrl- or Alt-letter combination. With
a little effort, any user-defined macro can be added to the definitions on
the help screens. You can also give a command macro a meaningful name and
save it as a file with the Register Save command, but this is not
possible with a macro that includes functions and recorded keystrokes.

File Handling

The maximum file size for editing in VEDIT is half the available disk
space, since both an input and an output file are required. If the input
and output files are on different drives, the maximum size is the whole
disk. If backward disk buffering is used, the maximum size is one third
of the disk size, since temporary equivalents to input and output files
must be created.

Automatic forward and backward disk buffering handles large files by
reading a section of the file, called a file page, into the edit buffer,
then writing it to an output file when it reaches the end of the edit
buffer and reading in another file page. The size of the file page is 8Kb
by default. Backward disk buffering reads a file page from the output
file back into the edit buffer, writing text from the end of the edit
buffer out to a temporary disk file; forward buffering subsequently
reads first from the temporary file, then from the input file.

You can merge files by using the EG command to specify an entire file or a
line range to extract from one file and insert into the file being edited.
The EL command displays the file or specific portion with line numbers. A
section of text can be written from one file into one of the text
registers and placed in a second file if there is not enough memory to
reserve a 64Kb segment for EA-EZ.

To create windows you split the current window in half horizontally or
vertically. The number of possible windows is limited by the screen size,
since the minimum window size is one line and 15 columns, plus a border,
and there are no overlapping windows. Seventy windows are possible on a
standard 80-column-by-24-line screen.

Windows are independently configurable as to size, number of lines or
columns, and foreground/background colors, and each is given a one-
character name shown in the top border line and highlighted when the
window is active.

The Window function allows you to create a window, specifying top,
bottom, left or right position, number of lines or columns, and the name
of the window. You can also delete a window, switch between windows, and
zoom the current window up to full screen size, which is a very nice

One helpful feature is the option of using the command mode as an on-line
calculator for integer calculations or finding the numeric value of any
ASCII character. The only drawback is that it cannot handle decimal or
fractional values.

VEDIT is seriously limited in that it does not provide for invoking a
compiler from within the editor or returning to the text with the cursor
positioned on the particular text line when a compiler error occurs.


VEDIT is a very good editor but I do have some complaints about it, mostly
concerning the lack of a direct link to a compiler to run from the editor
or return directly to error lines and certain organizational features that
should be improved.

There should only be one preconfigured keyboard layout; providing two
different layouts is unnecessary and confusing. The functions really
ought to be listed alphabetically for easy reference in both the help
screens and the documentation, and specific function and Ctrl-key
combinations should be described in the documentation as default values
that can be changed. The command mode, though very powerful, is
unnecessarily cryptic because the command names are often letter
combinations that are arbitrary rather than mnemonic. The functions that
provide one-line option menus when selected should be identified as such.
Several of these functions, such as Misc or User, have names that give no
indication of the options that lie beneath them.

Still, VEDIT's many options- speed, ability to switch between command and
visual modes, and the ease with which you can customize it to meet your
own needs-make it a powerful, flexible, and efficient editor. It is
highly recommended.

Figure 1:  This sample macro script will compile Microsoft C Version 4.0
           programs and report any errors found.

/*MSCCOMP - this file contains macros for automating the compile and
* error-search procedures under Microsoft 4.0. To compile the
* current buffer, press Alt-C. To search for errors, press Ctrl-N.
* Written by Marc Adler of MAGMA SYSTEMS. January 1987

#define ALT_C   174
#define CTRL_N  14

#define NO      0
#define YES     1

int reached_eof;

  assign_key("compile",    ALT_C);
  assign_key("next_error", CTRL_N);

  string currfile, extension;
  string errmsg;
  int    i;

  errmsg = "Error - the file is not a C file. <ENTER to continue>";

  currfile = filename();

  /* Make sure that the current buffer is a C file */
  if ((i = index(currfile, ".")) > 0)         /* get the extension */
    extension = substr(currfile, i+1, 3);
    if (extension != "c" && extension != "C") /* is it a C file?   */
      get_tty_str(errmsg);                    /* NO!!! Error!!!    */
  else            /* the buffer has no extension, so it's an error */

  writefile(currfile);    /* save the current version & compile it */
  set_buffer_modified(currbuf(), 0);
  os_command(sprintf("msc /AL /Zi /J %s; > errs", currfile));
  next_error();           /* see if there are errors               */

  int id, old_id;
  int n;
  string cl, foo;

  old_id = currbuf();          /* save the id of the source buffer */

  if ((id = find_buffer("errs")) == 0)
    id = create_buffer("errs");
    reached_eof = NO;
  id = setcurrbuf(id);

  /* Microsoft C 4.0 puts out error messages in this format:      */
  /*      filename(line #) : messagetype error# message           */
  if (reached_eof == NO && fsearch("([0-9][0-9]*) : ?*error") > 0)
    n = atoi(substr(cl = currline(), currcol()+1, 5));
                                /* get line # */
    gobol();                    /* move to the next error */
    if (!down())
      reached_eof = YES;
    show_buffer(old_id);        /* go to the source buffer and */
    goline(n);                  /* move to the offending line */
    message(cl);                /* display the error message */
    get_tty_char();             /* and let the user acknowledge */

  else          /* Reached the end of the error file */
    foo = get_tty_str("NO MORE ERRORS");
    delete_buffer(id);          /* get rid of the error buffer */
    show_buffer(old_id);        /* and set the current buffer */

Inside ME

Marc Adler

ME was born out of frustration with current editing environments.
Programmers always have a wish list of features that they would like in
their editing environment, but which their editor does not support.
Magma's goal was to develop an editor that offered both flexibility and
extensibility. Users would then be able to create an environment tailor-
made to meet their specific programming needs.

The ability to reconfigure was at the top of our list when working on the
design for a new editor. This meant the editor needed a macro language
that would allow one to write new commands. Unfortunately, most of the
editors that provide macro capability have LISP or TECO-like macro
languages which are extremely cumbersome to use. The growing
popularity of the C language led us to believe that our greatest
audience would be C programmers, and thus we focused our attention on
their needs. This eliminated the need to put thousands of parentheses in
ME's macro code, as would be required by the LISP syntax. However, a macro
language should be at a "higher level" than a traditional third
generation programming language such as C. The programmer should not
have to worry about pointers, addresses, and memory allocation when
creating a macro. Therefore, we decided that our macro language would
differ from C with respect to the ease of programmability.

We also wanted the macros to run as quickly as possible. Therefore a macro
would need to be compiled into a low-level intermediate code before being
executed by the editor's interpreter. The intermediate code would contain
opcodes for assignment, control flow, and procedure invocation, and would
be low-level enough that an intermediate code file could be turned into an
assembly language routine and linked in with the editor.

Dynamic typing of variables is one feature that saves the macro
programmer a bit of time. Variable declarations are usually at the
programmer's discretion, but in our design the macro interpreter itself
figures out the correct typing at run-time.

String manipulation should be performed as easily as possible. ME's
macro language relieves C programmers from constant worry about memory
allocation when dealing with strings by performing all memory allocation

Linking in C Functions

To expand their environments, users should have access to windowing,
database, and possibly communications libraries. The ability to
integrate these functions at the macro language level is very desirable.
Unfortunately, if all of this functionality were put directly into ME's
kernel, the resulting size would make the editor very unwieldy.

The ability to call external C functions from within the macro language
was added in version 2.0 of ME in order to make the editor truly
reconfigurable. A table was provided to let ME users add C functions to
the editor's database of callable routines. To add an external function to
the macro language, two items are inserted into this table-the name of the
function and a reference to the function. For instance, if a programmer
wants to add a database routine called db_create(), then an entry such as
the following is added to the table:

  { "create_database", db_create }

After adding this entry, the table needs to be recompiled and linked into
the editor.

Whenever a programmer wants to access this function in the macro language,
the line


is coded into the macro program. The macro interpreter, upon seeing this
function, looks at the external function table and calls the function.

OS2 and Porting

ME is written almost completely in the C ANSI standard. This allows the
source to be compiled by any compiler supporting the ANSI standard.

ME will soon support OS/2 protected mode. OS/2's Family API was designed
so that most DOS applications could easily be ported to protected mode.
Since ME uses the standard C memory allocation calls, one immediate
benefit will be the increased file size that the user will be able to
edit. At the macro language level, support will be provided for the user
to access dynamic-link libraries.

Future Directions

Since ME is available with full source code, Magma Software Systems is
anxious to have its users determine our future direction. Various
applications have been written in ME's macro language, including a DBASE
programming system, and a Computer-Aided Software Engineering (CASE)
system. The ability to access external C functions will allow our users
to be more creative in their applications. We will work with our users to
insure that ME will be able to fully support their needs.


Solution Systems
541 Main St., Suite 410B
South Weymouth, MA 02190
(617) 337-6963 or (800) 821-2492

Epsilon Editor
Lugaru Software
5740 Darlington Rd.
Pittsburgh, PA 15217
(412) 421-5911

Mansfield Software Group, Inc.
P.O. Box 432
Storrs, CT 06268
(203) 429-8402

Magma Software Systems
138-23 Hoover Ave.
Jamaica, NY 11435
(201) 792-3954

Phaser Systems, Inc.
115 Sansome St.
San Francisco, CA 94104
(415) 434-3990

The Norton Editor
Peter Norton Computing, Inc.
2210 Wilshire Blvd., #186
Santa Monica, CA 90403
(213) 453-2361

Custom Software Systems (CSS)
P.O. Box 678
Natick, MA 01760
(617) 653-2555

Phoenix Technologies, Ltd.
320 Norwood Park South
Norwood, MA 02062
(617) 769-8310

Uni Press Emacs
Uni Press Software
2025 Lincoln Highway
Edison, NJ 08817
(800) 222-0550

CompuView Products, Inc.
1955 Pauline Blvd.
Ann Arbor, MI 48103
(313) 996-1299

WordPerfect Program Editor
(part of the WordPerfect Library)
WordPerfect Corporation
288 West Center St.
Orem, Utah 84057
(801) 226-7877

Box 3888
Spokane, WA 99220-3888
(509) 624-8088


Vol. 3 No. 3 Table of Contents

SQLWindows Brings a Graphical User Interface to SQL Database Applications

Gupta Technologies, Inc., will soon release SQLWindows, an innovative
developement system that makes it easier to create database applications
by combining the graphic user interface of Windows with the power of
programming in SQL. We talked with the designers of this new product.

The Graphics Programming Interface: A Guide to OS/2 Presentation Spaces

The Graphics Programming Interface (GPI) of the OS/2 Presentation Manager
(PM) introduces many new concepts for programmers. Charles Petzold examines
the three types of presentation space, data structures internal to the PM
that act as a passport to using its graphics functions.

Using OS/2 Semaphores to Coordinate Concurrent Threads of Execution

OS/2 semaphores are an interprocess communication facility for coordinating
concurrent threads of execution. They can serialize access to code and
allow a thread to signal that an event has occured. This article looks at
the four types of semaphores; exclusive system, nonexclusive system, RAM,
and FSRam.

Design Concepts and Considerations in Building an OS/2 Dynamic-Link Library

Dynamic-link libraries are among the most valuable facilities of OS/2. They
permit multiple applications to share only one copy of a function. This
article provides programmers with a road map that demonstrates that writing
a dynamic-link library is not as complicated as it may seem at first glance.

New Compiler Technology Boosts Microsoft(R) QuickBASIC 4.0 Productivity

Using proprietary threaded pseudocode technology, the latest version of
Microsoft QuickBASIC offers the best features of both a compiler and an
interpreter, eliminating the need to choose between the two. It provides
an instant, integrated programming environment.

Debug Microsoft(R) Windows Programs More Effectively with a Simple Utility

Unfortunately, many traditional debugging techniques don't work in the
Windows environment. The DEBUG utility provides print statementlike
debugging within Windows. The utility will also record and display
internal program events and lets you manage the process with a central
control panel.

An Examination of the Operating Principles of the Microsoft Object Linker

The Microsoft Object Linker (LINK) is a powerful development tool that
supports segment ordering, run-time memory management, and dynamic overlays.
We discuss the details of how LINK works to resolve memory references for
more efficient and simpler programs.


This issue's lead article takes and advance look at Gupta's SQLWindows
product, a fine example of systems engineering using Microsoft Windows(R)
to provide a graphical user interface for the building of end-user-programs.
It will be one of the first commercial systems to exploit the client/server
application architecture that is fundamental to distributed processing.

In a short period of time, the SQL language has become an important computer
standard and implementations now exist on a wide variety of mainframe and
minicomputer systems. SQL is also available on personal computer systems
in a variety of incarnations. The Database Manager included in the IBM(R)
Extended Edition of OS/2 may provide the most important implementation of
SQL, since it is part of Systems Application Architecture (SAA). Products
competing with the IBM offering, including the Ashton-Tate/Microsoft SQL
Server, the Oracle Database Add-In for 1-2-3(R), and the Gupta SQL products
discussed here, use the same SQL statements and process them in the same
way. This allows software developers to build compatible applications that
can access data on a variety of servers.

We will present a variety of articles on SQL in coming issues that will
concentrate on how SQL can serve as a bridge between the application you
develop and the variety of SQL databases on local and remote systems.

This issue also takes a look at designing OS/2 dynamic-link libraries,
shareable libraries that run efficiently in a multitasking environment.
A guide to three OS/2 presentation spaces provides some helpful tips on
choosing the right one for your application. Four types of OS/2 semaphores
used for coordinating concurrent threads of execution are explored. We
examine the new compiler technology in Microsoft QuichBASIC Version 4.0
and debug windows applications with a utility that lets you manage program
debugging more effectively.

Look for the DEBUG utility code and all our source code listings on DIAL,
CompuServe(R), and two public access bullitin boards. On the East Coast,
users can call (212) 889-6438 to join the RamNet bulletin board. On the
West Coast, call (415) 284-9151 for the ComOne bulletin board. In either
case, look for the MSJ directory.──Ed.


Editor and Publisher


Technical Editor

Assistant Editor

Production Editor

Staff Editor

Editorial Assistant


Art Director

Associate Art Director


Circulation Manager

Assistant to the Publisher

Administrative Assistant

Copyright(C) 1988 Microsoft Corporation. All rights reserved; reproduction
in part or in whole without permission is prohibited.

Microsoft Systems Journal is a publication of Microsoft Corporation, 16011
NE 36th Way, Box 97017, Redmond, WA 98073-9717. Officers: William H.
Gates, III, Chairman of the Board and Chief Executive Officer; Jon Shirley,
President and Chief Operating Officer; Francis J. Gaudette, Treasurer;
William Neukom, Secretary.

Microsoft Corporation assumes no liability for any damages resulting from
the use of the information contained herein.

Microsoft, MS-DOS, MS XENIX, and CodeView are registered trademarks of
Microsoft Corporation. QuickC is a Trademark of Microsoft Corporation.
CompuServe is a registered trademark of CompuServe Incorporated. dBase
is a registered trademark of Ashton-Tate Corporation. Hayes is a registered
trademark of Hayes Microcomputer Products, Inc. IBM and PC/AT are registered
trademarks of International Business Machines Corporation. Macintosh is a
registered trademark of Apple Computer, Inc. 1-2-3 is a registered trademark
of Lotus Development Corporation. PostScript is a registered trademard of
Adobe Systems, Inc. UNIX is a registered trademark of American Telephone
and Telegraph Company. WordStar is a registered trademark of MicroPro
International Corporation.


SQLWindows Brings a Graphical User Interface to SQL Database Applications

Also see the following related articles:
  SQL Server
  Inside Microsoft Windows

Craig Stinson☼

Gupta Technologies, Inc., the Menlo Park, Calif., vendor of distributed
database software, is preparing to release a one-of-a-kind Microsoft(R)
Windows application called SQLWindows. The product is an application
development system specialized for database engines based on structured
query language (SQL). It is designed to let programmers create data entry
and manipulation tools that combine the user interface sophistication
of Windows on the front end with the power and universality of SQL on the
back end.

Scheduled for shipment in July, SQLWindows is the third link in a triad of
data management products that Gupta Technologies calls The SQL System.
The other two components are SQLBase, a database server tailored for the
80286/80386 environment, and SQLNet, which is a transparent gateway to
IBM's DB2 (future versions will offer connectivity to other mainframe DBMS
engines, as well).

Gupta Technologies was founded in 1984 by Umang P. Gupta and D. Bruce
Scott, two former key players at Oracle Corp. Gupta was the vice president
and general manager of the microcomputer products division at Oracle, and
Scott is coauthor of the Oracle database management system.

The company's first product, SQLBase, made news when it was introduced in
1986 because it was the first true database server, as opposed to a
multiuser DBMS running on a file server, designed specifically to run on
Microsoft-Networks-based microcomputers. An expanded version of this
product recently received attention when Lotus Development Corp.
announced that it would be the engine for the forthcoming Lotus/DBMS

Lotus will, of course, supply its own application development facilities
for Lotus/DBMS. However, because the Lotus engine will be essentially the
same product as SQLBase and will incorporate the same API, applications
written in any language to run under SQLBase will run unchanged under
Lotus/DBMS. This includes applications that are written in Gupta's

"SQLWindows works with SQLBase today and with the future Lotus/DBMS
Server," says Umang Gupta. "And in the future we expect to provide similar
support for other database servers. A design objective from the very
beginning has been to make the product work with other SQL engines."

Gupta stresses that the forthcoming SQL Server from Microsoft and Ashton-
Tate is among the products that are targeted for support.

"SQLWindows is unique," continues Gupta. "There's no other player we know
of that has a product in the market, or close to market, that is
Presentation Manager- or Windows-based, engine-independent, and tied both
to the Windows environment and SQL."

A Presentation Manager version of SQLWindows is also in the works; Gupta
believes it will be ready by the fourth quarter of 1988. After that,
the company plans to turn its attention to the Macintosh(R).

"Our long-term plans," says Gupta, "assume that the platforms will be
PCs, PS/2(R)s running Presentation Manager or Windows, Macintoshes, or
UNIX(R) workstations running some standardized UNIX windowing system."

Covering all the front-end platforms, providing compatibility with the
growing number of SQL engines in the marketplace, and at the same time
expanding SQLNet to provide connectivity with other mainframe DBMS
engines besides DB2, is, to say the least, an ambitious program. Gupta
Technologies currently has about 25 employees, including 12 full-time
programmers. "We plan to increase our development and marketing staff
significantly in the very near future," says Gupta.

Interacting Windows

Gupta and Michael Geary, chief designer of SQLWindows, gave MSJ a look at
a beta version of the company's newest product. SQLWindows consists of
three main components: a visual forms-layout editor (similar to the
dialog editor that comes with the Microsoft Windows Toolkit), an outline
editor, and a programming language called SQLWindows Applications
Language (SAL). The outline editor and layout editor function as
interacting windows: changes made in either window immediately update
the other window.

Figure 1☼ shows a sample SQLWindows application in a very early stage of
development. The programmer has created a sample "About" box. The box is
displayed in the layout editor window (the lower window in Figure 1☼),
while a portion of the corresponding source code is shown as selected in
the outline window.

The solid diamonds in the outline mark headings that have subordinate
entries; the open diamonds indicate entries that can't be expanded
further. In Figure 1☼, the "Contents" entry, which is subordinate to
"Dialog Box: AboutBox", has been expanded to reveal two lines of text and
a single push button.

Figure 2☼ zooms in on a different portion of the source code for the same
"About" box. Here the focus is on the push button, and you can see the
button's title, its location and size attributes, its keyboard
"accelerator" (the keystroke that will be considered equivalent to a
mouse click on the button), and the "message actions" associated with
this push button. In the context of this application there's only one push
button event of interest (the user clicks on it or performs the equivalent
action on the keyboard), therefore this object only needs a single
message action. The message action entry says, in effect, "When the user
clicks, terminate this dialog and return to the parent window." SAM_Click
means the button has been clicked, and SalEndDialog and SalParentWindow
are functions supplied in the SQLWindows Application Language.

Progressive Disclosure

Gupta emphasizes that SQLWindows is designed to serve the needs of
programmers, and the outline approach to source code display is very much
in keeping with that objective.

"A lot of 4GLs take a sort of dialog box approach to developing code,"
he says. "You add a field somewhere, and a dialog box pops up and asks you
all sorts of questions." The drawback to that style of working, asserts
Gupta, is that you can't see your code as it develops because it's all
encapsulated in the dialog boxes. "That might suit an end user because
an end user doesn't necessarily want to see a lot of code. But a
programmer──at least sometimes──needs to be able to see all of it. You have
to give him a means of doing that, a sort of progressive disclosure. An
outline is the appropriate means."

Designer Geary says the idea of incorporating an outline editor into
SQLWindows occurred to him as he was studying documentation for some
other application development systems that were not based on an outline.
"I noticed in a couple of cases that they drew an outline in their
documentation as a way of explaining what they were doing. And I thought,
if they draw an outline to explain what they're doing, let's use an
outline to do what we're doing."

The programmer using SQLWindows can move freely between the outline editor
and the layout window, working in whichever window is more convenient at
any given moment. The windows are simply two representations of a common
underlying data structure; changes made in either window alter the
underlying structure and are immediately reflected in the other window.

Figures 3 and 4☼, illustrate these two ways of working. In Figure 3 the
programmer is inserting a new data field into the layout window, using one
of the tools in the SQLWindows Draw menu. In Figure 4, the programmer has
made a change directly to the outline by pressing the Insert key and

Note that the programmer's action in Figure 4☼ is inappropriate. A form
window cannot be added to a menu, so SQLWindows objects. The
programmer has three choices: to return to the outline and edit the
entry (Esc), to throw out the bogus entry (No), and to accept the entry
(Yes). If Yes is chosen, SQLWindows leaves the entry in place but converts
it into a comment, thus allowing the programmer to attend to the problem

Access to SQL

All access to SQL takes place via 15 functions supplied in SQLWindows.
The list includes SQLImmediate, which immediately executes an SQL
statement; SQLPrepare, which readies an SQL statement for later
execution; SQLConnect, which connects the application to a specified
database; SQLDisconnect; SQLFetch, which retrieves a given row of data;
SQLFetchNext; and SQLFetchPrevious. Figure 5☼ demonstrates the use of the
SQLImmediate function.

The application in Figure 5☼ is a database used by Geary to track bugs in
the prerelease versions of SQLWindows. Figure 6☼ shows a completed form
used as part of this database. The selected section of code is the action
taken by the New command from the application's Bug menu. A validation
test is performed first (ValidateInsert). If that fails, an SAL message
box function is called. Otherwise, two calls to SQLImmediate are made──the
first to determine the next sequential bug number (select max(bugno) +
1), and the second to make the appropriate insertion.

Gupta stresses that the SQLWindows programmer has explicit control of all
SQL statements generated by the system. "In some other 4GLs," he
maintains, "SQL statements are implicitly generated, and the programmer or
user is not actually executing SQL directly. To the extent that that's the
case, the programmer has less control, or no control. In SQLWindows, you
can execute SQL statements to your heart's content, any time you want, in
any order you want."

Internal and External Functions

The call to ValidateInsert in Figure 5☼ is an example of what SQLWindows
terms "internal functions," functions that are not part of the system's
built-in repertoire but can be developed by the programmer using SAL
functions and SQL functions to suit his or her own needs. The system also
provides for external functions, which can be either dynamic-link
libraries or static libraries written in C.

Figure 7☼ shows the declaration of functions stored in two external
libraries, SQLWIN.EXE and USER.EXE, which is part of the Microsoft Windows
Toolkit. Figure 8☼ illustrates the code for an internal function, DoQuery,
that calls SQL functions to prepare, execute, and fetch one row of data.

Why Windows?

Why did Gupta Technologies choose to create an SQL front end in Microsoft
Windows? Ask the designer and the CEO, and you get two variations of the
same answer.

Says Geary, "Windows has the user interface that I like. It's really
oriented toward building applications that are both easy to learn and easy
to use once you've learned them. It also gives us the tools that we can
pass on to our SQLWindows developers to let them build applications that
have a good, crisp feel to them."

Says Gupta, "A major premise of our business is that users want unified
access to personal, departmental, and corporate database systems and
that SQL is the lingua franca that makes this access possible. A second
major premise is that access to database servers will take place from
advanced applications written to take advantage of the next generation
of PCs. And that means from OS/2 Presentation Manager and Microsoft
Windows-and not from dumb terminals."

Clearly, the technology of "industrial-strength database management," as
Gupta calls it, is changing dramatically these days. SQL is emerging as a
standard medium for database access. And the arrival of 80386-based PCs
and high-speed local area networks has enabled database servers running
on PCs to meet departmental needs as cost effectively as minicomputer-
based systems.

Gupta Technologies has demonstrated a strong sense of direction with
these developments. It made an early commitment to SQL and developed
the first PC-based database server product. In Gupta's words, the company
"made a few good calls."

On the front end, advances in interface technology have signaled the slow
beginning of the end for traditional, minicomputer-style, text-based
4GLs. Within the next year or two, we can expect to see a myriad of new
approaches to application development from software houses whose roots are
planted firmly in the microcomputer world. The OS/2 systems and
Presentation Manager will play a dominant role in this development. By
utilizing the Microsoft Windows technology of today, Gupta and
SQLWindows are in the position to be trendsetters in the Presentation
Manager and OS/2 environment of the not-so-far-off future.

SQL Server

SQLWindows is the front-end interface to Gupta's SQLBase. When the Ashton-
Tate/Microsoft SQLServer, a powerful relational database server for PC
local area work groups, becomes available, Gupta plans to modify
SQLWindows to serve as a front-end application for it. SQL Server is based
on technology licensed from Sybase, and runs on top of OS/2-based networks
and communicates with workstations running under OS/2 or DOS 3.2 or

SQL Server is based on a superset of the ANSI standard Structured Query
Language (SQL) that applications use to communicate with relational
databases. SQL is transparent to the user and complements languages such
as dBASE(R), through which users gain access to data.

SQL Server uses a client/server approach that cleanly splits the functions
of a database management system into a back-end (server) component where
data is managed and a front-end (client) component that allows data to be
accessed by users. SQL is used as the access language between the client
and server.

The client workstation communicates with SQL Server over PC-based
networks using TRANSACT-SQL, a superset of ANSI standard SQL with a set of
extensions that include flow control, temporary variables, transaction
management, precompiled SQL queries (stored procedures), and special
stored procedures called triggers.

SQL Server creates the concept of an intelligent database server,
centralizing database intelligence by using stored procedures and
triggers. Stored procedures minimize message and network traffic from the
workstation to the server and can result in performance that is many
times greater than standard SQL queries. Triggers are stored procedures
which are automatically executed or "fired" when database values are
inserted, updated, or deleted. Using stored procedures and triggers, SQL
Server provides a consistent method of maintaining database integrity
across applications.

SQL Server has an open platform architecture thus allowing multiple
workstation applications to access back-end database services. This
gives users the ability to utilize a variety of PC applications and
languages against the same data at the same time. A client application
programming interface (API), DB-Library, may be used by the application
developer to integrate a program with SQL Server.

SQL Server runs as a single multithreaded process under OS/2 with the
capability of managing many users and multiple databases
simultaneously. This results in high transaction throughput and
efficient use of computer resources. A 386-based SQL Server can be
expected to support more than 35 users accessing a 300Mb database with
response time under a second.

With its transaction-oriented DBMS kernel, SQL Server makes the database
constantly available for such administrative tasks as backup, recovery,
database updates, design changes, and integrity rule changes even while
users continue to access the database. SQL Server also maximizes
transaction throughput so that performance, as seen by the individual
user, stays virtually constant as users are added to the network. The
system can be administered from anywhere on the network, so that there is
no need for a separate administrator's console.

Inside Microsoft Windows

Michael Geary, designer of SQLWindows, likes a lot of things about
programming in Microsoft Windows.

"To begin with," he says, "there's the user interface. One of the nice
things about Windows is that the Windows style guide spells out an excellent
user interface in great detail. That makes life easier for the programmer,
and it also makes things much easier for the user. Once someone has learned
one application, it's a lot easier to go learn another one."

Geary said he appreciates the fact that Windows provides the tools to build
its interface. "When I want to write a menu, I don't have to write code to
say how menus work. All the basic user interface tidbits are built in," he

Messages and Child Windows

Geary, who had programmed extensively in both the Macintosh and the MS-
DOS(R) environments before taking on SQLWindows (among his credits is the
respected communications package Transend/PC), was particularly impressed
by two aspects of Windows programming: messages and child windows.

"I'm often asked to compare the Macintosh and Windows," he says. "From the
user's point of view and the programmer's point of view, they're very
similar. But I was really struck with the way Windows took the Mac's
notion of events and generalized that into the message concept. Messages
are like a superset of events. What you can do in Windows that you can't
do on the Mac is make up your own messages and send them from one
application to another. That's what makes DDE [Dynamic Data Exchange]
possible; DDE is just a protocol for sending messages."

As for child windows, there's basically only one kind of window on the
Macintosh, Geary explains. Each window is essentially a peer of all the
others, and once a program has built a window it's up to the programmer to
manage everything that goes on inside it. The Macintosh offers such tools as
push buttons, radio buttons, scroll bars, and so on, but the program has to
manage the details of where they're placed and how they're used.

In Windows, the common objects by means of which the user interacts with the
application, such as the various components of a dialog box, are
typically implemented as child windows. And because child windows behave
essentially the same way parent windows do, a lot of things happen
automatically that would have required explicit programming on the


"Another thing I like about the child window concept," adds Geary, "is that
Windows supplies you with a number of predefined classes of child windows
and then makes it easy for you to subclass them. You just take the
predefined class and add on some behavior of your own by interposing your
own window function in front of the one that comes with it, in effect
filtering out the messages that you want to handle differently."

Geary used this technique extensively in programming SQLWindows. Objects in
the forms-layout editor window have to behave one way when the user of
SQLWindows is developing an application and another way when the end user of
that application is working with it. For example, the developer needs to be
able to click on such objects as push buttons in order to size and position
them; the end user wants them to behave like normal push buttons.

"I was able to do all that split behavior just by using the subclassing
technique," Geary explains. "I put my own window function ahead of the
standard one. At design time I handle many of the messages differently; at
run time, I just pass them through."

Geary says he created a number of window classes from scratch, including a
list window that handles columns as well as rows, somewhat like a
spreadsheet, and can fetch data from a database on the fly (a "regular"
Windows list window requires that all its data be in memory).

"The nice thing about this," he elaborated, "is that my own window classes
coexist with the built-in Windows classes, and there's not much special I
have to do to make my own objects work side by side with pre-existing ones.
The rest of my program treats them all the same; it doesn't care whether
the window was provided by me or by Windows."

Resource Management

Other advantages to programming in Windows involve the management of
memory and the keyboard. Because all code in Windows is relocatable and
discardable, the programmer, according to Geary, is liberated from both
memory fragmentation problems and the need to juggle the trade-offs
associated with code overlays.

"In programs that I've done in the past," he explains, "I've had to use
overlays a lot. And it's always been a pain trying to decide, ╘How heavily
do I overlay this? What's the target machine size? Whom do I optimize this
for?' I've been through that process a number of times and it's awful. You
can't satisfy everybody.

"But in Windows, all you do is break up the program into a lot of code
segments. Each code segment is an independent entity as far as being loaded
into memory is concerned, and Windows just loads in as many as it can fit.
When it runs out of room, it moves them around or discards them as needed.
In other words, it optimizes for all the different machines at once.

"You have to do a little extra work to take advantage of Windows' memory
management, but it's much better than dealing with a fixed-overlay

As for keyboard management, Geary applauds the fact that Windows supplies
the programmer with information that eliminates the need for direct
programming of the hardware.

"In Windows," he said, "keyboard handling is very simple. They send you
messages for everything. With any keypress, you get WM_KEYDOWN. When any
key is released, you get WM_KEYUP. You can distinguish between the first
press of a key and presses that come from the auto repeat mechanism.

"Basically, you can look at the keyboard at whatever level you like, without
being an ill-behaved program. There's no incentive to go to the hardware,
because they've provided all the stuff you want to do in the first place."

Finally, Geary cites the advantages afforded to programmers and users
alike by DDE and the Windows Clipboard. He believes the presence of a well-
defined standard for interapplication communications will have a long-
range impact on the types of software people write.

"Philosophically, the Clipboard and DDE allow people to get away from
building complex all-in-one applications and open up the possibility of
getting back to building smaller programs that work effectively together."


The Graphics Programming Interface: A Guide to OS/2 Presentation Spaces

Charles Petzold☼

Programmers experienced with Microsoft(R) Windows will discover that much of
the OS/2 systems Presentation Manager is familiar territory. The windowing
and user interface portion of the Presentation Manager is obviously derived
from Windows, and many of the window messages are identical.

However, the Graphics Programming Interface (GPI) is mostly new and
offers some very significant enhancements over the Windows Graphics Device
Interface (GDI). GPI is derived largely from IBM's Graphical Data Display
Manager (GDDM) and 3270 Graphics Control Program (GCP) with some
features, such as bitmaps and regions, derived from Windows. GPI also
reflects influence from many other graphics interfaces, ranging from the
Graphical Kernel System (GKS) to PostScript(R).

One of the first hurdles in approaching GPI is dealing with the concept of
the presentation space (PS). What makes it difficult is that the
Presentation Manager supports three different types of presentation
spaces, the "cached micro-PS," the "micro-PS," and the "normal-PS." Which
type of presentation space you choose for your application depends on a
variety of factors, some of which I'll discuss here.

The subject becomes more interesting when you realize that the type of
presentation space you choose determines to some degree how you structure
a Presentation Manager application for drawing. I'll demonstrate this
in three programs, CACHEDPS, MICROPS, and NORMALPS, each of which uses one
of the three available types of presentation space.

Each program displays the same simple graphics shown in Figure 1☼. The text
string "Graphics Programming Interface" is displayed in the center of the
window with dotted lines drawn from the corners of the text string to the
corners of the window. Both the text and the dotted lines are displayed in

GPI Drawing Functions

Before we discuss the presentation space, let's take a quick look at
the functions that these three programs use for drawing the text string
and the dotted lines. The CACHEDPS.C program calls these GPI drawing
functions in its ClientWndProc function during processing of the WM_PAINT

Many of the GPI drawing functions require a point in x and y coordinates,
which is provided to the function as a structure of type POINTL. The
POINTL structure is defined in the OS2DEF.H header file like this:

  typedef struct _POINTL
  LONG x ;
  LONG y ;

The LONG data type is simply a 32-bit unsigned long integer.

To define a POINTL structure in a C program you can use

  struct _POINTL ptl ;

or more simply:

  POINTL ptl ;

By convention, the names of POINTL variables begin with the prefix ptl.

(One big difference between GPI and some other high-level graphics
languages is that GPI does not use floating point. This is for performance
reasons──floating point calculations on personal computers are not yet fast
enough for a highly interactive graphical system such as the Presentation

GPI includes the concept of a current position. To draw a line, the first
step is to set the current position to the beginning of the line. This
requires assigning coordinate values to the x and y fields of the POINTL
structure and calling the GpiMove function:

  ptl.x = ... ;
  ptl.y = ... ;
  GpiMove (hps,&ptl) ;

I'll discuss the hps parameter of GpiMove shortly. GpiMove does not draw
anything; it merely sets the current position. To draw the line, set the
fields of the structure to the end point of the line and call GpiLine:

  ptl.x = ... ;
  ptl.y = ... ;
  GpiLine (hps,&ptl) ;

The GpiLine function also moves the current position to the indicated

It certainly is a nuisance to set two fields of a POINTL structure before
calling GpiMove or GpiLine. However, you'll find that the use of a
structure to represent points offers a nice clean consistency in GPI
function syntax. For example, the GpiQueryCurrentPosition function, which
obtains the current position, has the same syntax as GpiMove:

  GpiQueryCurrentPosition (hps,&ptl) ;

Upon returning from the function, the ptl structure is filled with the
coordinates of the current position.

The use of a structure for points also allows syntax consistency in those
GPI functions that require multiple points. One such function is
GpiPolyLine, which draws a series of lines beginning at the current
position. You define an array of POINTL structures like this:

  POINTL aptl [15] ;

The call to GpiPolyLine requires the number of points and the array name:

  GpiPolyLine (hps,15L, aptl) ;

That GpiPolyLine call is almost equivalent to this code:

  for (i = 0 ; i < 15 ; i++)
  GpiLine (hps,aptl + i) ;

The points you specify in GPI functions are in "world coordinates."
Although GPI supports several transforms to convert world coordinates to
pixels, I'll defer discussion of these transforms. By default, world
coordinates are in units of pixels relative to the lower left corner of
the window. Values on the x (horizontal) axis increase to the right;
values on the y (vertical) axis increase going up.

  CACHEDPS.C uses the function GpiCharStringAt to display the text string:
  GpiCharStringAt (hps, &ptl,lTextLength,cText) ;

Under default conditions, the baseline of the left side of the first
character is positioned at the point given in the ptl structure. The cText
parameter is a pointer to a character string or a character array, and
lTextLength is the length of the string. For example, this code displays
the text "Hello" at the point (10, 10):

  ptl.x = 10 ;
  ptl.y = 10 ;
  GpiCharStringAt (hps, &ptl,5L,"Hello") ;

By default, the font that GPI uses is the normal system font, the same
font that appears in Presentation Manager title bars, menus, and dialog

GPI Attributes

CACHEDPS.C calls two functions to set the attributes that GPI uses when
drawing. Before drawing anything, the program calls GpiSetColor:

  GpiSetColor (hps,CLR_RED) ;

This causes all text and lines displayed after this function call to be
colored red. Before displaying the four lines, CACHEDPS.C calls another
attribute function to set a dotted line type style:

  GpiSetLineType (hps, LINETYPE_DOT) ;

GPI also has functions that obtain information from the system. One of
the query functions used in CACHEDPS.C is GpiQueryTextBox. This function
fills a POINTL array with the coordinate positions of the four corners of
an imaginary box that would surround a particular character string if
the string were displayed at the point (0, 0). The CACHEDPS program uses
this information to draw the four lines.

Near the beginning of WM_PAINT processing, CACHEDPS.C makes a call to
GpiCreateLogColorTable. In the version of the Presentation Manager I used
for this article, the default background color is black and the default
foreground color is white. Thus, the GpiErase function erases the window
to black. The call to GpiCreateLogColorTable in CACHEDPS reverses this so
that the default background color is white and the foreground color is
black. Whether this function will be necessary in the final version of the
Presentation Manager is not clear as of this writing.

The calls to GpiRestorePS, GpiSavePS, and GpiResetPS in CACHEDPS.C during
the WM_PAINT message are temporary fixes to avoid some bugs in the
version of the Presentation Manager I have. These functions should not
be necessary when the Presentation Manager is in final shape.

The PS and DC

The first parameter to every GPI function in CACHEDPS.C is the variable
named hps. This variable is defined as type HPS, a handle to a
presentation space.

Before a Presentation Manager program can draw on the display, it must
either create a presentation space or use a presentation space that has
already been created by the Presentation Manager. How you do this depends
on the type of presentation space you decide to use.

The presentation space is basically a data structure internal to the
Presentation Manager. All the attributes of the presentation space──the
current position, the current background and foreground colors, the
current line style type, transforms, and others──are stored here. For the
normal-PS, the presentation space can also store collections of GPI
function calls.

A Windows programmer might at first assume that the presentation space is
equivalent to the Windows display context (DC), but they are really two
different things. In fact, the Presentation Manager includes the
concept of a display context.

In the Presentation Manager, the display context usually describes a
physical output device, such as the video display, printer, or plotter. A
display context can also be a block of memory that is used as if it were
an output device; it can also be a metafile. You can think of the display
context as comprising the output device and its device driver.

A presentation space does not itself imply any type of output device.
Instead, the presentation space must be associated with a particular
display context. The reason for the separation of the presentation space
and the display context will be evident later. The conceptual
relationship between an application program, the presentation space, and
the display context is shown in Figure 2.

Of the three different types of presentation spaces supported by the
Presentation Manager, normal-PS, sometimes also called the full-PS, is the
most versatile, with micro-PS second and cached micro-PS being the least

To use a normal-PS or micro-PS, a program must create the presentation
space by calling the function GpiCreatePS. Strictly speaking, these are
the only two types of presentation space supported by GPI. The cached
micro-PS is provided courtesy of the component of the Presentation
Manager concerned with the windowing and user interface functions,
sometimes called the "Win" or "User" component. The Presentation Manager
creates the micro-PS for you.

The Cached Micro-PS

The CACHEDPS program shown in Figure 3 uses a cached micro-PS. (For more
information on cached micro-PS see Microsoft(R) Operating System/2 Software
Development Kit, Presentation Manager Specification, Vol. 1, p. 210) A
program can obtain a handle to a cached micro-PS by calling WinBeginPaint
or WinGetPS. CACHEDPS.C uses both of these functions.

You can call the WinGetPS function while processing any window message.
After you're done with the presentation space you release it by calling

  hps = WinGetPS (hwnd) ;
  < use GPI functions >
  WinReleasePS (hps) ;

CACHEDPS calls WinGetPS during the WM_CREATE message. The only GPI
function the program calls at that time is GpiQueryTextBox. CACHEDPS needs
to obtain the dimensions of the character box only once because the height
and width of the text string will not change while the program is

The functions WinGetPS and WinReleasePS must be called as a pair while
processing a single message. Do not call WinGetPS while processing one
message and WinReleasePS while processing another.

During processing of a WM_PAINT message, a program calls WinBeginPaint to
obtain a handle to a cached micro-PS and WinEndPaint to release the

  hps = WinBeginPaint (hwnd, NULL,NULL) ;
  < use GPI functions >
  WinEndPaint (hps) ;

A cached micro-PS has several important characteristics. First, the cached
micro-PS is always associated with the video display, or more precisely,
with a window on the video display. That makes the cached micro-PS easy to
use (WinGetPS requires only one parameter) but also limits it to the screen.

Second, all attributes of the presentation space are set to their default
values when the handle is obtained by calling WinGetPS or WinBeginPaint.
This is why CACHEDPS.C sets the color to red and the line style type to
dotted when it processes the WM_PAINT message.

Finally, when using the cached micro-PS a program usually draws in units
of pixels. You can change that with a few calls to certain GPI functions,
but it is not quite as easy as when a program creates its own
presentation space by calling GpiCreatePS.

Windows programmers will note that CACHEDPS is structured much like a
Windows program. If you're converting a program from Windows to the
Presentation Manager, you may want to use a cached micro-PS to ease the

The WM_PAINT Message

CACHEDPS does all of its drawing while processing the WM_PAINT message.
Handling the WM_PAINT message correctly is an important structural
consideration in both Windows programs and Presentation Manager

A window procedure receives a WM_PAINT message whenever an area of the
window has become invalid, meaning that the area no longer contains what
the program originally drew there. If a portion of a window has been
covered and then becomes uncovered, the Presentation Manager will post a
WM_PAINT message to that window. A window procedure can also receive a
WM_PAINT message when the window has been changed in size.

Although a Presentation Manager program can draw on a window at almost any
time, the program must be able to update the entire window on receipt of a
WM_PAINT message. Programmers often satisfy this requirement by doing all
window painting during the WM_PAINT message and none at any other time.
As you'll see, dealing with WM_PAINT messages becomes somewhat simpler
with the normal-PS.

The Micro-PS

Because the cached micro-PS is always associated with a window on the
video display, you cannot use a cached micro-PS for drawing on another
output device, such as a printer or a plotter. To use a printer or
plotter you must create either a micro-PS or normal-PS. A program can
also create a micro-PS for drawing on the program's window. The MICROPS
program in Figure 4 shows how this is done.

MICROPS.C creates the presentation space by calling GpiCreatePS while
processing the WM_CREATE message in ClientWndProc. GPIT_MICRO in the
GpiCreatePS function tells GPI to create a micro-PS. GpiCreatePS also
requires a handle to a display context with which the presentation space
will be associated. For a video display window, this display context
handle is obtained from WinOpenWindowDC:

  hdc = WinOpenWindowDC (hwnd) ;

The presentation space includes an imaginary drawing surface called the
"presentation page." (I promise that this is the last term I'll be
presenting here.) You define the size and units of the presentation page
in the GpiCreatePS function. You set the size of the page by the SIZEL
structure. Setting the cx and cy fields to 0 makes the page size the same
as the size of the entire video display. The PU_PELS identifier indicates
that the coordinates of the page are in units of pixels. You can instead
use one of the identifiers shown in Figure 5 for units other than pixels.

Using a pixel-coordinate page is often the best approach for simple
graphics output. Information obtained from the Presentation Manager
windowing system, such as the size of the window obtained during the
WM_SIZE message, is always in units of pixels regardless of the
presentation page size. You would have to convert those coordinates and
sizes to the page units using GpiConvert.

The presentation space handle obtained from GpiCreatePS is valid until the
presentation space is destroyed by a call to GpiDestroyPS. MICROPS
destroys the presentation space while it's processing the WM_DESTROY
message, which is the last message the window procedure receives. Until
that time, the hps value must be used during all WM_PAINT and WM_SIZE
messages, so it is stored in a static variable.

Note that MICROPS sets the color and line style type during WM_CREATE
processing rather than during WM_PAINT processing. The micro-PS exists
until it is explicitly destroyed so any GPI attributes that a program sets
remain in the presentation space until they are changed. In contrast, a
cached micro-PS is reset to default attributes each time a program
obtains it.

The syntax of the function WinBeginPaint for a micro-PS is a little
different from that for a cached micro-PS. Rather than use WinBeginPaint
to obtain the presentation space handle, the existing handle is passed to
the function as the second parameter. (In the version of the Presentation
Manager that I used for this article, WinBeginPaint and WinEndPaint were not
working correctly; that's why they are shown in MICROPS.C between comment

Windows programmers, watch out! You'll find it natural to do something
like this during the WM_PAINT message when using a cached micro-PS:

  hps = WinBeginPaint (hwnd, NULL,NULL) ;
  < draw using default colors >
  GpiSetColor (hps,CLR_RED) ;
  < draw using red >
  WinEndPaint (hps) ;

However, if you switch to a micro-PS, you might first alter the code like

  WinBeginPaint (hwnd,hps, NULL) ;
  < draw using default colors >
  GpiSetColor (hps,CLR_RED) ;
  < draw using red >
  WinEndPaint (hps) ;

This will work fine during the first WM_PAINT message but not during
subsequent messages because the color will still be set to red. Remember
that the micro-PS retains all GPI attributes that you change until you
change them again. You can avoid such problems by using GpiSavePS and
GpiRestorePS to save and restore the GPI state:

  WinBeginPaint (hwnd,hps, NULL) ;
  < draw using default
  colors >
  GpiSavePS (hps) ;
  GpiSetColor (hps,CLR_RED) ;
  < draw using red >
  GpiRestorePS (hps,-1L) ;
  WinEndPaint (hps) ;

Or you can explicitly set the color back to the default value at the end
of WM_PAINT processing:

  WinBeginPaint (hwnd, hps, NULL) ;
  < draw using default colors >
  GpiSetColor (hps,CLR_RED) ;
  < draw using red >
  GpiSetColor (hps, CLR_DEFAULT) ;
  WinEndPaint (hps) ;

Comparing CACHEDPS.C and MICROPS.C listings reveals only a subtle change
in structure-two GPI attribute functions called during the WM_PAINT message
in CACHEDPS are handled in MICROPS during the WM_CREATE message. The real
change in structure occurs when you move up to the normal-PS.

The Normal-PS

The micro-PS gives a program access to only a subset of the GPI
functions. Although it is a very substantial subset and includes all the
basics, it excludes all functions that pertain to GPI "segments"; these
are supported only by the normal-PS.

A segment (not to be confused with a memory segment in 80286
microprocessor architecture) is a stored collection of graphics drawing
and attribute commands. You open a segment, you call a bunch of GPI
functions, and then you close the segment. All the functions are stored
in the segment. You can then cause these commands to be drawn on the
output device by issuing one of several functions such as GpiDrawChain.
The NORMALPS program shown in Figure 6 gives you a little taste of

The graphics output of NORMALPS depends only on the size of the window.
So, during the WM_SIZE message, a segment is created by a call to
GpiOpenSegment. The attribute and drawing commands are called just as they
were in the two earlier programs. But these graphics orders are not
displayed just yet; they are instead saved in the segment. During the
WM_PAINT message, the segment is displayed by calling GpiDrawChain.
Obviously this simplifies WM_PAINT processing considerably.

You can create many segments in your program; they are saved as part of
the presentation space. A segment can be either chained or unchained.
The root chain consists of all chained segments; GpiDrawChain draws all
the segments in the root chain. Unchained segments can be called from
other segments. When an unchained segment is called, the coordinate
positions specified in all the drawing commands in the segment can be
transformed with translation, scaling, or rotation, which is useful for
graphical modeling.

You can edit segments, change the order in which segments in the root
chain are drawn, and test whether an object drawn in a segment is within a
specified radius of a particular point. This is very handy for mouse hit-

Besides the support of segments, the normal-PS has another advantage over
the micro-PS. It is the only presentation space that can be
reassociated with another display context; that is, you can disassociate
the presentation space from the video display and then associate it with
the printer. Because the segments are part of the presentation space,
you need not recreate them. The segments are ready to be drawn on the

Of course, the use of segments is overkill in NORMALPS, as it would be in
many other small Presentation Manager programs. The basic rule is to use
the simplest type of presentation space that meets your needs. If you
can do everything you need to do using the cached micro-PS, use that.
Don't take a full set of luggage on an overnight trip.

Figure 2:  The relatonship between an application program, the presentation
           space, and the display context.

         │                                                             │█
         │         ╔══════════════════════════════════════════╗        │█
         │         ║            Application Program           ║        │█
         │         ╚════════════════════╤═════════════════════╝        │█
         │                         draws to the                        │█
         │                              ↓                              │█
         │         ╔══════════════════════════════════════════╗        │█
         │         ║             Presentation Space           ║        │█
         │         ╚════════════════════╤═════════════════════╝        │█
         │                   which is associated with a                │█
         │                              ↓                              │█
         │         ╔══════════════════════════════════════════╗        │█
         │         ║              Display Context             ║        │█
         │         ╚════════════════════╤═════════════════════╝        │█
         │           which causes output to be displayed on a          │█
         │                              ↓                              │█
         │         ╔══════════════════════════════════════════╗        │█
         │         ║          Physical Output Device          ║        │█
         │         ╚══════════════════════════════════════════╝        │█

Figure 3:


cachedps.obj : cachedps.c
     cl -c -G2sw -W2 -Zp cachedps.c

cachedps.exe : cachedps.obj cachedps.def
     link cachedps, /align:16, NUL, os2, cachedps

CACHEDPS.DEF Module Definition File

DESCRIPTION   'Demonstrates Cached Micro-PS(C) Charles Petzold, 1988'
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        ClientWndProc

CACHEDPS.C-Demonstrates Cached Micro-PS

#define INCL_GPI

#include <os2.h>
#include <stddef.h>


main ()
    static CHAR szClientClass [] = "CachedPS" ;
    HAB    hab ;
    HMQ    hmq ;
    HWND   hwndFrame, hwndClient ;
    QMSG   qmsg ;

     hab = WinInitialize (0) ;
     hmq = WinCreateMsgQueue (hab, 0) ;
     WinRegisterClass (hab, szClientClass, ClientWndProc,
                                           CS_SIZEREDRAW, 0) ;

     hwndFrame = WinCreateStdWindow (HWND_DESKTOP,
                               | FS_SYSMENU    | FS_MINMAX,
                  szClientClass, "Cached Micro-PS",
                    0L, NULL, 0, &hwndClient) ;

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

     WinDestroyWindow (hwndFrame) ;
     WinDestroyMsgQueue (hmq) ;
     WinTerminate (hab) ;

     return 0 ;

MRESULT EXPENTRY ClientWndProc (hwnd, msg, mp1, mp2)
     HWND          hwnd ;
     USHORT        msg ;
     MPARAM        mp1 ;
     MPARAM        mp2 ;
     static CHAR   szText []  = "Graphics Programming Interface" ;
     static LONG   lTextLength    = sizeof szText - 1L ;
     static LONG   alColorData [] = { CLR_BACKGROUND, RGB_WHITE,
                                      CLR_NEUTRAL,    RGB_BLACK } ;
     static POINTL ptlTextStart, aptlLineStart [4],
                   aptlTextBox [TXTBOX_COUNT] ;
     static SHORT  cxClient, cyClient ;
     HPS           hps ;
     POINTL        ptl ;
     SHORT         sIndex ;

     switch (msg)
          case WM_CREATE:
               hps = WinGetPS (hwnd) ;

               GpiQueryTextBox (hps, lTextLength, szText,
                                TXTBOX_COUNT, aptlTextBox) ;

               WinReleasePS (hps) ;
               break ;

          case WM_SIZE:
               cxClient = LOUSHORT (mp2) ;
               cyClient = HIUSHORT (mp2) ;

               ptlTextStart.x = (cxClient -
                             aptlTextBox [TXTBOX_BOTTOMRIGHT].x -
                             aptlTextBox [TXTBOX_BOTTOMLEFT].x) / 2 ;

               ptlTextStart.y = (cyClient -
                             aptlTextBox [TXTBOX_TOPLEFT].y -
                             aptlTextBox [TXTBOX_BOTTOMLEFT].y) / 2 ;

               for (sIndex = 0 ; sIndex < 4 ; sIndex ++)
                    aptlLineStart [sIndex] = aptlTextBox [sIndex] ;
                    aptlLineStart [sIndex].x += ptlTextStart.x ;
                    aptlLineStart [sIndex].y += ptlTextStart.y ;
               break ;

          case WM_PAINT:
               hps = WinBeginPaint (hwnd, NULL, NULL) ;

               GpiSavePS (hps) ;                       /* temp fix */
               GpiResetPS (hps, GRES_ATTRS) ;          /* temp fix */
               GpiCreateLogColorTable (hps, LCOL_RESET,
                  LCOLF_INDRGB, 0L, 4L, alColorData) ; /* temp fix */

               GpiErase (hps) ;

               GpiSetColor (hps, CLR_RED) ;

               GpiCharStringAt (hps, &ptlTextStart,
                                   lTextLength, szText) ;

               GpiSetLineType (hps, LINETYPE_DOT) ;

               GpiMove (hps, aptlLineStart + TXTBOX_BOTTOMLEFT) ;
               ptl.x = 0 ;
               ptl.y = 0 ;
               GpiLine (hps, &ptl) ;

               GpiMove (hps, aptlLineStart + TXTBOX_BOTTOMRIGHT) ;
               ptl.x = cxClient ;
               GpiLine (hps, &ptl) ;

               GpiMove (hps, aptlLineStart + TXTBOX_TOPRIGHT) ;
               ptl.y = cyClient ;
               GpiLine (hps, &ptl) ;

               GpiMove (hps, aptlLineStart + TXTBOX_TOPLEFT) ;
               ptl.x = 0 ;
               GpiLine (hps, &ptl) ;

               GpiRestorePS (hps, -1L) ;     /* temp fix */

               WinEndPaint (hps) ;
               break ;

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

Figure 4:


microps.obj : microps.c
     cl -c -G2sw -W2 -Zp microps.c

microps.exe : microps.obj microps.def
     link microps, /align:16, NUL, os2, microps

MICROPS.DEF Module Definition File

NAME           MICROPS
DESCRIPTION   'Demonstrates Micro-PS(C) Charles Petzold, 1988'
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        ClientWndProc

MICROPS.C-Demonstrates Micro-PS

#define INCL_WIN
#define INCL_GPI

#include <os2.h>
#include <stddef.h>


HAB  hab ;

main ()
    static CHAR szClientClass [] = "MicroPS" ;
    HMQ    hmq ;
    HWND   hwndFrame, hwndClient ;
    QMSG   qmsg ;

    hab = WinInitialize (0) ;
    hmq = WinCreateMsgQueue (hab, 0) ;
    WinRegisterClass (hab, szClientClass, ClientWndProc,
                                          CS_SIZEREDRAW, 0) ;

    hwndFrame = WinCreateStdWindow (HWND_DESKTOP,
                              | FS_SYSMENU    | FS_MINMAX,
                 szClientClass, "Micro-PS",
                   0L, NULL, 0, &hwndClient) ;

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

    WinDestroyWindow (hwndFrame) ;
    WinDestroyMsgQueue (hmq) ;
    WinTerminate (hab) ;

    return 0 ;

MRESULT EXPENTRY ClientWndProc (hwnd, msg, mp1, mp2)
    HWND          hwnd ;
    USHORT        msg ;
    MPARAM        mp1 ;
    MPARAM        mp2 ;
    static CHAR   szText []  = "Graphics Programming Interface" ;
    static HPS    hps ;
    static LONG   lTextLength   = sizeof szText - 1L ;
    static LONG   alColorData [] = { CLR_BACKGROUND, RGB_WHITE,
                                     CLR_NEUTRAL,    RGB_BLACK } ;
    static POINTL ptlTextStart, aptlLineStart [4],
                  aptlTextBox [TXTBOX_COUNT] ;
    static SHORT  cxClient, cyClient ;
    HDC           hdc ;
    POINTL        ptl ;
    SHORT         sIndex ;
    SIZEL         sizl ;

    switch (msg)
         case WM_CREATE:
              hdc = WinOpenWindowDC (hwnd) ;

              sizl.cx = 0 ;
              sizl.cy = 0 ;

              hps = GpiCreatePS (hab, hdc, &sizl, PU_PELS |
                                  GPIF_DEFAULT | GPIT_MICRO |
                                  GPIM_NORMAL  | GPIA_ASSOC) ;

              GpiCreateLogColorTable (hps, LCOL_RESET,
                  LCOLF_INDRGB, 0L, 4L, alColorData) ; /* temp fix */

              GpiQueryTextBox (hps, lTextLength, szText,
                               TXTBOX_COUNT, aptlTextBox) ;

              GpiSetColor (hps, CLR_RED) ;
              GpiSetLineType (hps, LINETYPE_DOT) ;
              break ;

         case WM_SIZE:
              cxClient = LOUSHORT (mp2) ;
              cyClient = HIUSHORT (mp2) ;

              ptlTextStart.x = (cxClient -
                             aptlTextBox [TXTBOX_BOTTOMRIGHT].x -
                             aptlTextBox [TXTBOX_BOTTOMLEFT].x) / 2 ;

              ptlTextStart.y = (cyClient -
                             aptlTextBox [TXTBOX_TOPLEFT].y -
                             aptlTextBox [TXTBOX_BOTTOMLEFT].y) / 2 ;

              for (sIndex = 0 ; sIndex < 4 ; sIndex ++)
                   aptlLineStart [sIndex] = aptlTextBox [sIndex] ;
                   aptlLineStart [sIndex].x += ptlTextStart.x ;
                   aptlLineStart [sIndex].y += ptlTextStart.y ;

              break ;

         case WM_PAINT:
              /* BeginPaint (hwnd, hps, NULL) ; */

              GpiErase (hps) ;

              GpiCharStringAt (hps, &ptlTextStart,
                                  lTextLength, szText) ;

              GpiMove (hps, aptlLineStart + TXTBOX_BOTTOMLEFT) ;
              ptl.x = 0 ;
              ptl.y = 0 ;
              GpiLine (hps, &ptl) ;

              GpiMove (hps, aptlLineStart + TXTBOX_BOTTOMRIGHT) ;
              ptl.x = cxClient ;
              GpiLine (hps, &ptl) ;

              GpiMove (hps, aptlLineStart + TXTBOX_TOPRIGHT) ;
              ptl.y = cyClient ;
              GpiLine (hps, &ptl) ;

              GpiMove (hps, aptlLineStart + TXTBOX_TOPLEFT) ;
              ptl.x = 0 ;
              GpiLine (hps, &ptl) ;

              /* EndPaint (hps) ; */

              WinValidateRect (hwnd, NULL, FALSE) ; /* temp fix */
              break ;

         case WM_DESTROY:
              GpiDestroyPS (hps) ;
              break ;

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

Figure 5:  The identifiers used in the GpiCreatePS function to set the units
           of the presentation page.

Page Units
Identifier        Units

PU_ARBITRARY      Pixels (with adjustment)
PU_PELS           Pixels
PU_LOMETRIC       0.1 millimeter
PU_HIMETRIC       0.01 millimeter
PU_LOENGLISH      0.01 inch
PU_HIENGLISH      0.001 inch
PU_TWIPS          1/1440 inch

Figure 6:


normalps.obj : normalps.c
     cl -c -G2sw -W2 -Zp normalps.c

normalps.exe : normalps.obj normalps.def
     link normalps, /align:16, NUL, os2, normalps

NORMALPS.DEF Module Definition File

DESCRIPTION   'Demonstrates Normal-PS(C) Charles Petzold, 1988'
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS       ClientWndProc

NORMALPS.C-Demonstrates Normal-PS

#define INCL_WIN
#define INCL_GPI

#include <os2.h>
#include <stddef.h>


HAB  hab ;

main ()
     static CHAR szClientClass [] = "NormalPS" ;
     HMQ    hmq ;
     HWND   hwndFrame, hwndClient ;
     QMSG   qmsg ;

     hab = WinInitialize (0) ;
     hmq = WinCreateMsgQueue (hab, 0) ;
     WinRegisterClass (hab, szClientClass, ClientWndProc,
                                           CS_SIZEREDRAW, 0) ;

     hwndFrame = WinCreateStdWindow (HWND_DESKTOP,
                               | FS_SYSMENU    | FS_MINMAX,
                  szClientClass, "Normal-PS",
                    0L, NULL, 0, &hwndClient) ;

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

   WinDestroyWindow (hwndFrame) ;
   WinDestroyMsgQueue (hmq) ;
   WinTerminate (hab) ;

     return 0 ;

MRESULT EXPENTRY ClientWndProc (hwnd, msg, mp1, mp2)
     HWND          hwnd ;
     USHORT        msg ;
     MPARAM        mp1 ;
     MPARAM        mp2 ;
     static CHAR   szText []      = "Graphics Programming Interface" ;
     static HPS    hps ;
     static LONG   lSegmentName   = 1 ;
     static LONG   lTextLength    = sizeof szText - 1L ;
     static LONG   alColorData [] = { CLR_BACKGROUND, RGB_WHITE,
                                      CLR_NEUTRAL,    RGB_BLACK } ;
     static POINTL aptlTextBox [TXTBOX_COUNT] ;
     HDC           hdc ;
     POINTL        ptl, ptlTextStart, aptlLineStart [4] ;
     SHORT         cxClient, cyClient, sIndex ;
     SIZEL         sizl ;

     switch (msg)
          case WM_CREATE:
               hdc = WinOpenWindowDC (hwnd) ;

               sizl.cx = 0 ;
               sizl.cy = 0 ;

               hps = GpiCreatePS (hab, hdc, &sizl, PU_PELS    |
                                   GPIF_DEFAULT | GPIT_NORMAL |
                                   GPIM_NORMAL  | GPIA_ASSOC) ;

               GpiCreateLogColorTable (hps, LCOL_RESET,
                  LCOLF_INDRGB, 0L, 4L, alColorData) ; /* temp fix */

               GpiQueryTextBox (hps, lTextLength, szText,
                                TXTBOX_COUNT, aptlTextBox) ;

               GpiSetDrawControl (hps, DCTL_ERASE, DCTL_ON) ;
               GpiSetDrawingMode (hps, DM_RETAIN) ;
               break ;

          case WM_SIZE:
               cxClient = LOUSHORT (mp2) ;
               cyClient = HIUSHORT (mp2) ;

               ptlTextStart.x = (cxClient -
                             aptlTextBox [TXTBOX_BOTTOMRIGHT].x -
                             aptlTextBox [TXTBOX_BOTTOMLEFT].x) / 2 ;

               ptlTextStart.y = (cyClient -
                             aptlTextBox [TXTBOX_TOPLEFT].y -
                             aptlTextBox [TXTBOX_BOTTOMLEFT].y) / 2 ;

               for (sIndex = 0 ; sIndex < 4 ; sIndex ++)
                    aptlLineStart [sIndex] = aptlTextBox [sIndex] ;
                    aptlLineStart [sIndex].x += ptlTextStart.x ;
                    aptlLineStart [sIndex].y += ptlTextStart.y ;

               GpiDeleteSegment (hps, lSegmentName) ;

               GpiOpenSegment (hps, lSegmentName) ;
                    GpiSetColor (hps, CLR_RED) ;

                    GpiCharStringAt (hps, &ptlTextStart,
                                     lTextLength, szText) ;

                    GpiSetLineType (hps, LINETYPE_DOT) ;

                    GpiMove (hps,aptlLineStart + TXTBOX_BOTTOMLEFT) ;
                    ptl.x = 0 ;
                    ptl.y = 0 ;
                    GpiLine (hps, &ptl) ;

                    GpiMove (hps,aptlLineStart + TXTBOX_BOTTOMRIGHT);
                    ptl.x = cxClient ;
                    GpiLine (hps, &ptl) ;

                    GpiMove (hps, aptlLineStart + TXTBOX_TOPRIGHT) ;
                    ptl.y = cyClient ;
                    GpiLine (hps, &ptl) ;

                    GpiMove (hps, aptlLineStart + TXTBOX_TOPLEFT) ;
                    ptl.x = 0 ;
                    GpiLine (hps, &ptl) ;
               GpiCloseSegment (hps) ;
               break ;

          case WM_PAINT:
               /* WinBeginPaint (hwnd, hps, NULL) ; */

               GpiDrawChain (hps) ;

               /* WinEndPaint (hps) ; */

               WinValidateRect (hwnd, NULL, FALSE) ;   /* temp fix */
               break ;

          case WM_DESTROY:
               GpiDeleteSegment (hps, lSegmentName) ;
               GpiAssociate (hps, NULL) ;
               GpiDestroyPS (hps) ;
               break ;

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


Using OS/2 Semaphores to Coordinate Concurrent Threads of Execution

Kevin Ruddell

OS/2 semaphores are a powerful and general mechanism for coordinating
multiple concurrent threads of execution. One common use of semaphores
is to serialize access to pieces of code and the resources referenced by
the code. The other common use of semaphores is to allow one thread to
signal another thread that an event has occurred, for example, that an I/O
operation has been completed. The OS/2 systems fully support both these
uses of semaphores.

There are four types of semaphores in OS/2: exclusive system,
nonexclusive system, RAM, and FSRam (Fast-Safe Ram). FSRam semaphores are
a new addition to Version 1.1 of OS/2. Nonexclusive system and RAM
semaphores can be used either for serialization or signaling, while
exclusive system and FSRam semaphores are intended only for
serialization. FSRam semaphores are intermediate in safety and their
performance is comparable to that of RAM semaphores.

System semaphores can be used by threads of different processes that do
not share memory and they offer some safety advantages over the other two
types. Future versions of OS/2 will provide additional protection
mechanisms and network support for system semaphores.

When access to a reusable resource, such as a file, a data structure, or
the screen must be serialized, we associate a semaphore with the resource
and require that the semaphore have only one owner at a time. A thread
then attempts to gain ownership of a semaphore by issuing a DosSemRequest
or DosFSRamSemRequest call. If the semaphore is unowned, the call returns
a value of zero and the calling thread now owns the semaphore. When the
thread is already owned, the call either returns immediately (if the
Timeout parameter is zero) or else waits n milliseconds (if Timeout = n)
or it waits indefinitely (if Timeout = -1). We say the thread is blocked
if it must wait for the completion of a semaphore request. If the
requesting thread gains ownership of the semaphore before the timeout
period expires, it immediately returns a zero value. Otherwise, it waits
until the timeout period has elapsed and returns a non-zero value. A
thread surrenders ownership of a semaphore by issuing a DosSemClear or
DosFSRamSemClear call.

FSRam and exclusive system semaphores (system semaphores where the
NoExclusive option was not selected in the DosCreateSem request that
created the semaphore) are well suited to serialization. RAM semaphores
and nonexclusive system semaphores can provide serialization, but you
should follow some guidelines when using them.

Whether semaphores are used for serialization or for signaling, they can
cause the hazards of deadlock, violations of serialization, and process
termination while owning resources. By careful analysis of the
application, knowledge of the OS/2 semaphore functions, and employment
of some simple rules, these hazards can be avoided.

Basic semaphore operation for serialization of reusable resources.

             ─┐ ╔═══════════╗       DosSemRequest
sem={1,2,...n}├─╢THREAD{sem}╟───► DosFSRamSemRequest
              │ ║issues call║     ────────┬─────────
     THREAD{1}│ ╚═══════════╝             ▼
     THREAD{2}│                           R  ◄───────────────────┐
        ∙     │                           ▼                      │
        ∙     │                 THREAD{sem} owns  R              │
        ∙     │         ───────────────────────────────────      │
     THREAD{n}│         When THREAD{sem} owns the resource       │
            ─┘         all other threads requesting access      │
        │                      to  R  are blocked                │
                        ──────────────────┬────────────────      │
        │                        ╔════════▼═════════╗            │
                                 ║THREAD{sem} issues║            │
        │                        ║    DosSemClear   ║            │
    ╔═════════════════════╗      ║ DosFSRamSemClear ║            │
    ║ THREAD{sem} either  ║      ╚══════════════════╝            ▼
    ║ reissues a request  ║    ────────────────────────    ╔════════════╗
◄───║ for  R  or goes off ║◄───THREAD{sem} relinquishes───►║     R      ║
    ║to do something else.║        ownership of  R         ║  becomes   ║
    ╚═════════════════════╝    ────────────────────────    ║ availiable ║
    R  = Reusable Resource and an associated semaphore     ╚════════════╝

Semaphore Types

When you write a program with semaphores, you should ask yourself two
questions: Is this semaphore for serialization or signaling? Will it be
used between threads of one process or between different processes? The
answers to these questions indicate which type of semaphore you should
use and the system calls with which you will access it. Generally, you
should use RAM semaphores for serialization of or synchronization
between threads within one process, FSRam semaphores or exclusive system
semaphores for serialization of threads in different processes, and
nonexclusive system or RAM semaphores for signaling between threads in
different processes.

All OS/2 semaphore operations are performed on a semaphore handle,
which is a 32-bit quantity used to access the semaphore. In the case of
a RAM or FSRam semaphore, the handle is the semaphore data structure's
address; in the case of a system semaphore, it is the value that is
returned by the DosCreateSem or by the DosOpenSem call.

RAM Semaphores

RAM semaphores are mainly useful for coordinating the execution of
threads within a process. They are fast, since they use a simple data
structure, a double word of data storage, and receive relatively little
management or protection from OS/2.

RAM semaphores can be used between processes that share memory, which
could be created by a DosAllocShrSeg call. However, RAM semaphores should
not be used for serialization between processes, since OS/2 will not free
a RAM semaphore if the owner terminates while owning it. They are fine,
though, for providing serialization between threads within one process
and can be employed for signaling within or between processes.

The RAM semaphore data structure is a double word of storage that must be
initialized to 0 (unowned/clear) before it is used as a semaphore. It
should then be accessed using any of the following: DosSemClear,
DosSemRequest, DosSemSet, DosSemSetWait, DosSemWait, or DosMuxSemWait.

System Semaphores

System semaphores are the most flexible, safe, and easy-to-use type of
semaphore, but the overhead needed to maintain them exacts a performance
penalty. They do not require shared memory, and their ownership is
relinquished when their owner terminates, so they are suitable for
serialization between processes.

Each process that wishes to use a system semaphore must get a handle from
OS/2 by making a DosCreateSem or DosOpenSem call. The process supplies a
null-terminated semaphore name with the same format as the name of a file
in the subdirectory \SEM\, for example, \SEM\RESOURCE.LCK. OS/2 then
returns a handle for future access. The process also specifies whether
nonowning processes can alter the state of the semaphore while it is
owned. System semaphores are usually created nonexclusive for signaling
applications and exclusive for serialization applications.

DosCreateSem is used if the semaphore does not already exist (an error
code is returned if it exists) and initializes the new semaphore to
unowned. DosOpenSem is used if another process has created the system
semaphore, and an error code is returned if it does not exist. If, in your
application, one process will definitely run first, it can issue
DosCreateSem; later processes can then issue DosOpenSem. If the order of
execution of processes is not known, each one can call DosCreateSem and,
if this fails because the semaphore already exists, then call DosOpenSem.

When a process no longer needs to reference a system semaphore, it should
issue a DosCloseSem call. The system semaphore itself will be deleted when
all of the processes using the semaphore have called DosCloseSem. If a
process terminates with open system semaphores, then the semaphores are
closed by the system (see Figure 1).

Normally, a thread should not request a semaphore while it already owns
it. This would cause it to block waiting for itself to free the semaphore,
resulting in deadlock.

A system semaphore created with the exclusive option does not cause a
thread to block if the thread makes a DosSemRequest while owning the
semaphore. Instead, the semaphore's use count is incremented. Each time
the owning thread issues a DosSemClear, the semaphore's use count is
decremented, and when the use count becomes 0, the semaphore is released.
Note that such a semaphore is not the same as a classical counting
semaphore, which will be simulated later in this article. If another
thread attempts to do a DosSemRequest on an exclusive semaphore that is
already owned, it will block and wait for the use count to go to 0.

Nonexclusive system semaphores behave just as RAM semaphores do when they
are used with DosSemClear, DosSemRequest, DosSemSet, DosSemSetWait,
DosSemWait, and DosMuxSemWait.

FSRam Semaphores

FSRam semaphores provide serialization between processes, combining most
of the speed of RAM semaphores with much of the safety of system
semaphores. They use a simple data structure and have a small system
overhead, so their performance is very good. Since they store the data to
track semaphore ownership properly, FSRam semaphores permit DosExitList
processing to free an owned resource when the owning process terminates.

Just as with exclusive system semaphores, FSRam semaphores support
recursive DosFSRamSemRequest's by incrementing a use count, and support
DosFSRamSemClear's by decrementing a use count. When the use count goes
to 0, the semaphore becomes unowned. (Again, note that such a semaphore is
different from a classical counting semaphore.) DosFSRamSemRequest and
DosFSRamSemClear are the only OS/2 calls available for FSRam semaphores.

The FSRam semaphore data structure exists in memory shared by the
cooperating processes and must have its length field set to 12 and all of
its other fields initialized to 0 before its first use.

Comparison of Semaphore Types

  │Semaphore │                               │            │            │█
  │Types     │            Use                │Protection  │Performance │█
  │          │             │Between Threads  │            │            │█
  │Exclusive │Serialization│in Different     │Some Safety │            │█
  │System    │Only         │Processes        │Provided    │            │█
  ├──────────┼─────────────┼─────────────────┤Managed     │Moderate    │█
  │Non-      │Serialization│Between Threads  │by OS/2     │            │█
  │exclusive ├─────────────┤in Different     │            │            │█
  │System    │Signaling    │Processes        │            │            │█
  │          │             │Between Threads  │            │            │█
  │          │Serialization│Within One       │Minimal     │            │█
  │          │             │Process          │Protection──│            │█
  │RAM       ├─────────────┼─────────────────┤Not         │High        │█
  │          │             │Between Threads  │Managed     │            │█
  │          │Signaling    │in the Same or   │by OS/2     │            │█
  │          │             │Different Process│            │            │█
  │          │             │                 │Some        │            │█
  │FSRam     │Serialization│Between Threads  │Protection──│            │█
  │(Fast-Safe│Only         │in Different     │Partially   │High        │█
  │Ram)      │             │Processes        │Managed     │            │█
  │          │             │                 │by OS/2     │            │█


In OS/2, semaphores can ensure that no more than one thread at a time has
access to a protected resource. This is done by assigning a semaphore to
the protected resource and adopting the programming convention that each
piece of code that accesses the protected resource must be preceded by a
call to DosSemRequest (or one to DosFSRamSemRequest) and must be followed
by a call to DosSemClear (or one to DosFSRamSemClear). It is essential
that every thread accessing the protected resource follow this procedure,
or time-dependent errors may occur.

The thread whose semaphore request returns without error or time-out
"owns" the semaphore, and all other threads that issue a similar request
will block. This permits the owning thread to have exclusive access to the
code that affects the protected resource. When the owning thread no longer
needs to access the protected resource, it issues a semaphore clear and
thereby gives up its ownership. If another thread is blocked because of a
request on the newly released semaphore, it is permitted to proceed and
access the protected resource.

For example, suppose two threads use a semaphore to serialize access to a
resource. You might end up with code such as that shown in Figure 2, where
the ──1 indicates that the calling thread will wait indefinitely until the
requested semaphore becomes available.

The programming convention described previously should always be strictly
followed unless it is unnecessary for a particular application. Even a
slight deviation can lead to a failure of serialization, deadlock, or

An FSRam semaphore can be safely used for serialization between threads of
different processes if each process provides for recovery in case it
terminates while owning the semaphore. Before terminating, each process
should set up an ExitList routine to ensure the integrity of the resource
protected by the semaphore by making a call to DosExitList. When the
process terminates, the ExitList routine will execute and should first
issue DosFSRamSemRequest to acquire the semaphore, then clean up the
resource, and finally issue DosFSRamSemClear in order to free the
resource for use by other processes.

If the FSRam semaphore is owned by a thread of the terminating process,
at the point when DosFSRamSemRequest is issued during the execution of an
ExitList routine, the owning thread ID is forced to become the current
thread ID and the use count for the semaphore is set to 1. This allows the
ExitList routine to put the resource into a consistent state and then
free it with DosFSRamSemClear (see Figure 3).

RAM semaphores should only be used to serialize threads within a single
process. In this case, they are efficient (since there is little system
overhead), convenient (since the threads inherently share memory), and
safe (since the process only ends when all of its threads end).

It is possible to use a nonexclusive system semaphore for serialization,
but all threads that access the semaphore must follow the programming
conventions for serialization. Nonexclusive system semaphores
generally behave in the same way RAM semaphores do, except that if the
owner dies while owning one, the next thread to return from a
DosSemRequest on it will own it and get an error code. The new owner, an
ordinary thread or an ExitList routine, can clean up the resource and
issue DosSemClear.

Exclusive system semaphores can be safely used to provide serialization
between different processes, since the system semaphore data structure
contains the ID of the owning thread, and any attempt by one thread to
change the state of an exclusive system semaphore owned by another thread
will cause the thread to block or report an error.

When a thread ends, either individually or because its process
terminates, while owning a system semaphore, a flag is set in the
semaphore data structure. The next thread that gets the semaphore by means
of a call to DosSemRequest gets the error code "owner ended owning
semaphore" and can take suitable recovery measures. A subsequent call to
DosSemClear will free the semaphore and clear the error flag.

ExitList processing for system semaphores used for serialization is very
similar to the ExitList processing for FSRam semaphores.


RAM or nonexclusive system semaphores allow one thread to detect an event
in another . For example, suppose code fragment F1 in thread1 must
execute before F2 in thread2, and the system semaphore with handle sigSem
has been created with the NoExclusive option and has been set with
DosSemSet. The two threads could appear as shown in Figure 4.

In the worst case, thread2 will execute the call to DosSemWait before
thread1 finishes executing F1, at which time it blocks. When thread1
finishes executing F1, it clears sigSem, and thread2 proceeds to execute

The semaphore functions DosSemSet, DosSemClear, DosSemWait,
DosSemSetWait, and DosMuxSemWait are used for signaling. DosSemSet sets a
semaphore, and DosSemClear clears it. DosSemWait blocks the current thread
until the semaphore is cleared or until an optional time-out interval has
elapsed. DosSemSetWait is an indivisible concatenation of DosSemSet
followed by DosSemWait. DosMuxSemWait blocks the current thread until one
of a list of semaphores is cleared or until an optional time-out interval
is over.

You should not use the signaling calls with exclusive system semaphores.
As an example, consider a thread that issues DosSemSetWait. If the
exclusive system semaphore is already owned, an error is returned.
Otherwise, the thread sets the semaphore and waits for it to clear.
However, this will never happen since no other thread can clear it.

Figure 5 is an example of the use of DosSemSetWait, similar to the one
mentioned above with DosSemWait but this time in an infinite loop. The
code fragment F2 can execute only once for each execution of F1.

Figure 6 demonstrates the use of DosMuxSemWait. In this case, thread4
blocks until thread1, thread2, or thread3 clears its semaphore. Thread4
proceeds and takes whatever action is appropriate in response to the
signaling thread.

As with serialization, a thorough analysis is necessary when using
semaphores to implement signaling in order to avoid the hazards of deadlock
and synchronization failure.

Circular Buffers

Suppose two threads of one process are communicating via a circular
buffer, that is, thread1 writes to Buffer and thread2 reads from Buffer.
Buffer has a fixed capacity, bufSize, and pointers to the locations of the
newest (head) and oldest (tail) items. The buffer is circular in the sense
that each thread accesses location 0 after it accesses location bufSize──1.
Buffer is empty when head=tail and is full when tail=head+1 (mod bufSize).

Suppose that mutexSem, emptySem, and fullSem are RAM semaphores and that


is executed before the threads are spun off. Then the two threads would
appear as shown in Figure 7.

The protected resource in this case is Buffer, together with its
associated variables (head, tail, emptySem, and fullSem). Each thread
accesses the protected resource between calls to DosSemRequest and to
DosSemClear on the mutual exclusion semaphore, known as mutexSem, so the
accesses are made serially, and there are no race conditions.

Before the threads are spun off, the protected resource is in a consistent
state: head==tail, emptySem is set, and fullSem is clear. Each thread
changes the state of the protected resource so that if it was consistent
before entering the critical section, it is consistent on leaving the
critical section. For example, if thread1 adds an item that occupies the
last available free space in Buffer, it then sets the fullSem semaphore.
Thread1 always clears the emptySem semaphore since it has just added an
item to Buffer. Similarly, thread2 sets the emptySem semaphore when it
removes the last item from the buffer and always clears the fullSem

If Buffer is full, thread1 sets fullSem and then blocks on fullSem. In
this case, emptySem is not set and, since thread1 is blocked outside its
critical region, thread2 can enter its critical region, remove an item
from the buffer, and clear fullSem, allowing thread1 to proceed.

If Buffer is empty, thread2 sets emptySem and then blocks on emptySem.
Since fullSem is not set and thread2 is blocked outside its critical
section, thread1 can enter its critical section, add an item to Buffer,
and clear emptySem, allowing thread2 to proceed.

Simulating a Classical Counting Semaphore

A classical counting semaphore can be easily simulated with the
semaphore facilities that are offered by OS/2. ClassicCountSem is an
integer variable that can only be accessed, apart from initialization,
via the two atomic operations P and V (see Figure 8).

To simulate classicCountSem and its associated operations P and V, use the
OS/2 semaphores countSem for signaling and mutexSem for serialization,
both initially clear, and the integer variable count, initially
nonnegative (see Figure 9).

All of the code in the P and V routines that changes count and countSem
lies in mutual exclusion regions bracketed by calls on mutexSem,
simplifying the analysis of these routines.

These routines would work correctly with all references to countSem
removed, but P would burn up a lot of cycles testing conditions that are
not satisfied. The wait on countSem prevents useless activity, but it also
allows P to proceed if there is a chance that it will run to completion. P
will block on countSem only if count is 0. When V executes, count is
incremented and countSem is cleared, permitting P to run until count is
found to be 0 again. Therefore, whenever V "produces" a unit, any waiting
P's have an opportunity to "consume" it.


The precise timing and behavior of threads synchronized by means of
semaphores is affected by the activity of other threads in the system,
such as priority and position in the list of blocked threads. If several
threads are waiting on a semaphore, your application should not count on
one in particular to be scheduled to run, as factors outside your control
may affect the decision of the scheduler.

DosSemWait, DosSemSetWait, and DosSemRequest are level-triggered. This
means, for example, that a thread blocked by one of these calls will only
return if the blocking semaphore remains clear until the blocked thread
can actually run. If some other thread, in the meantime, sets the
semaphore again, then the blocked thread will remain blocked. If the
semaphore remains clear until the blocked thread can be scheduled and
actually run, then the call returns and the once-blocked thread continues
with its execution.

DosMuxSemWait, on the other hand, is edge-triggered. That is, a thread
that is blocked by a call to DosMuxSemWait will return if one of the
semaphores in its list is cleared, even if the semaphore is set by another
thread before the blocked thread can run again.


Semaphores in OS/2 can be used to provide serialized access to protected
resources by associating a semaphore with the resource and combining OS/2
function calls with programming conventions to ensure that the semaphore
has only one owner at a time. Semaphores can also allow one thread to
signal another that an event has occurred. By analyzing the particular
application, you can pick the appropriate semaphore type and OS/2
function calls.

Figure 1:  Using System Semaphores

if( rc=DosCreateSem( NoExclusive, &sysSem, &semName))
       DosOpenSem( &sysSem, &semName) ;

<...use the semaphore...>

DosCloseSem( sysSem) ;

Figure 2:  Two Threads Serializing Access to a Resource

long resourceSem = 0;

     DosSemRequest( &resourceSem, -1L);

     <... read/modify resource ...>

     DosSemClear( &resourceSem);

     DosSemRequest( &resourceSem, -1L);

     <... read/modify resource...>

     DosSemClear( &resourceSem);

Figure 3:  ExitList Processing for a Fast-Safe Ram Semaphore

     DosExitList( 1, &Cleanup);    /* add to exit list */

   if( DosFSRamSemRequest( &sem, 0L) != ERR_TIMEOUT)
          <... put in a consistent state ...>
          DosFSRamSemClear( &sem);
   DosExitList( 3, 0);  /* goto next ExitList routine */

Figure 4:  Signaling Between Threads

DosSemSet( sigSem);

     DosSemClear( sigSem);

     DosSemWait( sigSem, -1L);

Figure 5:  Using DosSemSetWait

          DosSemClear( sigSem) ;

          DosSemSetWait( segSem, -1L) ;

Figure 6:  Using DosMuxSemWait

struct {
   int numSem;
   int res1;
   unsigned long semHandle1;
   int res2;
   unsigned long semHandle2;
   int res3;
   unsigned long semHandle3;
} muxSemList ;

int muxIndex;

     DosSemClear( sigSem1) ;


     DosSemClear( sigSem2) ;

     DosSemClear( sigSem3) ;


     muxSemList.numSem = 3 ;
     muxSemList.res1 = 0;
     muxSemList.semHandle1 = sigSem1 ;
     muxSemList.res2 = 0 ;
     muxSemList.semHandle2 = sigSem2 ;
     muxSemList.res3 = 0 ;
     muxSemList.semHandle3 = sigSem3 ;
     DosMuxSemWait( &muxIndex, &muxSemList, -1L) ;
     switch( muxIndex) {
     case 1:
                                    ∙    /* respond to F1 */
     case 2:
                                    ∙    /* respond to F2 */
     case 3:
                                    ∙    /* respond to F3 */

Figure 7:  Example of a Circular Buffer

     <... get item c ...>
     DosSemWait( &fullSem, -1L) ;

     DosSemRequest( &mutexSem, -1L);
     Buffer.head = c;              /* store c in Buffer */
     head++;                       /* advance head of Buffer */
     head %= bufSize;              /* wrap around to beginning */
          DosSemSet( &fullSem);    /* set if full */
     DosSemClear( &emptySem);      /* not empty */
     DosSemClear( &mutexSem);

     DosSemWait( &emptySem, -1L);

     DosSemRequest( &mutexSem, -1L);
     c = Buffer.tail;              /* get c from Buffer */
     tail++;                       /* advance tail of Buffer */
     tail %= bufSize;              /* wrap around to beginning */
     if( head==tail)
          DosSemSet( &emptySem);   /* set if empty */
     DosSemClear( &fullSem);       /* not full */
     DosSemClear( &mutexSem);

     <... use item c ...>

Figure 8:  Classical Counting Semaphore

     <... wait until classicCountSem > 0 ...>


Figure 9:  Simulating a Counting Semaphore under OS/2

     int blocked=1;

     while( blocked == 1)
          DosSemWait( &countSem);         /* wait til maybe ok */

          DosSemRequest( &mutexSem, -1L); /* mutual excl */
          if( count == 0)                 /* not ready yet */
            DosSemSet( &countSem);        /* set up block */
          else {
            count-;                       /* decrement count */
            blocked-;                     /* set up loop exit */
          DosSemClear( &mutexSem);        /* mutual excl */

     DosSemRequest( &mutexSem, -1L);       /* mutual excl */
     count++;                              /* increment count */
     DosSemClear( &countSem);              /* free waiters */
     DosSemClear( &mutexSem);              /* mutual excl */


Design Concepts and Considerations in Building an OS/2 Dynamic-Link Library

Ross M. Greenburg☼

You are in a maze of twisty little passages, all alike.


I see no DLL here.


I don't know how to make a DLL.


You have:

  ■  an 80286 machine
  ■  sufficient memory
  ■  OS/2
  ■  An OS/2 toolkit. In the toolkit is:
     ■  a text editor
     ■  a C compiler
     ■  an assembler
     ■  a linker



Just as in the game ADVENTURE, not knowing the keywords when you're
trying to create a dynamic-link library can be very frustrating. Once you
know the keywords, though, you can explore new areas of the game and bring
home prizes and treasures. The objective of this article is to help teach
you some of the new keywords and techniques you need to build a dynamic-
link library of your own.

What Is a DLL?

The idea of dynamic-link libraries is one of the most important
concepts that the OS/2 systems introduce. Although the principle of DLLs
has been around for some time-they are usually called shareable libraries
in other operating systems-OS/2 makes such a shareable library an
intrinsic part of the operating system. In fact, the system library
functions themselves are DLLs in OS/2, and are clearly separated by
device type, making upgrading easy.

Under MS-DOS(R), after you compile a program, you then link it with other
portions of the program and with portions from a library of commonly used
routines. The end result is a standalone piece of code that is loaded
into memory, has its outstanding address references resolved, and is then
executed. The physical file resulting from the link contains portions of
the library it used; two programs that use printf will each contain a copy
of the library functions that comprise that ubiquitous function.

In a single-tasking operating system, with a sufficiently large hard disk,
this really isn't a problem. In a multitasking operating system that
allows for shared memory use, loading multiple copies of the same code
seems wasteful. OS/2 obviates this by permitting only one copy of a given
function to be loaded in memory and to have this copy shared by any task
that wants to use it. Since the function itself is not physically part of
the program file, it is possible for the executable to be rather small
and to update the library only as required. The concept of separate
overlay files, and complicated linkers, is no longer needed; just include
the specific DLLs needed and let the operating system do the rest.

However, the advantages of using DLLs go far beyond the convenience; there
is a functionality to DLLs that you can exploit in many different ways.
For example, there is the ability for two or more completely separate
and distinct programs to share memory by simply accessing the same run-
time routine. This communication, already an intrinsic part of OS/2, can
be fine-tuned with DLLs to fit your exact needs in a complicated

This is not without a price, though; there are some tricks, cautions, and
caveats to writing an operable DLL. I discovered some of them the hard way
while preparing a simple example of DLL use. Of course, if you want to
avoid the supposed complexity of these useful techniques, there is
nothing in OS/2 to prevent you from using the standard, and more
familiar, linking techniques of the past, except, perhaps, the knowledge
that there is a better way.

A dynamic-link library is not simply a new method of linking an old
library. There are some intrinsic differences between the two techniques.
A look at how statically linked libraries are linked into your code will
help you to understand how the new Dynalink approach differs and why it is
an improvement.

Static Linking

When you compile a standard C program, the resulting output of the
compiler is an object file, which contains a number of different types of
records for procedures and routines, externally accessible variables,
local stack variables, and so on. Each item has a unique record type
associated with it.

There is also a unique record type that indicates where the code for a
routine starts. Another record type has the name of the routine and a
pointer to that routine's code. Some record types indicate that the
routine requested is not found and hence must be external to the object
module; other record types indicate that the object is an externally
located data item.

Each call to a routine that cannot be resolved within the particular
source module is changed into a parameter that simply involves an
"external item" record.You can help the compiler by specifying the call
type as being a near or far routine, or a near or far external item of
some other type.

After you've finished compiling all your various source modules into
their object modules, you link them together, along with some
appropriate libraries, and end up with an executable piece of code. The
linker examines each object module it sees, usually in the order in which
they're presented, and keeps a list of all record types that either
indicate a request for an external item or define a global item. Then, as
it sees record types indicating actual code routines, it determines that
routine's placement and resolves all calls for it into an actual address
within the eventual output file. Far calls, of course, indicate not
just an offset within a 64Kb segment, but allow additional segments to be
addressed, which in turn permit the creation of much larger programs.

There is really nothing intrinsically foreign to the compiler in the
concept of mixed memory-model code as long as it knows how a routine will
be called. The compiler generates a far return for routines defined as far
calls and near returns for near calls. Addressing far data items is
resolved in a similar fashion: the compiler puts out a record type that
the linker can understand and resolve into an actual segment and offset.
There is an extra step for the actual loading and executing of the code,
discussed later in this article.

Whatever items are not resolved within the linking of the various object
modules are next searched for in the libraries. These libraries are
basically object modules with nothing but local references resolved. A
module in the library usually starts off as a simple object file and is
then stored and indexed into a library as an entire unit. It is not stored
on a routine-by-routine basis, but on an object-file-by-object-file basis

The appropriate routine or external item is found in the library, and the
module is then pulled from the library and inserted into the executable
form. All references to it are resolved, and the process continues. An
important consideration is that the object module originally loaded
into the library as one unit is pulled from it as one unit as well, even
if only one of the functions specified in the routine is referenced.

The end result is a totally self-contained image, which, when executed, is
loaded at some address, called the base address. The base segment address
is added to all of the other segment addresses throughout the code in the
mysterious load routines, and finally, with one simple call or jump, your
program is executed. That's a basic, not-too-technical description of how
static linking works.

The differences between dynamic linking and the idea of statically linking
an already existing library are not all that substantial, but the end
product is. Consequently, the conceptual design of the dynamic-link library
is different.

Dynamic Linking

With a normal library, you compile all of the object modules you need and
then use a librarian program to create a library. The library itself is in a
strange format, suitable only for linkers and librarian programs.

Things are a little different with DLLs, though. First, there are two
separate link steps. You must link the constituent object file members
that form the DLL together and then link your own code with the resulting
DLL. Creating the DLL itself, however, requires some work.

After you've created the object files that will make up DLL, you link them
and a special module definition file with the normal linker to create
DLL. Its format is really no different from a normal EXE file-it even
has the "MARKZIBO" as its first two bytes. It is therefore well suited for
the standard system loader to load as if it were actually a program.
Later on, the system does just that for the initialization routine. The
new library will have an extension of DLL, as this name is hardwired into
any affiliated EXE files.

The special module definition file, or DEF, describes the external
interface for each of the accessible routines: their public names and
their attributes. Anything not specifically mentioned in the DEF file
cannot be routinely accessed by an outside program. This definition file
is called the export module definition file.

By running the export DEF file through a program called Import Librarian
(IMPLIB), you can create a special library file. This library file is
conceptually similar to the standard notion of a library and hence has
the LIB extension (see Figure 1).

Instead of providing a LIB file, another option is to create what is in
essence the inverse of the export DEF file. Such a file is called an import
module definition file, which also has the extension DEF (see Figure 2).

When you link the DLL with your own code, the linker sees the special
record format of the import library (the LIB file created by IMPLIB), or
reads the import DEF file and creates special records that OS/2's program
load facilities understand. The end result of a link using DLLs is a
hybrid file that can be considered a partial EXE and a partial OBJ at the
same time. A compiled object module resolves local variables and routines
into a segment and an offset, leaving external references virtually
undefined. The Dynalink program results in an EXE coming from the linker
with its external references to DLL routines effectively unresolved.

At this point, I'm going to refer to segments as selectors, since I am
referring to DLLs implemented under OS/2 running in protected mode.

Part of OS/2's program loader recognizes that the EXE it's about to load
contains DLL calls. Finding these records causes a lookup on an internal
table to determine if the DLL has already been loaded. Only one copy of
any code within the DLL is loaded-all sessions and processes share this

Each module in the DLL is defined as a "load-at-run-time" or a "load-on-
demand" module. Regardless of which definition is chosen, a selector is
allocated for each module, and all references to those modules are now
resolved into a selector and offset pair. If the module has been defined
as a load-at-run-time module, then the actual code for the module is read
from the file and loaded into memory, and any outstanding linkages are
resolved. When trying to locate a DLL, by the way, the operating system
looks in the directory paths specified in your environment's LIBPATH

Value of Protected Mode

Consider what happens when a load-on-demand function is called before that
selector points to valid code: a page fault occurs, and the memory
management module can easily resolve what the problem is, load the
appropriate code, and let the program continue operating as if nothing
had happened. Subsequent calls to routines within the same selector would
operate without a page fault. Once the page fault mechanism, an intrinsic
part of OS/2 and protected mode applications, has been enabled, it is
transparent whether or not a requested page exists in real memory or in
virtual memory.

The 80286 and 80386 chips have a table within them called the Local
Descriptor Table, or LDT, which holds selectors and the characteristics of
these selectors. There is an LDT for each of the processes currently
running. If a process attempts to access memory by using a selector not
within its LDT, the hardware will cause a fault to occur──effective
hardware protection of memory space.

The Global Descriptor Table, or GDT, is similar to the LDT (see Figure 3),
except that all tasks can access the selectors, and their associated
memory, contained therein. Although this seems like a simple way to make a
selector and its data space accessible to multiple processes, OS/2 does
not use the GDT for shared memory access; instead it makes an entry into
the LDT of each process. This is because memory space used by a DLL should
only be accessible to the processes actually using the DLL. Using the GDT
would make the DLL selectors available to every session and process on
the system.

When a request for memory allocation is made to OS/2, the type of memory
(shared or nonshared) is included in the request, and an entry is made in
the LDT for all processes permitted to share this memory. Actually,
whenever shareable memory is allocated, an entry is made in every
process's LDT.

Processes requiring access to the memory have proper entries in the LDT;
other tasks have illegal contents that will cause a memory fault to be
generated if they are used to access DLL-allocated memory. Only the
kernel (Ring 0) code can write to these tables, however. (Device drivers
also run at Ring 0, so they'd have write access to the descriptor tables
as well.)

What's the Big Deal?

So far, a relatively efficient mechanism exists for linking in routines as
required at run time instead of just once at link, all automatically and
transparently. However, just what are the advantages of such an
ability? First, swapping the DLL routines in and out of memory becomes
pretty easy; the LDT has a "present" bit indicating whether the
requested segment is in memory or not. If not in memory, a page fault
occurs as described above, and the swapped-out DLL routine can be brought
into real memory. Since the selector itself is just an index into a table
that contains real address information, the individual DLL modules can
end up anywhere in memory, transparent to your own code.

Program code without some data space associated with it is a rarity; pure
code can't manipulate items, although it is frequently useful in
mathematical routines. The 8086/8088 family of processors used the data
segment register to address its data space. The 80286/80386 family of
chips requires data to be addressed through a selector as well. The
information for the data selector is also stored in the LDT. By setting
the appropriate bits in the LDT entry for a given selector, its associated
memory can be made private or publicly accessible, or it can be set to be
written to as read-only. Data selectors can even require a certain level
of privilege in the code attempting to access it. Any illegal operation
causes a fault to occur, and OS/2 can deal with the faulting process as

This means that, with the LDT set properly, memory can be shareable
between tasks and protected from illegal or erroneous access, and other
interesting memory uses and control techniques are made possible.

As such, the DLL can be controlled and fine-tuned in a variety of
different ways, through the DEF files.

Defining the DEF File

There are two different types of DEF files. The EXPORT definition file
lets the world know what the various entry points and their characteristics
are. The IMPORT definition file indicates which functions of many potential
DLLs will be used and should therefore be linked at run time. A DLL can have
both IMPORTS and EXPORTS defined in the same DEF file.

The IMPORT library is created by processing the EXPORT definition file
through IMPLIB. Let's look at each piece separately, along with its
available features and options. These options must all be entered in the
appropriate DEF file in uppercase.

LIBRARY Statement

The EXPORT DEF file requires several fields (see Figure 4). The most
important one that is required is the LIBRARY field, which defines the
file as a DLL Export definition file, instead of a normal application DEF

The LIBRARY statement must be the first one in the DEF file, allowing the
linker (and IMPLIB) to have a head start on what is to come. The first
argument, [name], to the LIBRARY statement is the eventual output name
for the created DLL. The filetype, or extension, will automatically be

When the DLL is first loaded, you may want to initialize some things, for
instance, setting specific data items, and assuring that certain system
resources are available. Each DLL can have an initialization routine that
will be called when the DLL is first loaded and there are no other
references to it, or upon each invocation of the DLL. The second argument,
[init_type], lets you specify whether you want the initialization routine
called every time the DLL is invoked (INITINSTANCE), or once, when the DLL
is first loaded (INITGLOBAL, the default).

NAME Statement

Similarly, if the linker sees the NAME statement, it understands that you
are creating an application and not a DLL. The NAME statement allows you
to specify whether the application is Windows- or Presentation Manager-
compatible and, if so, whether it is capable of running in real mode or
protected mode. If you specify WINDOWAPI as the second argument, then the
application requires Windows in order to execute. Specifying WINDOWCOMPAT
means that it is Windows-compatible and can run in a window group under
OS/2. Finally, specifying NOTWINDOWCOMPAT indicates that the application
requires its own screen group when running.

The name command allows you to specify with [appname] the name the
application will have after linking. Naturally, the default extension is
EXE. You'll get a warning message from the linker if the name you choose
for the executable name and the name you choose in the NAME line don't

CODE Statement

All code segments within the DLL share a similar set of attributes unless
otherwise specified. The default attributes are set with the CODE statement.

There are a few other optional parameters that are allowed (but ignored)
in the CODE statement for compatibility with Windows and the Presentation

The [load] parameter indicates whether you want the segment
automatically loaded upon DLL invocation (PRELOAD) or to wait until the
segment is actually accessed with a call (LOADONCALL, the default). In an
application with large areas of the code that might never be called, you
no longer need to load those library calls into memory all at once. For
example, if you had a very large error recovery routine that would only
get called once in a great while, you wouldn't need to give up the memory
space it required until the error occurred. Another example would be having
many levels of help menus that are rarely used──a perfect fit for DLLs.

When a LOADONCALL routine is called for the first time, it is loaded
automatically and will stay loaded in memory unless it is marked as
discardable. That means that its associated memory can be freed as required
with fresh copies loaded from disk.

If you use the [executeonly] option to specify that other processes
cannot read this segment, then, even though the LDT marks the selector as
globally accessible, it cannot be read or treated like a data segment
selector by any process without the appropriate privilege level. The
default, EXECUTEREAD, allows the memory allocated to this selector to be
read for purposes other than execution.

It is important to note that routines containing such items as a
switch/case statement cannot be made EXECUTEONLY since a portion of the
code space is used as data space for storage of the switch label table.

Only code segments with a high enough privilege level can access the
hardware directly. You can use the [iopl] parameter to specify that a
segment has this ability. The default (NOIOPL) makes sense; unless
otherwise specified, an attempt to access the hardware directly, for
example the com port, will cause an immediate fault. When you allow a
segment to access hardware directly, include the IOPL parameter in the
CODE line. It is probably best to specify IOPL only when required. OS/2
still requires that you make a system call to request the privilege of
hardware access.

The way privilege level functions in OS/2 is an important factor when
trying to understand the implications of the IOPL parameter. The Intel
programmer's reference manuals for the 80286 and 80386 provide a detailed
description of the 80286/80386 IOPL levels.

The 80286/80386 chip prohibits direct transitions between code segments
with different levels of privilege. The default privilege level for
application code in OS/2 is Ring 3. Ring 2 code segments can access
hardware directly. The only way to transfer from one privilege level to
another is through what is known as a call gate, which has a specific
selector type in the LDT and an actual "destination selector," the
selector belonging to the actual code segment of the privileged call.
Also, the gateway has its own attendant privilege level and can only be
called by code segments of the same privilege level.

Conceptually, when a call is made to a privileged routine, it passes
through the call gate before passing to the privileged routine. Since the
only way through the call gate would be with either a CALL instruction
(going into the routine) or a RET (coming back from the routine), the call
gateway provides extra code security, but at the cost of some additional
hardware overhead. Each transition via a gateway causes parameters on
the stack to be copied to a new stack──another interesting security
feature of the 80286, since a program with a lower privilege level could
manipulate the return address on the stack otherwise.

A routine utilizing the IOPL parameter uses up an additional slot in the
LDT table. Although the LDT table has 8Kb of possible entries in it-each
LDT entry takes up 8 bytes, so an entire segment has been allocated to
the LDT in OS/2-5Kb of those are reserved for all shared segments
throughout the entire system, including the selector space used for DLL
selectors. This leaves you with about 3Kb of LDT entries for private use,
which is probably enough for the foreseeable future. This has been
modified to a 3-to-1 shared-to-private ratio in the next release of OS/2.

The final parameter in the CODE statement allows you to specify whether
the segment is a NONCONFORMING or a CONFORMING segment. This also deals
with the IOPL privilege level and can be pretty confusing at first.
Consider it to be the inverse of the gateway approach. Normally, a segment
will execute with the privilege level of the calling segment. However,
there are times when this might not be appropriate; consider a Ring 3
communications protocol checking routine called from a Ring 0 device
driver. In this situation, you might not want to allow the protocol
checker to operate with the higher privilege of its calling segment. The
default case, the NONCONFORMING parameter, would cause the Ring 3 routine
to execute at Ring 3. Set to CONFORMING, it would execute at the privilege
level of the routine calling it, the device driver running at Ring 0.

Data Space Definitions

Just as code segments have a method of setting default parameters, the
data segments also allow certain parameters to be set with the DATA
statement, which shares some of its parameter list with the CODE
statement. This makes a great deal of sense because these parameters
describe how to make the default settings for each data selector in the
LDT. The format of the DATA statement is therefore very similar to the
CODE statement.

As before, [load] indicates whether the data segment should be loaded upon
first invocation or when the first access to the selector's address is
made. The default condition is LOADONCALL; however, you can specify
invocations of load with PRELOAD.

The [readonly] parameter helps you to determine whether the data segment
can be written into with the default parameter of READWRITE or should be
protected against write access (READONLY). Attempts to write to a READONLY
segment cause a hardware fault.

You use [instance] to specify whether or not the data segment, the DGROUP
data segment in most cases, should be automatically allocated upon
invocation, and if so, whether there should be one copy allocated for
the entire DLL (SINGLE, the default setting for DLLs) or each instance of
DLL use should have its own automatic data segment allocated (MULTIPLE,
the default setting for applications). If no automatic allocation is
required, then the parameter should be set to NONE.

Each data segment can have its own IOPL level, which allows you to set the
minimum privilege level required to access this data segment. Setting the
[iopl] parameter to IOPL means that only Ring 2 and higher privileged
levels are granted access to the data segment. The default, NOIOPL,
enables Ring 3 code segment routines to have access to the data affiliated
with the data segment. This allows you to create an interesting interface
between IOPL and NOIOPL segments through common shared memory, like
passing a message through a keyhole.

Finally, [shared] allows you to determine whether or not a data segment
marked as a READWRITE segment can be shared among different tasks. If it
is marked as shareable, then only one segment is allocated at load time,
and any process with a privilege level sufficient to write to it can do
so. The default, NONSHARED, does not permit write access to a common data
segment and causes a separate copy to be loaded for each instance. If a
data segment is marked as READONLY, then by definition it is shareable.

Segment Parameters

Unless otherwise specified, code and data segments have the attributes
you set in the CODE and DATA statements, or their predefined default
values if you don't describe them. However, using the SEGMENTS statement,
you can specify the individual characteristics for a given named segment.

The [CLASS 'classname'] parameter is an option that lets you specify that
the <segmentname> parameter, which is required, be assigned to the class
specified. If you don't specify a classname, then the CODE classname will
be assigned to code segments and "unclassed" data segments will be
assigned to the DGROUP classname, or whatever was specified in the
appropriate DATA group definition for the object/segment.

EXPORTS Statement

The EXPORTS statement is the only method of informing the outside world
about the routines of the DLL and is applicable only to DLLs. Unless
specified by inclusion in the EXPORTS section of the DEF file, a DLL routine
is invisible to applications.

A name used internally within the DLL need not be the name the application
knows the routine by; you can easily make the outside name different from
the internal name. This gives you a class of functions serving a similar
purpose and then lets you categorize them with a meaningful prefix.

If you want, you can allow access to the function by its ordinal (or the
routines library "slot" number) instead of by its name, by specifying the
desired ordinal, obviously unique for the DLL, preceded by an at (@)
sign. If you do, lookups will be faster at load time, and less space will
be required for the in-memory search list.

There is a drawback to using ordinals, though; once you pick an ordinal
number, you're stuck with it. This makes debugging a bit harder. Of
course, the ordinal approach is good for DLLs whose internal structure
you'd rather keep secret.

If you do decide on the [@ordinal] option, then you may have to consider
using the [RESIDENTNAME] option as well; normally, if an ordinal is used,
OS/2 does not keep the specified external name available. If you don't use
the ordinal parameter, OS/2 keeps the name resident in its search tables.

If you include the use of any privileged functions in your routine, you
have to let the linker know how many words to reserve for parameter
copying by using the [pwords] variable. This information is later passed
to the loader for construction of call gates. Since a calling task will
have its own parameters copied as it passes through the gateway, you would
have to reserve that space beforehand.

IMPORTS Statement

The IMPORTS section enables you to specify which external DLL routines you
need in your application.

Again, like the EXPORTS lines, you can specify a name your routine uses
when it is trying to resolve external routines. You could, therefore,
create a debugging DLL and a normal DLL and be able to link between them
only by changing the <modulename> or the <entryname> associated with the
named routine. The name of the application or DLL containing the desired
<entryname> is the <modulename> specified in the EXPORTS statement for the
DLL. The <entryname> can also be an ordinal number.

If the optional [name=] parameter is not specified, then the default name
by which the routine will be known will be the same as <entryname>. You
must specify an internal name, however, if you've specified an ordinal
number instead of an <entryname>.

You can include other statements in the DEF file(s). They are summarized
in Figure 5.

Using the DEF Files

There are two specific ways to use the DEF files: first, just include
them on the command line to the linker, and second, pass them to the
Import Library Manager utility, IMPLIB.

IMPLIB is a standard part of the developer's toolkit in OS/2. If you're
creating a DLL and the application to use that DLL, you don't necessarily
need IMPLIB, since you can create the EXPORT and IMPORT library definition
files as you desire. However, if you're creating a DLL for other
applications to use, such as a commercial functions library or a
replacement for a product that already exists, then IMPLIB should be part
of your development cycle.

IMPLIB takes one or more definition files for input and then produces what
appears to be a simple LIB file for output. This allows you to include the
LIB file in the link step. Assuming that you had two DLLs, called
COM_INP.DLL and COM_OUT.DLL, each with their associated DEF files, you
could create an IMPLIB statement such as the one shown in Figure 6.

Then, simply distribute the COM_STUF.LIB and the two DLLs, keeping the
internal details of the DLLs to yourself. Special versions of these import
library files can allow for different public definitions of your DLL,
for instance, one for production, and one for debugging.

A DLL Example

In attempting to create a DLL, I ran into a number of difficulties,
some of which cannot, by the very nature of the DLL and multitasking
software, be resolved. Deadlocks can occur in DLLs just as they do in
other types of software.

Think of the requirements of a multisession process such as a "chat"
facility: multiple copies of the same process running, each of which
occasionally generates a message, which is added to some internal queue.
Each message generated must be collected by all other processes before it
can be erased from the queue of outstanding messages, and each such message
must eventually be displayed by each process. Finally, each process must be
able to login or logout from the chat session and must have a unique

I've designed such a facility, called DLL_CHAT (see Figure 7 for the code
listings), as a method of demonstrating some of the unique abilities and
problems that occur when using a DLL as the glue to hold a multiprocess
concept like this together. It may not be useful on a single-screen
machine, but if the output were going to a number of communications
ports, it would be.

You should note that all the problems inherent with this code design can
be easily solved by including the OS/2 system resource of queues. However,
that approach wasn't used, primarily because it wouldn't have required
DLLs and wouldn't have been as efficient, either.

Stepwise Design

One advantage of using DLLs in this application is the ability not only to
have private and shared memory, but for separately compiled and executed
tasks to utilize the same code simultaneously. There is nothing to prevent
users of DLL_CHAT from following my coding conventions and creating their
own user-friendly interfaces──the bane of spiffy-concept designers. In
fact, there is no reason why differently designed front ends couldn't be
used for each session. Starting with this concept, I designed this code
with most of the capabilities in the DLL.

One ability of my DLL, called CHATLIB (see Figure 8 for code listings), is
to provide for initialization code that is executed either on the
first invocation of the DLL only, or on each invocation. This
initialization routine is called before the process itself starts to
run. This DLL only calls its initialization routine the first time, so its
EXPORT file contains the INITGLOBAL parameter. Since this is the default
condition, you can exclude it if you wish. The routine I use in this DLL
is simple, setting certain default conditions and allocating some
required queue space.

First, the login procedure has to advise the library code that another
consumer and provider of messages has suddenly appeared. To make things
easier, the login procedure returns some user identifier to the process.
It is useful to include an ID when generating new messages, when
consuming old ones, and, of course, when logging out.

The login procedure could have been part of the initialization
routine, which would have been of type INITINSTANCE. However, since the
initialization routine is run before the application gets control, I
felt uneasy about logging in before the main routine had even been
reached. As you can see, the initialization routine is used for things
that I consider to have a "system" nature, while logging in seems to be
more of an "application" task. However, this is only a personal

When the DLL sees the login, it allocates and assigns whatever global and
local objects and structures are necessary for the new process. Where the
actual allocations of memory would be made had to be decided in the
design, since the memory could be allocated either in the DLL (becoming,
in essence, a hidden object from the client code) or in the per-process
code itself. There are advantages to having a DLL routine allocate memory
that is globally accessible to all processes but that only the DLL
routines know about.

Also, a login causes each message already in the queue to appear unread to
the newly logged-in task. Later, when requests are made for an
outstanding and unread message, these messages will be returned.

The general design of the DLL causes a sharing of the cleanup task on each
call to the get-a-message routine. When a message is passed to the DLL, it
is added to a queue, a structure that includes a flag word with one bit
for each session. A mask word with a set bit for each empty task slot is
used for the initial value of this flag word. An "or" operation is then
used to obtain the current task ID, allowing the sender of the message to
indicate that it has already received the message.

When a process fetches a new message, it sets the bit in the flag word to
show that it has done so. Then, when that flag indicates that all
processes have received a copy of the message, the message can be removed
from the global queue. Therefore, each process has to be able to
manipulate that queue directly or must call a routine that has that
ability. I've opted for a more modular design; using a routine to
specifically remove the message from the queue, or to add a message to
the queue, allows me to isolate the queue itself.

Although the queue resides in global memory at this point, perhaps in the
future it might reside on some node on a network or some memory device
that might require a higher privilege level. Therefore, isolating the
routine that physically modifies the queues is a good idea.

Since there isn't a human attached to each session, each session sends a
message only after a random amount of time has passed. And, just to keep
things interesting, there is a suitable sleep period while the imaginary
typist is entering his or her message, letting messages build up in the
queue. Whenever the sender is not typing or sending a message, it is
executing a loop that constantly seeks the outstanding message count.
Blocking on a null message count would prohibit the sender from sending a
message. Of course, OS/2 lets you have two different threads, one of which
could block on a null message count within the DLL, but that is outside
the scope of this article.

Displaying of received messages occurs on a per-process basis. This
could cause problems if the display takes place when the session is not
the current foreground session; messages could scroll off the virtual
screen while the process is in the background, an unacceptable condition
in a real chat system.

If a message is to be displayed but the session is not the foreground
session, it is stored in an internal display queue. Normally, this queue
would be a linked list of calloc'd memory. For DLL_CHAT, a short
character array was used, one which would fill up quickly. Eventually,
when it does fill up, it will stop fetching messages from the DLL queue.
Since the DLL queue is constantly fed by all of the sessions, it too will
fill up.

Another session will then block when attempting to add a message to the
queue. This condition can escalate until all sessions are blocked.
Therefore, before any session sends a message, it checks to determine if
room exists in the queue.

OS/2 is a multitasking operating system, so a routine must not be
interrupted between the time it determines there is room on the queue and
actually adding the message to the queue. However, you don't want to
starve other sessions, especially not those related in any way to

To prevent this, DLL_CHAT uses a globally accessible system semaphore and
assigns the semaphore immediately upon entry to any collision-sensitive
routine. Other processes trying to enter one of these routines would block
on this flag and wait for it to free up or for a certain amount of time to
pass. If the flag didn't change within the specified time-out period, then
an error condition would be returned to the calling task.

Finally, there is the logout routine. When the session gets a quit or exit
command from the keyboard, it simply exits. However, as an intrinsic part
of exiting, all DosExitList routines are called. The exit routine, in
turn, calls the DLL's logout routine, which then sets the semaphore and
proceeds to loop through the outstanding message list. For each
outstanding message, it sets the flag as if the process had already
received the message. After each flag word has been set, it is examined to
determine if it has been read by all processes; if so, it is removed from
the queue.

Each message on the queue is a member of a linked list, and its memory is
allocated from the global memory pool. When removing a message from the
queue, the pointers of the other messages it points to are modified to
point to each other, and then the memory is deallocated.

Caveats and Warnings

There are a few things you have to be aware of when you're designing your
DLLs. I mentioned the extraordinary lengths I went to in the original
design of DLL_CHAT to assure that certain areas of the code are
protected against two competing tasks attempting to access them at
once. This is a problem inherent in any multiprocessing system.
Typically, the problem is called "reentrancy," that is, a piece of code
being entered by a calling process before another process has finished its
call. The actual definition of reentrancy is much stricter than this, but
this is good enough for our purposes.

Utilizing semaphores is effective in most circumstances. However, the
method I chose would not work best without some of the special hooks OS/2
provides for the safe use of semaphores. Consider what happens if the
session currently executing a semaphored routine happens to be interrupted
and then gets killed by some high-priority event after it sets the
semaphore, but before the semaphore is released. Such an event could be
as normal as a keystroke being entered, or, if attached to a comm port,
the modem losing carrier, or something even more esoteric. There is no
guarantee that it will return to where it left off, but if it doesn't
return and finish the routine, the semaphore will forever be marked as in

OS/2 does offer an alternative if you use one of the system semaphores.
The semaphore is created with a DosOpenSem call, which returns a
semaphore handle, similar to a file handle. By using other semaphore
calls, a process can effectively keep reentrancy from occurring. In the
event that a process owning the semaphore at that time, and therefore
blocking others waiting on it, gets killed for some reason, even
unintentionally, the system will effectively call DosCloseSem, which
clears the semaphore if set and restores it as a system resource if there
are no other references to it.

Each individual session of DLL_CHAT uses a few resources, which I wanted
to make sure were properly cleaned up as a session exits. I used the OS/2
system call DosExitList to add some specific routines to each session's
exit list, the list of routines that OS/2 executes on my behalf between
the time the client program dies and the time it is buried. Currently,
the exit routine simply calls the logout procedure, which in turn resets
the systemwide flag word and the bits in each message and finally cleans
up the message base and any outstanding semaphores.

When designing a DLL, you should always think of worst-case scenarios,
such as what would happen if this line of code were running while ten
other processes were running those ten different lines of code. Since you
cannot effectively control what the other processes are doing as they
start to execute common areas of code, it is better to design the code as
modularly as possible and be sure to semaphore around areas sensitive to
multitasking occurring at just the wrong time──chances are that it will.

Remember that you must not only program defensively against other
processes using the DLL routines and their attendant data; if you opt to
use OS/2 threads, you have to protect against their reentrant use of the
DLL routines. Most of these considerations regarding DLLs can also be
important when designing a threaded program.

When considering unanticipated or asynchronous interruptions, you
should think about signal catching and about not doing it in a DLL. If
you're going to use the system to set a routine to catch a particular
asynchronous event, such as program termination or Ctrl-C trapping
performed with the DosSetSigHandler system call, doing it in the DLL can
be dangerous. This is a general concept that can be applied toward
program design not just in OS/2 but in any multiprocessing operating

The concept of resource plays a critical role here. Who owns the resource
of a signal catcher──an interrupt vector or another system facility in a
multiprocessing operating system? Generally, the operating system
itself owns the resource. When you take over such a system resource for
your own purposes, you must remember that other processes might cause the
resource to be used──and that you have no control over the event that
resulted in the use of the resource.

Normally, this would not be much of a problem, since the state of the
resource can be saved before you take it over and then restored when
you're done with it. But with at least one system facility, DosError,
which allows a process to suspend hardware error processing, the state
cannot be preserved for the next call or use. Thus, the state of the
system can get pretty confused.

Similarly, it is probably a good idea to avoid DosSetVect, which lets your
exception handler be called when certain conditions, such as attempts to
execute an illegal opcode, occur. Generally, if there is a possibility
that some state in the operating system cannot be preserved owing to your
taking over a system resource or facility, you should consider designing
your code differently.

If you must include such calls in your code, be sure to isolate thoroughly
those portions of the code from other client members of the DLLs, and to
preserve all other aspects of your process state. Be sure to terminate
normally, too, not in a unique way, since DLLs have some special
characteristics that are properly dealt with in automatic exit list
processing upon client death.

Design Considerations

When designing your program to use DLLs, you have to be careful about a
few things in your initial program design. First, access to all DLL
routines is through a far call. So, although you can use the small memory
models in the client section of your code and in the DLL itself, the
external definition of the DLL routines must indicate it is a far routine.
As such, the routine itself must also indicate in its prototype that it
is a far routine; or else the CALL and RET statement types won't match.

Remember to design your call to a DLL routine and the routine itself using
the Pascal calling convention instead of the C calling convention. Although
this is not a strict requirement (however, both calling sequences must
match), it's a good idea for several reasons. First, it's required if you're
using the routine as an IOPL gate. Also, it's more efficient, since the
stack restore takes place only once in the called routine itself, and more
languages support the Pascal convention, allowing your DLL to be used by
these other languages.

What about the data allocated in the DLL? That, too, must be addressed as
far data from the client routines. Locally, within the DLL, it can be
addressed as near or far as required.

Before your client code ever executes, the initialization routine for the
DLL will already have executed. Expecting any initialization by the main
routine in your client code would be premature. Therefore, your DLL
initialization code should only access data within the DLL itself, since
the startup code may not even have allocated memory as of yet. You can
find out a great deal when the loader calls your initialization
routine, including the handle by which the DLL has been opened as well as
the size of the heap in the calling EXE program. You can find more
information on this in the OS/2 Programmer's Guide.

What of the differences between global and instance data items? They can
be confusing, since each DLL module has no easy method to determine
whether the data object it is using is a private object or one common to
all tasks. This can be tricky, since many programmers routinely use
temporary pointers to objects that they place in global data space
instead of allocating it on the stack for local use.

It is important to recognize the differences between global data, such as
items defined and allocated outside the scope of any C routine, and
globally accessible data. In the first case, it is really local data, that
is, data local to the client process itself and not accessible to other
clients of the DLL. In the second case, it is accessible to all clients of
the DLL, and that can fool you if you're not careful. You must be sure
that globally accessible data items don't change value when you're not
looking. Keeping items on a local stack frame is probably the safest bet.
Items that are kept around without changing value are best placed in
private client data space.

You can easily indicate through the DATA statement in the EXPORT file
which data segments you want to be allocated on a private per-client
basis and which you want to be globally accessible. If a segment is marked
as READONLY, then it is globally accessible. The Microsoft(R) C data group
named CONST should always be marked as READONLY; this allows for only one
copy of literal strings to be loaded for the entire DLL.

Using Microsoft C

There were some rumors floating around for a while that it was impossible
to create DLLs with a C compiler because the stack segment (SS) did not
equal the data segment (DS) upon entry into a DLL──clearly a run-time issue
and not a compiler problem. Since the library has many routines that
expect them to be equal, it appeared at first that creating DLLs in C was
expressly forbidden, but this is not the case.

There are some specific enhancements available in the Microsoft C Version
5.1 compiler that make writing C DLLs very easy.

A new pragma, pragma data_seg, allows you to specify for any function that
later loads its own data segment exactly which data segment to use. By
specifying the data segment as

  #pragma data_seg (segment_name)

you not only facilitate using DLLs, but you have more control over which
data segment all initialized static and global data will reside in. The
default data segment name if you don't specify one is the name used by
DGROUP, which depends on the memory model you use.

This is half of the solution of which data segment to use in the DLL. The
other half is to specify the called function as one that uses the
previously saved data segment with the _loadds keyword. Upon entry into a
_loadds function, the current DS register is saved, the last one specified
by the pragma data_seg directive is written into it, the function is
executed, and the saved DS is restored upon exit. This is not such a new
concept, since you've been able to use /Au as a compiler option for some
time now, but this lets you specify some capability on a function-by-
function basis.

In order for the compiler to know, in advance, that the routine is going
to be part of a dynamic-link library, the new keyword _export has been
added. In particular, if the function has an I/O privilege level (IOPL)
associated with it, then the number of words to reserve for the privilege
level transition stack copy operation can be easily calculated at compile
time if _export is used. In fact, if you use _export as part of your
function definition, the number of words to reserve as indicated in the
DEF file is ignored.

When setting up the various data segments into their constituent types
(SHARED and READONLY, for instance), you should also take a look at the
map file produced from the link; some additional segments might be created
that you hadn't thought about. In particular, some NULL segments are
created for each group as _CONST and _BSS. So as not to confuse the
linker, each member of the group should be specified within the SEGMENTS
section of the EXPORT DEF file, and you need only mention the special
segments, those with attributes different from the default setting of the
DATA statement.

Creating the Initialization Routine for Your C DLL

Remember that the DLL, once passed through the linker, looks much like an
EXE file. In fact, the same load routine used for your own client module
loads the DLL itself. And, if you've defined an initialization routine
within the DLL, it will be executed almost as if it were a standalone
routine; it is called immediately after the DLL is loaded, and then only
called upon subsequent loads of the DLL, if you specify so.

You can easily tell the loader where the initialization routine is located
by including a small assembly language routine as part of your DLL and
linking it into the DLL. In fact, it is probably not a bad idea to have a
module similar to the one in Figure 9 and to always give your DLL
initialization routine the same name. The secret of the initialization
routine is just that the only program the loader will find is the one that
is addressed by the END START directive.

The MSC compiler throws a small monkey wrench in your path. Meaning to be
helpful, the compiler throws the _acrtused variable into each object
module. This forces the linker to include some of the startup routines
from the C run-time library into the eventual output of the linker, which
the compiler thought was going to be a normal EXE file. To prevent this
code from being loaded into your DLL, you should define the variable
yourself, as external data:

  int _acrtused = 0x1234;

or some number of particular meaning to you.

Also, to have global data items show up in the named segment for the
particular object module that you are linking, it should be initialized,
declared as static, or both. Finally, you could also allocate static or
global data using the keyword const along with the appropriate compiler

When writing your own DLL, you should use the -Gs switch on the MSC
compiler to disable stack checking. Aside from the slight gain in
efficiency──somewhat smaller code and one less function call per
function──the DLL requires this since the stack segment is different for
each client process and the size of the stack may vary on a per-client
basis as well. In the few places where you really need stack checking, MSC
provides you with an abundant set of pragmas and routines.

MSC 5.1 includes some welcome additions to the run-time libraries
package. These include a library that provides multithread support, a
run-time library that is statically linked with your routines to provide
for a DLL written almost entirely in C, and a run-time library that, in
and of itself, is a dynamically linked library and works with EXE and
other DLL libraries. The new DLL run-time libraries also allow you to use
any of the functions that you've grown accustomed to.

There are some subtle differences in the way certain things are handled
in a multitasking/multiprocessing environment, but they are hidden
from view in some of the header files. The errno variable, for example, is
now a macro that translates into a function call. This is done because a
table must now be used in the functions of the run-time library to enable
a single run-time package to handle errors from multiple sources.

By the look of things and how they operate, it is most likely safe to
assume that semaphores were used throughout the library──to keep a call
using a globally accessible variable from being clobbered by two client
processes trying to use it simultaneously. This forces a heavy overhead in
system calls to frequently called routines, but there is little choice in
this matter. Remember that the libraries had to be written under a worst-
case scenario, and you pay a penalty in speed and efficiency for the
safety inherent in putting semaphores around the dangerous routines (see
Figure 10).

Certain idiosyncracies do exist in the C libraries, however. For example,
the memory allocation functions alloc, malloc, and calloc follow the
ANSI calling convention. However, the ANSI calling convention does not
allow you to specify whether memory should be private or shared between
different processes. Microsoft has yet to add functions to the library
that allow this; this is the reason for the rather convoluted routines
used in DLL_CHAT for dealing with allocating memory in a shared way.


With the introduction of DLLs in OS/2, a new programming environment has
been created. Much like Windows and Presentation Manager programming, it
has its own strict rules, but they make a great deal of sense once the
underlying design concept and limits of the chip set and the appropriate
portions of OS/2 are understood.

You can avoid a lot of these sticky problems by piece-at-a-time
programming: get as much of your program to work using routines in a more
normal library using the library utilities and then move the routines out
into a DLL. By adding the functionality and safeguards required for shared
memory access between sessions and reentrancy problems, you can easily
create a program that uses up less disk space, less memory space, and
allows for interprocess communication in whatever manner you wish to
design-not bad features for a new operating system to be written around.

And once you've been through the maze of twisty little passages, it isn't
so hard the next time to get through it rapidly and collect that treasure.
The secret is just knowing a couple of key phrases and thinking ahead
before you enter the maze.

Figure 1:  Once a DLL is created, use IMPLIB to create a LIB file from the
           EXPORT DEF file. The LIB file provides a dynamic-link reference
           that is built into the EXE file. The OS/2 loader then uses this
           information to resolve DLL references when the EXE file is
           executed. By providing both the DLL and LIB files, the internal
           format of DLL remains hiddenfrom the user of the DLL.

Step 1: Create the DLL
 │OBJ Files              │  │                        ╔═════╗
 └───────────────────────┘  │                    ┌──►║ DLL ║
 ┌───────────────────────┐  │      ╔══════╗      │   ╚═════╝
 │Static Library         │  ├─────►║LINKER╟──────┤
 └───────────────────────┘  │      ╚══════╝      │   ╔═════╗
 ┌───────────────────────┐  │                    └──►║ MAP ║(Optional)
 │Export Definition File │  │                        ╚═════╝

Step 2: Create the LIB file
 ┌───────────────────────┐         ╔══════╗          ╔══════════╗
 │Export Definition File ├────────►║IMPLIB╟─────────►║ LIB FILE ║
 └───────────────────────┘         ╚══════╝          ╚══════════╝

Step 3: Create an Application that Uses the DLL
 │LIB File               │ │                         ╔═════╗
 └───────────────────────┘ │                     ┌──►║ DLL ║
 ┌───────────────────────┐ │       ╔══════╗      │   ╚═════╝
 │Application OBJ Files  │ ├──────►║LINKER╟──────┤
 └───────────────────────┘ │       ╚══════╝      │   ╔═════╗
 ┌───────────────────────┐ │                     └──►║ MAP ║(Optional)
 │Static Library         │ │                         ╚═════╝

Step 4: Load the EXE File and Resolve DLL References
 │ DLL │ │
 └─────┘ │       ╔═══════════╗       ╔═════════════════════════════════╗
         ├──────►║OS/2 LOADER╟──────►║ IN MEMORY EXECUTION OF EXE FILE ║
 ┌─────┐ │       ╚═══════════╝       ╚═════════════════════════════════╝
 │ EXE │ │

Figure 2:  Using IMPORT DEFINITION files is a better method for developing
           your DLL than using LIB files. There is no reason not to use
           IMPLIB for your finished DLL and its distribution. The EXPORT DEF
           file allows you to define everything that you need to.

 │       OBJ Files          │ │
 └──────────────────────────┘ │
 ┌──────────────────────────┐ │    ╔═════════╗        ╔═══════╗
 │      Static Library      │ ├───►║  LINK   ╟────┬──►║  DLL  ║
 └──────────────────────────┘ │    ╚═════════╝    │   ╚═══════╝
 ┌──────────────────────────┐ │                   │   ╔═══════╗
 │     EXPORT DEF File      │ │                   └──►║  MAP  ║(Optional)
 └──────────────────────────┴─┘                       ╚═══════╝
 │     IMPORT DEF File      │ │
 └──────────────────────────┘ │
 ┌──────────────────────────┐ │    ╔═════════╗        ╔═══════╗
 │        OBJ Files         │ ├───►║  LINK   ╟────┬──►║  EXE  ║
 └──────────────────────────┘ │    ╚═════════╝    │   ╚═══════╝
 ┌──────────────────────────┐ │                   │   ╔═══════╗
 │      Static Library      │ │                   └──►║  MAP  ║(Optional)
 └──────────────────────────┴─┘                       ╚═══════╝

Figure 3:  A Local Descriptor Table (LDT) contains a list of private
           descriptors accessible only to the local task. These descriptors
           can point to data segments, executable segements, call gates, and
           task gates. There can be at most 8192 descriptors, and each LDT
           can be, at most, 65,355 bytes long. Note that all LDT descriptors
           must reside, or be defined, in the Global Descriptor Table.

                  GDT                       ╔══════════════════════════════╗
┌─────────────────────────────────────┐ ┌──►║            LDT(1)            ║
├─────────────────────────────────────┤ │   ║EXECUTABLE SEGMENT DESCRIPTORS║
├─────────────────────────────────────┤ │   ║TASK GATE DSCRIPTORS          ║
│GLOBAL CALL GATE DESCRIPTORS         │ │   /               :              /
├─────────────────────────────────────┤ │   ║               :              ║
│GLOBAL TASK GATE DESCRIPTORS         │ │┌─►║            LDT(N)            ║
├─────────────────────────────────────┤ ││  ║DATA SEGMENT DESCRIPTORS      ║
├─────────────────────────────────────┤ ││  ║CALL GATE DESCRIPTORS         ║
│       LDT(1) DESCRIPTOR             │  │  ╚══════════════════════════════╝
/               :                     /  │
│       LDT(N) DESCRIPTOR             ├──┘

Figure 4:  DEF File Options and Parameters

LIBRARY [name][init_type]

NAME [appname][apptype]

CODE [load][executeonly][iopl][conforming]

DATA [load][readonly][instance][iopl][shared]

SEGMENTS <segmentname> [CLASS 'classname']

EXPORTS <name>[=internalname][@ordinal][RESIDENTNAME][pwords]

IMPORTS [name=]<modulename>.<entryname>

Figure 5:  Additional DEF File Options

STUB 'filename'     Allows you to specify the name of a DOS 3.x file to
                    be run if this file is run under DOS instead of under

PROTMODE            Indicates that this file can only be run in protected
                    mode. An aid to the linker.

OLD                 This statement allows you to preserve the names
                    associated with ordinal numbers in a multi-DLL

REALMODE            The opposite of PROTMODE, this indicates the program
                    can only be run in real mode, and is an aid to the

EXETYPE             Insures that the specified operating system is the
                    current one for the program. You can specify OS2,

HEAPSIZE            Determines how much local heap must be allocated within
                    the automatic data segment.

STACKSIZE           Allows you to specify how much space should be reserved
                    in the stack segment when the program is run.

Figure 6:  A Typical IMPLIB Statement


Figure 7:

Also see:
  An overview of the way DLL_CHAT interfaces to its dynamic-link library

DEF File Format for DLL_CHAT


Header File for DLL_CHAT

/* Header file for DLL_CHAT */

struct gdtinfoarea{
    unsigned long    time;
    unsigned long    milliseconds;
    unsigned char    hours;
    unsigned char    minutes;
    unsigned char    seconds;
    unsigned char    hundreths;
    unsigned         timezone;
    unsigned         timer_interval;
    unsigned char    day;
    unsigned char    month;
    unsigned         year;
    unsigned char    day_of_week;
    unsigned char    major_version;
    unsigned char    minor_version;
    unsigned char    revision_number;
    unsigned char    current_screen_group;
    unsigned char    max_num_of_screengrps;
    unsigned char    huge_selector_shift_count;
    unsigned char    protect_mode_indicator;
    unsigned         foreground_process_id;
    unsigned char    dynamic_variation_flag;
    unsigned char    maxwait;
    unsigned         minimum_timeslice;
    unsigned         maximum_timeslice;
    unsigned         boot_drive;
    unsigned char    reserved[32];

struct ldtinfoarea{
    unsigned    current_process_pid;
    unsigned    parent_pid;
    unsigned    priority_of_current_thread;
    unsigned    thread_id_of_current_thread;
    unsigned    screen_group;
    unsigned    subscreen_group;
    unsigned    current_process_is_in_fg;

Code Listing for DLL_CHAT

* DLL_CHAT.C  - A demonstration program using a demo DLL
* (C) 1988, By Ross M. Greenberg for Microsoft Systems Journal
* This is the main body of the CHAT program, interfacing with
* and calling the DLL as if it were a bunch of routines
* available with a far call: which it is!
* Compile with:
* cl /c chat.c
* link chat,chat,,slibce+doscalls,chat
*/Remember: move the DLL itself into your DLL library directory

#include    <stdio.h>
#include    <stdlib.h>
#include    "chat.h"

#define        TRUE    1
#define        FALSE    0
#define        OK   TRUE
#define        NOT__OK  FALSE

#define        MAX_MSG        100
#define        MAX_MSG_LEN    80
#define        NULLP        (void *)NULL

/* The following OS/2 system calls are made in this module: */

extern far pascal dosexitlist();
extern far pascal dossleep();

/* The following DLL system calls are made in this module: */

extern far _loadds pascal login();
extern far _loadds pascal logout();
extern far _loadds pascal get_msg_cnt();
extern far _loadds pascal get_msg();
extern far _loadds pascal send_msg();

*    This is where the messages are stored, once received and
*    formatted. This could probably be replaced easily with a call
*    to calloc(), but then we wouldn't have to block on being a
*    background process

char    msg_array[MAX_MSG + 1][MAX_MSG_LEN];

/* Must be global so that before_death() can access it to logout */

int    my_id = 0;

#define    MAX_SLEEP    2

*    before_death()
*    Called the coins are lowered onto the eyes of this invocation
*    of CHAT. Any exit will cause this routine to be called. After
*    this routine calls the DLL logout procedure, it removes the
*    next exitroutine in the exit list.

void    far    before_death()
    dosexitlist(3, before_death);

*    main()
*    After logging in (which returns a unique login id), the
*    before_death() routine is added onto the exitlist. Then the
*    main loop:
*    If there are any messages, read them into out memory buffer
*    (provided there is room) and kick up the count. After
*    retrieving all the msgs which will fit, call the display
*    routine for each msg. Zero the count of messages when done.
*    Every couple of seconds, sleep for a little while (as if a
*    human typing on a keyboard), then send the message to all
*    other members of CHAT.
*    CHAT can only be exited (in its current form) with a
*    control-C or error condition.

int        msg_cnt = 0;
int        msg_id = 0;
int        l_cnt;
char       tmp_buf[MAX_MSG_LEN];
int        mcnt;

    printf("Logged into CHAT as user:%d\n", my_id = login());

    dosexitlist(1, before_death);

    while    (TRUE)
        for (m_cnt = get_msg_cnt(my_id); m_cnt ; m_cnt-)
            get_msg(my_id, (char far *)tmp_buf);
            if    (msg_cnt <= MAX_MSG)

        if    (ok_to_disp())
            for (lp_cnt = 0 ; lp_cnt < msg_cnt; lp_cnt++)
            if    (msg_cnt > MAX_MSG)
                disp_msg("Looks like you might have lost some")
                disp_msg(" messages while you were away\n");
            msg_cnt = NULL;

        if    (rand() % (my_id + 1))
            dossleep((long)(rand() % MAX_SLEEP) * 1000L);
            sprintf(tmp_buf, "Test message #%d from Session #%d\n",
                    msg_id++, my_id);
            if    (send_msg(my_id, (char far *)tmp_buf) == NOT_OK)
                printf("?Can't send a message....\n");

char    *ptr;
    printf("%s", ptr);

extern far pascal dosgetinfoseg();

struct gdtinfoarea far *gdt;
struct ldtinfoarea far *ldt;
unsigned    gseg;
unsigned    lseg;

    dosgetinfoseg((char far *)&gseg, (char far *)&lseg);
    gdt = (struct gdtinfoarea far *)((long)gseg << 16);
    ldt = (struct ldtinfoarea far *)((long)lseg << 16);

    return( gdt->foreground_process_id == ldt->parent_pid);

An overview of the way the example program, DLL_CHAT, interfaces to its
dynamic-link library.

│                  DLL_CHAT.C  (First session of DLL_CHAT)                 │
│     login()     │   get_msg_cnt()   │    get_msg()     │   send_msg()    │
    │       │        my_id      │         my_id    │       my_id      │
    │     my_id        │     msg_cnt        │      │         │     status
    │       │          │        │          ptr     │        ptr       │
    │       │          │        │           │      │         │        │
│     login()     │   get_msg_cnt()   │    get_msg()     │   send_msg()    │
│(returns first   │(returns msg cnt   │ (copies first    │(allocates memory│
│unused login     │of outstanding     │ outstanding      │as required for  │
│slot)            │messages for       │ msg for this     │message structure│
│                 │login id)          │ id into array    │and message, then│
│                 │                   │ at ptr)          │copies msg at ptr│
│                 │                   │                  │into this memory)│
│                DLL_CHAT.LIB  (Other sessions of DLL_CHAT)                │
    │       │          │        │           │       │         │      │
    │     my_id      my_id      │         my_id     │       my_id    │
    │       │          │     msg_cnt        │       │         │   status
    │       │          │        │          ptr      │        ptr     │
│     login()     │   get_msg_cnt()   │    get_msg()     │   send_msg()    │
│                 DLL_CHAT.C  (Other sessions of DLL_CHAT)                 │

Figure 8:




EXPORTS get_msg_cnt
EXPORTS send_msg
EXPORTS logout
EXPORTS get_msg

Code listing for CHATLIB

*    CHATLIB.C  - A demonstration Dynamic Link Library
*    (C) 1988, By Ross M. Greenberg for Microsoft Systems Journal
*    This DLL, when used with the CHAT program acts as a central
*    repository for all messages being passed
*    Compile with:
*    cl /AL /Au /Gs /c chatlib.c
*    Note -    Though broken here the following two lines are
*              entered as one line:
*    link startup+chatlib+chatmem,chatlib.dll,,llibcdll+doscalls,
*    chatlib/NOE
*    Remember to move the DLL itself into your DLL library
*    directory

#include    <stdio.h>
#include    <stdlib.h>
#include    <malloc.h>
#include    <dos.h>

#define      TRUE    1
#define      FALSE    0
#define      OK    TRUE
#define      NOT_OK    FALSE

#define      NULLP     ((char *)NULL)
#define      GET_SEM    (dossemrequest(&semaphore, -1L))
#define      RLS_SEM    (dossemclear(&semaphore))

/* The following OS/2 system calls are made in this module: */

extern far pascal dossemrequest();
extern far pascal dossemclear();

/* The following external calls are made in this module: */

char *my_calloc();

/* This semaphore used to coordinate access to "critical" areas */

long    semaphore = 0;

*    This structure defines the members of the linked list
*    which starts at master_ptr.  Once a structure is allocated,
*    it is never released, although the character array member
*    msg_ptr points to will be released when the message is no
*    longer needed
#define    MSG    struct    _msg
    MSG       *next_ptr;  /* Point to next MSG, or NULLM       */
    char      *msg_ptr;   /* Point to the actual message       */
    int       msg_len;    /* length of the message - optional  */
    unsigned  f_word;     /* flag_word. When set to 0xfff      */
                          /* all chat members have seen this   */
                          /* message, so it can be freed       */

int    flag_word = 0xffff; /* This is the word that f_word is  */
                           /* set to initially. It is modified */
                           /* so that each bit is "off" if that*/
                           /* "user" is logged in              */

#define    NULLM    ((MSG *)NULL)
MSG    *master_ptr = NULLM;    /* Where the linked list begins    */

*    new_msg_struct(pointer to last MSG)
*    Allocates a new MSG, initializes the contents of the
*    structure, and sets the linked list if not the first time
*    called (last_msg != NULLM)
*    Returns a pointer to the structure allocated, NULLM if an error

MSG    *new_msg_struct(MSG *last_msg)
MSG    *tmp_ptr;

    if  ((tmp_ptr = (MSG *)my_calloc(sizeof(MSG), 1)) == NULLM)

    tmp_ptr->next_ptr = NULLM;

    tmp_ptr->msg_ptr = NULLP;
    tmp_ptr->msg_len = NULL;

    tmp_ptr->f_word = flag_word;

    if  (last_msg != NULLM)
        last_msg->next_ptr = tmp_ptr;


*    initialize()
*    Called either by the initialization routine of the DLL, or by
*    the first login. It allocates the first MSG structure, then
*    allocates and sets up for the next member

void far _loadds pascal initialize()

    if  ((master_ptr = new_msg_struct(NULLP)) == NULLM)
        printf("Couldn't allocate MSG memory for header...\n");


*    login()
*    If the master MSG structure hasn't been allocated already by
*    an earlier call to initialize() (by the DLL initialize
*    routine), then make the call now.  Memory has already been
*    allocated, therefore, so now give ourselves access to the
*    segment we've allocated.
*    Get the next free bit slot in the flag word, set it to
*    indicate it's in use, then return our login id.

int far _loadds pascal login()
int    log_id;
int    tmp_msk;

    if  (master_ptr == NULLM)
        printf("Init in login\n");


    for (log_id= 0 ; log_id < 16 ; log_id++)
        tmp_msk = mask(log_id);
        if  (flag_word & tmp_msk)
            flag_word &= ~tmp_msk;


    printf("Login slots all used up!\n");

*    get_msg_cnt(login_id)
*    For every MSG structure in the linked list with an associated
*    message attached to it, increment a counter if the id in
*    question hasn't received it yet, then return that counter
*    when we fall off the end.

int far _loadds pascal get_msg_cnt(int id)
MSG    *tmp_ptr;
int    tmp_cnt = 0;
int    tmp_msk = mask(id);

    for(tmp_ptr = master_ptr; tmp_ptr; tmp_ptr = tmp_ptr->next_ptr)
        if  (!(tmp_ptr->f_word & tmp_msk))
            if    (tmp_ptr->msg_len)


*    send_msg(login_id, pointer_to_message)
*    If there are no other "chatter's" logged in, simply return.
*    (Flag_word or'ed with our mask would be 0xfff)
*    Find a free MSG structure (guaranteed to have at least one,
*    since every write leaves a free one allocated if its the last
*    one in the linked list).
*    Allocate memory for the message, copy the message into it,
*    then assign the pointer in the structure and the length of
*    the message. Finally, allocate a new structure if required.

int far _loadds pascal send_msg(int id, char far *ptr)
MSG    *tmp_ptr = master_ptr;
int    tmp_len = strlen(ptr) + 1;

    if  ((flag_word | mask(id)) == 0xffff)

    while   (tmp_ptr->msg_len)
        tmp_ptr = tmp_ptr->next_ptr;

    if  ((tmp_ptr->msg_ptr = my_calloc(tmp_len, 1)) == NULLP)
        printf("Can't allocate %d bytes for msg\n", tmp_len);

    strcpy(tmp_ptr->msg_ptr, ptr);
    tmp_ptr->msg_len = tmp_len;
    tmp_ptr->f_word = (flag_word | mask(id));

    if  (tmp_ptr->next_ptr == NULLM)
        if  (new_msg_struct(tmp_ptr) == NULLM)
            printf("Can't allocate new MSG_header\n");


*    logout(login_id)
*    Mark every mesage as read (freeing them if now "totally"
*    read),reset the flag word, and then indicate that the logout
*    worked.

int far _loadds pascal logout(int id)
MSG    *tmp_ptr;
int    tmp_msk = mask(id);

    for(tmp_ptr = master_ptr; tmp_ptr; tmp_ptr = tmp_ptr->next_ptr)
        mark_msg(id, tmp_ptr);

    flag_word |= mask(id);


    printf("In logout ... Hit a Key:");fflush(stdout);


*    get_msg(login_id, pointer to buffer)
*    Find the first message the login_id hasn't read, then strcpy
*    it into the buffer supplied. Then mark the message as read
*    (freeing as required).

int far _loadds pascal get_msg(int id, char far *ptr)
MSG    *tmp_ptr = master_ptr;
int    tmp_msk = mask(id);

    for(tmp_ptr = master_ptr; tmp_ptr; tmp_ptr = tmp_ptr->next_ptr)
        if  (!(tmp_ptr->f_word & tmp_msk))
            strcpy(ptr, tmp_ptr->msg_ptr);
            mark_msg(id, tmp_ptr);

*    mark_msg(login id, pointer to message structure)
*    Mark our bit in the MSG f_word as set.  If then set to
*    0xffff, the message is "totally" read, so free it.
*    free(pointer to message structure)
*    If there is a string associated with this structure, free the
*    memory so used, then zero out the pointer and the msg_len

mark_msg(int id, MSG *ptr)
    ptr->f_word |= mask(id);
    if   (ptr->f_word == 0xffff)

free_msg(MSG *ptr)
  if (ptr->msg_ptr)
ptr->msg_ptr = NULLP;
ptr->msg_len = NULL;

            /* GENERAL ROUTINES */

/* This routine merely returns with the bit corresponding
 * to our login set

mask(int log_id)
    return(1 << (log_id - 1));

Additional Module for CHATLIB

/*   CHATMEM.C - Memory allocation routines for shared DLL memory
*    (C) 1988, By Ross M. Greenberg for Microsoft Systems Journal
*    This module contains three functions. Allocation of memory,
*    de-allocation of memory and the getseg call.
*    The current ANSI calloc/alloc/malloc sequence does not allow
*    for an additional parameter to specify if memory requested
*    through these functions is to be sharable or private.
*    Therefore the MSC library calls all allocate private memory.
*    These routines allocate a 64K chunk of memory, requested from
*    OS/2 as a sharable chunk, then dole it out using the DosSub
*    allocation calls.
*    Only one 64K chunk is allocated: if more memory is desired an
*    additional call to dosallocseg would have to be made and all
*    sessions already logged in given access to the chunk. Out of
*    laziness, that was not done for these demonstration
*    routines.
*    Compile with:
*    cl /AL /Au /Gs /c chatmem.c
*    Note - Though broken here, the following two lines are
*           entered as one line:
*    link startup+chatlib+chatmem,chatlib.dll,,llibcdll+doscalls,
*    chatlib/NOE
*    Remember to move the DLL itself into your DLL library
*    directory

#include    <stdio.h>
#include    <stdlib.h>
#include    <malloc.h>
#include    <dos.h>

#define    TRUE    1
#define    FALSE   0

/* The following OS/2 system calls are made in this module: */

extern far pascal dosallocseg();
extern far pascal dosfreeseg();
extern far pascal dossuballoc();
extern far pascal dossubset();
extern far pascal dossubfree();
extern far pascal dosgetseg();
extern far pascal dossemrequest();
extern far pascal dossemclear();

#define    NULLP    (char *)NULL

/*    This semaphore is so that we don't hurt ourselves as we
 *    allocate and deallocate memory

long    memory_semaphore = NULL;

/*    This is the actual selector which the 64K dosallocseg()
 *    call returns

unsigned major_selector = NULL;

*    my_calloc(number_of_items,size_of_item)
*    Emulates the more typical calloc call, but returns memory
*    which can later be allocated as sharable.
*    After the first call (which causes a 64K chunk to be
*    allocated), all subsequent calls cause a dossuballoc call to
*    be made, and a long pointer to the returned memory to be
*    created and returned
*    The 64K chunk must be initialized by the dossubset call
*    before it can be used by other dossub functions.
*    Because the dossubfree call requires a size, the size
*    requested plus the sizeof an int is actually allocated and
*    the size of the total request is then stored in the first two
*    bytes of the returned character array. The ptr returned,
*    however, is this memory location plus the initial sizeof and
*    int-therefore the bookkeeping is transparent to the
*    application task.

char *
my_calloc(size1, size2)
int    size1;
int    size2;
unsigned    long    selector;
int    stat;
char   *ptr;
int    sizeit = (size1 * size2) + sizeof(int);

    dossemrequest(&memory_semaphore, -1L);
    if  (!major_selector)
       if   (stat = dosallocseg(0, &major_selector, 3))
            printf("dosalloc error:%d\n", stat);

       if   (stat = dossubset(major_selector, 1, 0))
            printf("Error in dossubset:%d\n", stat);

    selector = 0;
    if  (stat = dossuballoc(major_selector, &selector, sizeit))
        printf("dossuballoc error:%d\n", stat);

    ptr = (char *)(((long)major_selector << 16) + (long)selector);
    memset(ptr, (char)NULL, sizeit);
    *(int *)ptr = sizeit;
    return(ptr + sizeof(int));

*    my_free(pointer_to_a_character_array_previously_my_calloc'ed)
*    Subtract sizeof an int from the pointer, dereference as an
*    int, then free that number of bytes.

char    *ptr;
int    stat;

    ptr -= sizeof(int);

    dossemrequest(&memory_semaphore, -1L);
    if (stat = dossubfree(major_selector, FP_OFF(ptr), *(int *)ptr))
        printf("Error freeing: %lx\n", ptr);

*    my_getseg()
*    Causes the memory affilaited with the major_selector to
*    become accessible to this process.

int    stat;

if  (stat=dosgetseg(major_selector))
        printf("Error on getseg:%d\n", stat);


Figure 9:  A DLL Initialization Routine

          ASSUME    CS:_TEXT
          call INITROUTINE    ; the real initialization routine
END       START               ; defines auto-init entry point

Figure 10:  An Example of Semaphored Printf() vs. "Free-style" Printf()

Consider the two code fragments below. In the semaphore version of the
code, individual printf()'s are not interrupted──whatever they are printing
will be printed to completion with other calls to the same printf()
routine waiting on a semaphore before proceeding. This semaphore is reset
when the current process using the library function is exiting from the
library function.

So-called "free-style" functions, on the other hand, can be interrupted
at any time-even in the middle of printing. Therefore, you might have two
(or more!) streams of data merged into some unreadable mess.

It's worse, however, if global data is in use by the interrupted free-
style function. Such data can be modified by the "interrupter," and could
cause unpredictable results.

 Program 1                           Program 2

 main()                              main()
 {                                   {
   printf("Task1:%d\n", cnt++);        printf("Task2:%d\n", cnt++);
 }                                   }

            Semaphored                          Free Style

            Task1:1                             Task2:1
            Task2:1                             Task1:1
            Task1:2                             Task1:2
            Task1:3                             Task1:3
            Task1:4                             Task2:2
            Task2:2                             Task2:3
            Task2:3                             Task2:4
            Task1:4                             Task1:4
               ∙                                   ∙
               ∙                                   ∙
               ∙                                   ∙
What would the apparent results be if two sessions, each normally
independent of the other, but sharing a Dynamic-Link Library version of
printf(), were to try to execute the printf() routine in a semaphored
versus a free-style approach? The two program fragments above, each
running as a separate session, show the differences. In the semaphore
version, each call made to printf() by each session is run to completion,
although the "set" might be interrupted. In the free-style version, there
is not even a guarantee that any given printf() will finish before it is
interrupted. The same would also hold true for multithreads of the same
session using a common printf() routine. The motto? "If in doubt, use

New Compiler Technology Boosts Microsoft QuickBASIC 4.0 Productivity

Augie Hansen☼

Compiler technology takes a quantum leap forward with the most recent
version of Microsoft(R) QuickBASIC. If you already have Microsoft QuickBASIC
Version 3.0, you received an invitation to upgrade to Version 4.0 for a
nominal charge, but don't let the word "upgrade" fool you. The Microsoft
QuickBASIC 4.0 compiler is a brand new product. The compiler, as in
earlier versions, comes in two flavors: a traditional command-line
oriented compiler, BC, and a speedy in-memory integrated environment, QB,
that truly deserves the label "instant environment."

Microsoft QuickBASIC 4.0 is based on threaded pseudocode technology that
has been under development at Microsoft for a number of years. The p-code
technology, as it is called, draws upon the threaded-code interpreter
environments used by FORTH programming products for many years, but with
some important extensions and improvements.

This article will take a close look at threaded p-code and the way it is
applied to Microsoft QuickBASIC 4.0. We'll show you how the tightly
coupled editor, p-code interpreter, and debugger all work together to
provide instant BASIC programming that will boost your productivity.
The development of a video terminal emulation program provides an
example of the program in action.


The p-code technology used in Microsoft QuickBASIC 4.0 offers a
programming environment with startling advantages over earlier
integrated environments. When you type a program line and press Enter
or the down arrow key, the line is immediately compiled to intermediate
p-code. At the same time, the line is formatted by the editor so that all
BASIC keywords are displayed in uppercase, and all operators are
surrounded by spaces.

The line is also checked in real time for syntax errors. If any are found,
they are reported in a dialog box; do not ignore such errors. The system
lets you move off the line after it has told you about the problem, but be
sure the line is syntactically correct before moving on to some other task.

The net result is that a program is always ready to run virtually
instantly. You can select Start in the Run menu (or press Shift-F5) at any
time to see the results of your programming labors. The first time you
start a program, it is translated from editable intermediate p-code to
executable threaded p-code, which is as close to unoptimized native
machine code as you can get without actually being there. This is as far
as the code can be taken so that it can still be returned to the ASCII
representation for editing.

You can stop program execution at any time, change the values of
variables, add or delete any number of lines, and then return to running
the program in the blink of an eye. The trick is that Microsoft
QuickBASIC 4.0 works on one small unit of code at a time; it decompiles
only what you are going to be working on. If, for example, you are
working on changes to a SUB (subprogram), only that SUB is returned to
an editable intermediate p-code state. When you finish your changes, you
simply select Run-Continue (or press F5) to continue executing where you
left off before editing. Since all variable values are preserved, your
program resumes execution in exactly the same state it was in before you
interrupted it.

Built-in debugging, as well as the syntax checking already mentioned, is
patterned after Microsoft CodeView(R). Within the Microsoft QuickBASIC 4.0
instant environment, you can set watch expressions, watchpoints, and
breakpoints, single-step program execution, and even backtrack through
previously executed statements.

The separate command-line version of the Microsoft QuickBASIC 4.0
compiler, the BC.EXE program, provides three important benefits. To begin
with, it lets you produce disk-based executable programs in either
standalone form or smaller executable versions that work with a single
run-time module. Second, it provides a debugging code option that lets you
use CodeView for source level debugging of your programs with the best
available full-screen debugger. Finally, it makes mixed-language
programming possible, allowing you to use existing routines written in
Microsoft C (Version 5.0 and QuickC(TM)), Pascal, FORTRAN, and the Macro
Assembler (MASM).

Recursion has been added to this version of Microsoft QuickBASIC. Although
recursion offers no substantial code size or speed advantages over
equivalent iterative programming methods, some types of problems, such
as the Quick Sort procedure and DOS-directory traversal, lend themselves
conceptually to recursive solutions.

Another valuable addition to BASIC is the user-definable data type, which
lets you define structured data types that are equivalent to C structures
and Pascal records. Records are important in the creation of composite
data types which simplify organizing and tracking data and ease the
burden of dealing with random-access files.

Microsoft QuickBASIC 4.0 supports only IEEE number formats. The Microsoft
binary format used in earlier versions of the compiler has been dropped,
but functions have been added in order to permit data stored in the
Microsoft binary format to be translated to the IEEE format. Also,
automatic detection and use of a numeric data processor (NDP), a math
coprocessor, has been added. Executable programs can run on any IBM-
compatible machine regardless of whether it has an NDP. If one is present,
it is used to obtain the greatest execution speed; if not, math operations
are emulated in software. There is no longer any need to have two
separate versions of programs for different hardware configurations.

The help system now has three levels. One level is visible on the status
line at the bottom of the screen whenever a menu or command selection is
visible at the top. You can also get a series of general help panels by
pressing F1 or pressing Alt-H and choosing the General... option.

Best of all, a new context-sensitive help feature gives you on-line access
to BASIC manual pages for all statements, functions, and other keywords.
You simply move the editing cursor into the item of interest, and press
Shift-F1 or select the Topic option from the Help menu. The view window
shrinks as much as necessary to display the help frame above your source
code (see Figure 1☼). The editor is still active while the help
information is on the screen, letting you continue to edit while looking
at the syntax of a statement. After you have viewed the help window, press
Esc to close it.

For those of you who are beginning to program under an OS/2 system,
Microsoft QuickBASIC 4.0 will be provided as a real-mode option under the
BASIC compiler Version 6.0. The BASIC compiler 6.0, which is completely
compatible with Microsoft QuickBASIC 4.0, is essentially what Microsoft
will provide for OS/2 BASIC programming. All OS/2 system calls will be
supported directly from BASIC in the OS/2 version of the compiler.

Program Development

The Microsoft QuickBASIC development cycle is shown in Figure 2. The
figure compares the Version 3.0 development cycle with the Version 4.0
cycle. As you can see, the compile step appears to be missing from the 4.0
cycle, at least from your perspective as a developer.

The compile step really is there, of course, but it happens so fast that
it's not usually visible to you. When you first read in a new module's
source from disk, you only see a compile delay as the statements are
parsed and compiled, and even that process is fast. Creating a new program
or editing a program that is already in memory produces an interpreter-
like operation.

Transitions among the three steps are bidirectional. You can switch from
the "run" mode to the "edit" mode and back easily and quickly, or between
run and "debug", or edit and debug. The seamless integration of the edit,
run, and debug steps into an instant environment is something to behold.

Microsoft QuickBASIC 4.0 is heavily geared toward modular programming. The
recommended units of modularity are procedures called subprograms (SUBs)
and functions (FUNCTIONs). FUNCTIONs are new to the environment and
should not be confused with the DEF FNs of older BASICs. Subprograms
have many of the attributes of BASIC subroutines, but you don't have to
GOSUB much anymore. Indeed, there are many reasons favoring the use of
subroutines instead of traditional BASIC subprograms.

SUBs accept parameters and provide for local storage. A variable defined
inside a subprogram is not visible outside that subprogram unless you go
out of your way to make it visible by sharing it. You can pass values into
a SUB via parameters that use the call-by-reference method, which makes
the address of a parameter available to the SUB so that the statements
in the SUB can affect the parameter itself.

If you want call-by-value parameter passing, which means that a SUB can
use the value passed as a parameter but cannot access the original
storage location, you can simulate it by assigning parameters into
intermediate variables and passing the intermediates. You can also
surround the expression or variable identifier in parentheses to force
call-by-value parameter passing.

Functions are similar to subprograms, except that they return a value by
assigning the value to the function name. You can use functions in expres-
sions just as you use BASIC intrinsic functions, such as the INPUT$

You can declare variables local to either subprograms or functions as
STATIC so that they retain their values from one call to the next.
Alternatively, you can declare them without the STATIC keyword, in which
case all local variables are dynamic. They are managed on the stack and
exist only while the procedure is executing.

When a program is in memory, it typically has code at two levels: module
level and procedure level. Module-level code is at the top level. Items
declared and defined at module level are global and are available
throughout the module. Procedure-level code is contained within
procedures and has local scope by default. Statements within procedures
can access module-level items if those items are declared in DIM or COMMON
statements. Procedures can also declare SHARED access to module-level

Quick Libraries are another important tool for writing modular programs.
You can create procedures in any Microsoft language, compile them to OBJ
files, and collect them into Quick Libraries (QLB extension) that can
then be loaded into the Microsoft QuickBASIC environment. The procedures
in loaded Quick Libraries are immediately available for use in the active
program. You can also ship libraries to other Microsoft QuickBASIC 4.0
users to incorporate into their programs.

You can create Quick Libraries and standard standalone libraries (LIB
extension) either from the Microsoft QuickBASIC instant environment or
from the command line. However, the one-step process of the instant
environment makes creating libraries straightforward and simple.

Threaded P-Code

As we have already seen, Microsoft QuickBASIC 4.0 is both a compiler and
an interpreter. The compiler keeps your program in a pseudocode state
that is about 90 percent machine code. This p-code retains enough
information to allow reconstruction of your original source while having
only one internal representation of the program in memory at a time.

This is crucial because the program is always ready to be run, debugged,
and edited without a significant amount of translation time, and almost
no execution speed penalty. And because of the single representation of
your program instead of separate source, object, and executable
representations in memory, there is more room for your program.

The threaded p-code interpreter is not a discrete program entity, but
rather a distributed interpreter. When you key in a BASIC source line,
Microsoft QuickBASIC immediately checks its syntax. If errors are found,
you are obliged to remove them before proceeding. If the syntax is good,
the source line is translated into a threaded p-code representation that
consists of a series of executor addresses, each of which is simply the
memory address of an executor.

An executor is a routine that participates in doing the work of a BASIC
statement. A single BASIC statement usually results in a series of
executors being chained together, or threaded. The threading is achieved
by the distributed interpreter, which consists of two machine
instructions. The cost is four bytes and seven CPU cycles per executor.
Each executor looks like this:

  Executor routine + LODSW ES + JMP AX

That's it. Your program is executed simply by chaining together all the
executors that are needed to carry out its tasks. There is no wasteful
CALL/RET instruction overhead, so your program runs quickly-about as fast
as unoptimized machine code will run.

Figure 3 shows how Microsoft QuickBASIC handles your program. The
environment has three states called "parsed," "symbolic," and "threaded."
A threaded p-code system with only the parsed and threaded states would
appear slow because the time needed to translate from parsed code to
threaded p-code accumulates. However, by introducing the intermediate
symbolic state, Microsoft has achieved the goal of instant program

The parsed state is the result of parsing your BASIC statements, either
from keyboard input or from disk files. Most of the syntax checking is
done during this translation. Microsoft QuickBASIC 4.0 has the portions
of your program that are affected by editing changes to COMMON, SHARED, or
DEF statements in this state while the changes are made. Translation back
to the threaded state occurs at a rate of about 60,000 lines per minute
on an 8-MHz IBM(R) PC/AT(R).

The symbolic state is distinguished by the efficient symbol table
information that permits fast data access. Most editing that does not
involve global impacts is done in this state. Only the portions of your
program that are affected by editing need to be brought to this state.

Your program is run and debugged in the threaded state; it is in the
threaded state most of the time. Translation from symbolic to threaded
state involves inserting type checking code, binding procedure calls,
control structures, labels to memory addresses, linking COMMON data, and
generating the p-code executor addresses.

When you make nonglobal changes to your program, only the affected
portions are returned to the symbolic state. Before the program can run
again, the edited portions are translated back to threaded state. The
round trip from threaded to symbolic and back to threaded occurs at about
150,000 lines per minute on a typical 8MHz AT machine. Because you usually
deal with a single program item at a time, such as a FUNCTION or SUB, this
translation is virtually instantaneous.

Debugging Made Easy

The tight coupling between the run step and the edit step (see Figure 1☼)
is also true of the relationship of the run and debug steps. The built-
in debugging feature is a substantial subset of the Microsoft CodeView
full-screen symbolic debugger. It lets you observe any variable or set of
variables in your program, single-step execution, set breakpoints, trace
the stack, and get a statement execution history. If you detect a run-
time error condition, you can use the editor to make needed changes
quickly and return immediately to the run step. SET NEXT STATEMENT is
also available, and allows the programmer to set the next statement to be

You can set a watchpoint, which is an expression that causes program
execution to stop when it becomes TRUE. The debugger opens a "watch
window" just above the view window to your source code and displays all
watch information in it. To observe the value of a variable or an
expression as you run your program, you can set watch expressions and
breakpoints. The debugger displays the values of watch expressions in
the watch window, stopping at each breakpoint so you can see the values
before continuing execution.

You can also single-step program execution (F8) to track values as each
statement is executed. In this mode, all statements in SUBs and
FUNCTIONs are single-stepped. You can also do procedure stepping (F10),
which treats each SUB and FUNCTION as if it were a single statement. In
addition, you can turn on Trace mode, which will do a slow-motion single-
step through your program, making flow analysis and diagnosis a breeze.

A history feature, enabled by either Trace On or a separate History On
activation, allows you to record up to the last 20 lines executed in your
program. You can then scroll backward and forward through the lines to
examine program execution. This is particularly useful when you need to
follow branching patterns of IF and SELECT CASE statements and to observe
looping statements.

If the built-in debugging features aren't enough for your needs, you can
also create EXE files with debugging code that let you use CodeView for
greater access to the most intimate details of your program. CodeView is
not supplied with Microsoft QuickBASIC, but it comes with MASM and other
Microsoft language products. To debug pure BASIC code, the built-in
debugger is probably more powerful than CodeView. However, CodeView
compatibility is invaluable for mixed-language programming.

Smart Editor

The editing capabilities of Microsoft QuickBASIC 4.0 are quite impressive.
Syntax-directed editing, "pretty printing" of source code lines, and
WordStar(R) editing-command compatibility are among the editor's many

Microsoft QuickBASIC breaks a BASIC program module into editable and
compilable units, such as SUBs and FUNCTIONs. As you create or edit, the
editor keeps track of these units and displays information about them on
demand in a program item display window. You select the program item you
want to edit by moving a highlight and choosing one of several

The editor supports split-screen editing so that you can edit one item
while viewing another. It also allows you to switch to an immediate window
to execute BASIC statements in what is the equivalent of direct mode in
earlier versions of the BASIC interpreter. The split screen capability
also works when debugging, which effectively allows you to view both the
calling code and the called code simultaneously.

You can use the editor as a standard text editor for ASCII files if you
wish. You simply turn off the BASIC Syntax Checking feature in the Edit
menu in order to disable the sensitivity to BASIC language statement

Command-line Compiler

The Microsoft QuickBASIC instant environment is great when learning to
program in BASIC, experimenting, and doing modest program development
tasks. Should you get involved in large program development projects,
perhaps working with other programmers, or if you need to do mixed-
language programming using C, Pascal, FORTRAN, or MASM in addition to
BASIC, you will probably need to use the command-line compiler.

The BC program, in concert with MAKE, LINK, and LIB, provides the
traditional compiler you need to handle large projects. Figure 4 shows
how the programs work together to produce executable programs and libraries.
Note that you can even use the QB instant environment as a front-end
development tool for the BC compiler. When you ask QB to produce an EXE
file, it calls on BC and the linker, LINK, to translate your program source
into optimized machine instructions.

Mixed-language programming involves the use of Microsoft-format object
libraries that can be linked into an executable program. Both the QB and
BC programs let you create object library (LIB) files. The LINK program
supplied with Microsoft QuickBASIC can link object modules produced by
the C, Pascal, FORTRAN, and BASIC compilers, as well as by MASM.

Figure 5 shows the way an executable program might be created by
combining Microsoft QuickBASIC 4.0, QuickC or C 5.0, and MASM 5.0 object
modules. The latest CodeView debugger can debug such a mixed-language
program at the appropriate source level as long as you specify that
debugging code be included at each compilation and assembly step. You
can also create Quick Libraries to be used by Microsoft QuickBASIC.

User-defined Types

Microsoft QuickBASIC 4.0 adds user-defined types to its growing list of
modern programming features. This is something that BASIC has needed for
a long time. A user-defined type is a data type that contains more than
one element. C has structures and unions, and Pascal has records; now
Microsoft QuickBASIC has TYPEs.

User-defined types let you model program data to fit real-world
conditions. Instead of having lots of separate variables that appear to
have no relationship, you can now define data types that clearly show
data relationships. Each element in a defined type must have a type name
of INTEGER, LONG, SINGLE, DOUBLE, STRING (with a length specifier), or
another user-defined type.

For example, you can easily mirror the structure of an inventory-control
card. Use the keyword TYPE to define the structure of the data, as shown
in Figure 6. Having defined the type, you can declare variables of this
type by using the DIM, REDIM, COMMON, STATIC, and SHARED statements.

To reserve storage for a set of 100 cards, you declare an array of items
of this type:

  DIM CardTable(100) AS_ CardType

You must define all user types at the module level. However, you can
declare variables of the defined type at both the module level and the
procedure level. Within a defined type, each string must have a fixed
length, which is specified on a string-by-string basis by the asterisk
number modifier. Thus, PartNumber AS STRING * 20 allocates a 20-character
string to the part number. In programs, you can use the new functions
LTRIM$ and RTRIM$ to remove leading and trailing blanks, respectively, in
fixed-length strings.

The Microsoft QuickBASIC documentation favors the use of the term "record"
to describe a variable of user-defined type. To access a record element,
use the period operator. The following statement assigns a value of 20 to
the StockQty element of the tenth card in CardTable:

  CardTable(10).StockQty_  = 20

To make things even more interesting, you can use defined data types in
the definition of elements of yet other user-defined types. The Supplier
AS SupplyType element, for example, indicates that a variable of the
user-defined type SupplyType is part of the user-defined CardType

A Video Terminal Emulator

The best way to see how Microsoft QuickBASIC 4.0 works is to try it. The
program shown in Figure 7 and called ADM3A.BAS is a simple video terminal
emulator. It is a vehicle for showing off several of the new features of
Microsoft QuickBASIC and demonstrating the user interface and is a useful
program in its own right. ADM3A.BAS makes an IBM-compatible PC act like a
simple video terminal, the Lear Siegler adm3a.

I wrote this terminal emulation because the adm3a terminal is recognized
by most UNIX(R) and XENIX(R) systems as well as many other multiuser systems.
The emulator allows me access to several visually oriented programs, from
the VI editor to custom-designed database programs.

Figure 8☼ shows the Microsoft QuickBASIC Run menu. To run the emulator from
the Microsoft QuickBASIC environment, you would execute the Start command
by highlighting it on the menu or by simply pressing the keyboard shortcut
command, Shift-F5, while in the editing mode. Figure 8☼ shows the
selection of the Make EXE File... command, which produces an executable
program that you can run at the DOS command level. All Microsoft
QuickBASIC commands are available in menus like this, and all frequently
used commands have shortcut keyboard aliases.

The ADM3A program consists of a single module with a half-dozen
subprograms. The module-level code declares SUBs to let the compiler check
both the number of parameters to be passed to each SUB and their types.
Figure 9☼ shows the program item list that is maintained by Microsoft
QuickBASIC 4.0. The list keeps track of modules and procedures within
modules. You use this work screen to select, move, and delete modules and
procedures. The smart editor normally allows you to view and edit one
program item at a time. However, you can split the window into two parts
with the View menu to see different parts of a program at the same time.

The module-level code defines a set of symbolic constants for logical
values, colors, and cursor control. When you read a BASIC program, seeing
a cold, hard number like 3 gives you virtually no information about what
the value means. The context may provide some help, but

  COLOR 3, 1

is still a bit mysterious if you don't know what the numbers 3 and 1 stand

Using symbolic names for constant values is a better practice. Thus, the

  CONST BLACK = 0, _BLUE = 1, ..., CYAN = 3

lets you write a statement like


that provides the information a reader needs at a glance.

Notice the definition of a "record" data type, WinType. The "window type"
relates variables for the top, bottom, left, and right of a screen
window. The variables CmdWin and ViewWin are declared to be WinType
record type by the dimension statements.

The assignment statements following the variable declarations set up two
window areas on the ADM3A program screen. The top screen row becomes the
command window, and the rest of the screen is the view window. This
arrangement works well because the physical screen of an adm3a terminal
has 24 screen rows and is 80 columns wide. Putting the command line at
the top of the screen keeps it from being a distraction as the user's eyes
move from screen to keyboard and back. The InitScreen procedure takes care
of partitioning the screen into the command window and the view window
portions. The VIEW PRINT statement causes scrolling to occur within the
view window when print statements attempt to write past the last line.
The command window is stationary in this program.

Communications setup is easy in BASIC. The OPEN COM statement takes a
string parameter that specifies the serial port (COM1: or COM2:), the
transmission speed, and settings for parity and number of data bits and
stop bits. It opens the communication channel, if the necessary hardware
is installed, and sets up transmit and receive buffers and an interface
to your program.

The ADM3A program uses an optional DOS environment variable to customize
its setup. If the variable COMPARMS is defined in the DOS environment,
its value is used as the OPEN COM string argument. Otherwise, ADM3A uses a
set of built-in values. It then sets the values of the communication port
address and the BREAK signal mask, which are used in the BreakSignal

The main program loop alternately monitors the keyboard and the incoming
communication line buffer for input. From the perspective of the local
system, the program takes input from the keyboard and sends it to the
remote system over the communication line. It takes incoming data from the
communication line and puts it on the screen. This basic pattern is
altered by certain special codes.

When the user types anything at the keyboard, it is sent to the remote
system unless it is an extended code. The three ADM3A commands, Break,
Dial, and Quit, are initiated by extended codes, which are two-byte
sequences that have a NULL character as the first byte.

When INKEY$ returns an extended code, the value is sent in the form of an
argument to the DoCommand subprogram, which examines the second byte, the
scan code, and compares it with a list of command codes. The Alt-b and
Alt-d key combinations cause other procedures to be invoked. The Alt-q
command quits the emulator program by closing the communications
channel, restoring the display screen to full size and normal attribute,
and homing the cursor. The fourth scan code recognized by DoCommand is
that of the PC's Del key. On a terminal, the Del key sends the ASCII DEL
code (127), but the PC does not. So DoCommand converts the internal code
to the one expected by the remote system.

The Dial procedure implements simple keyboard dialing. When the user
types Alt-d, the program responds with the prompt "Number:" in the view
window and waits for a telephone number to be typed. After receiving a
number, the Dial procedure uses the ATDT string (assuming tone dialing on
a Hayes(R)-compatible modem) as a prefix for the telephone number and sends
the dialing command to the modem.

Once connected to the remote system, the user must log in and start a
terminal session in the manner expected by the remote host. During a
session, it may be necessary to tell the host to stop whatever it is
doing, which is usually done by pressing the Break key, if there is one.
UNIX and XENIX systems will accept a Del key for this purpose, too. The
BreakSignal procedure, initiated by the Alt-b command, sends a true break
signal to the host to get its attention. BreakSignal works by setting the
break bit in the communication port control register, holding it high for
a period of time that exceeds that of a normal character transmission
period, and then clearing the break bit.

The delay period, fixed at a half second in this implementation, is
created by the Delay procedure, which uses the BASIC TIMER function in a
loop to control the period. Delay gets the starting time and adds the
requested period to it, then continually checks for the arrival of the
future time specified by the sum. To prevent machine lockup at midnight,
or whenever the clock on the PC rolls over its count of seconds to 0, the
Delay procedure aborts if the current time value drops below the starting

Received data is similarly analyzed as it comes in. Most characters are
simply printed on the PC screen in the view window. However, a small set
of incoming character codes have special meaning: control codes that cause
absolute and relative cursor positioning and screen clearing. The adm3a
lacks line and character insert and delete features, so the emulation is

If the Esc code is seen, it could signal the start of a cursor-
positioning command. The cursor position is set by an escape sequence of
the form Esc=rc, where r is a row number and c is a column number, each
encoded as a character, and the ASCII space (decimal 32) represents row or
column 0. The logic of the ADM3A program detects the Esc code and looks
for the equal sign. If one is seen in the next character position, two
additional characters are read and converted to row and column numbers
relative to the view window values (Top and Left), and the cursor is
positioned. If the character following the Esc code is not an equal
sign, the program simply prints the escape character and whatever
follows it.

All other characters are examined by the ProcessInput procedure to see
whether they should be printed. Some codes are commands. Ctrl-z, for
example, clears the screen. Care is taken to handle boundary conditions
as the real adm3a terminal does. A request to move the cursor right when
it is already at the right window border, for example, uses a LOCATE
statement to implement the automatic margins feature, which moves the
cursor to the beginning of the next line, if there is one.

Debugging and testing is aided greatly by the built-in Microsoft
QuickBASIC debugging features. Figure 10☼ shows the screen appearance
after a watch expression, CmdKey$, and a breakpoint on the SELECT CASE
statement have been set. When you run the program, it stops at the
breakpoint so you can observe the value of the watch expression. You can
have multiple watch expressions and breakpoints. Press F5 to continue
execution after it stops at a breakpoint.

The sample session depicted in Figure 11☼ shows the ADM3A program accessing
a XENIX system. It is running the VI editor on the /etc/termcap file,
showing the termcap entry for the adm3a terminal, among others. Any other
program that knows how to interact with an adm3a terminal, and most of
them do, can run successfully with this emulation.

Programs that use fancy line-drawing characters, inverse video, and
special character formatting will not look too hot on an adm3a and may
not even run at all. You can use this emulation as a starting point for
more sophisticated emulations, such as DEC VT100-series terminals. Also,
you may want to add file transfer and other "intelligent terminal"
features to round out the program. Microsoft QuickBASIC has what it takes
to do that and much more.

Welcome to the Future

The technology in Microsoft QuickBASIC offers many benefits to its users.
Microsoft QuickBASIC ushers in the era of significantly higher
programmer productivity and injects a great deal of fun into what has
traditionally been drudgery.

Where to from here? Microsoft representatives have said publicly that a
significant amount of in-house work on current and future language products
is based on p-code technology, which will be the basis of compiler products
for other languages and some application programs. I hope that these
products will be announced in the near future.

Figure 2:  The Microsoft QuickBASIC Development Cycle

          │             Microsoft QuickBASIC 3.0            │
          │   ╔════════════╗               ╔════════════╗   │
          │   ║    EDIT    ╠══════════════►║  COMPILE   ║   │
          │   ║            ║               ║            ║   │
          │   ╚═══════════╝◄══════╗       ╚══════╦═════╝   │
          │         ║              ║              ║         │
          │   ╔═════╩══════╗       ╚═══════╦══════▼═════╗   │
          │   ║   DEBUG    ║               ║    RUN     ║   │
          │   ║            ║◄══════════════╣            ║   │
          │   ╚════════════╝               ╚════════════╝   │
          │             Microsoft QuickBASIC 4.0            │
          │   ╔════════════╗               ╔════════════╗   │
          │   ║    EDIT    ║═══════╗       ║  COMPILE   ║   │
          │   ║            ║       ║       ║            ║   │
          │   ╚════════╦══╝◄════╗ ║       ╚════════════╝   │
          │      ║      ║        ║ ║                        │
          │   ╔══╩══════▼══╗     ║ ╚══════►╔════════════╗   │
          │   ║   DEBUG    ║     ╚═════════╣    RUN     ║   │
          │   ║            ║◄══════════════╣            ║   │
          │   ╚════════════╝               ╚════════════╝   │

Figure 3:  Microsoft QuickBASIC 4.0 p-code

       │                  ╔═════════════════════╗          │        │
       │       ┌─────────►║       ASCII         ║          │  Disk  │
       │       │          ║       TEXT          ║          │   or   │
       │       │          ╚════════════════╤════╝          │ Screen │
       │       │                           ▼               │        │
       │       │          ╔═════════════════════╗          │   M    │
       │       │          ║       PARSED        ║          │        │
       │       │◄─────────╢                     ║          │   e    │
       │       │          ╚════════════════╤════╝          │        │
       │     LISTER              BINDER   ▼               │   m    │
       │       │          ╔════╧════════════════╗          │        │
       │       │          ║      SYMBOLIC       ║          │   o    │
       │       │◄─────────╢                     ║          │        │
       │       │          ╚════════════════╤════╝          │   r    │
       │       │                 BINDER   ▼               │        │
       │       │          ╔════╧════════════════╗          │   y    │
       │       │          ║      THREADED       ║          │        │
       │       └──────────╢                     ║          │        │
       │                  ╚═════════════════════╝          │        │

Figure 4:  The Microsoft QuickBASIC 4.0 Programming Environment

║   BAS   ╟────┐
╚═════════╝    │                                 ┌─────────┐    ╔═════════╗
               │                      ┌─────────►│   LIB   ├───►║   LIB   ║
╔═════════╗    │   ┌────────┐     ┌───┴────┐     └────┬────┘    ╚═════════╝
║   BI    ╟──┐ └──►│        │     │        │          │
╚═════════╝  └────►│        │     │        │     ┌────▼────┐    ╔═════════╗
                   │   QB   ├────►│   BC   ├────►│   LINK  ├───►║   EXE   ║
╔═════════╗  ┌────►│        │     │        │     └─────────┘    ╚═════════╝
║   MAK   ╟──┘ ┌──►│        │     │        │
╚═════════╝    │   └────────┘     └────────┘
╔═════════╗    │
║   QLB   ╟────┘

Figure 5:  Mixed-Language Programming

  ╔═════════╗  ┌────────┐
  ║   BAS   ╟─►│        │
  ╚═════════╝  │   BC   │
               │        │   ╔═════════╗
  ╔═════════╗  │        │──►║   OBJ   ╟───────┐
  ║    BI   ╟─►└────────┘   ╚═════════╝       │
  ╚═════════╝                                 │
  ╔═════════╗  ┌────────┐                 ┌───▼────┐  ╔═════════╗  ┌──────┐
  ║    C    ╟─►│        │                 │        │─►║   EXE   ╟─►│      │
  ╚═════════╝  │   CL   │   ╔═════════╗   │        │  ╚═════════╝  │      │
               │        │──►║   OBJ   ╟──►│  LINK  │  ╔═════════╗  │  CV  │
  ╔═════════╗  │        │   ╚═════════╝   │        │─►║   QLB   ╟─►│      │
  ║    H    ╟─►└────────┘                 └───────┘  ╚═════════╝  └──────┘
  ╚═════════╝                                 │
  ╔═════════╗  ┌────────┐   ╔═════════╗       │
  ║   ASM   ╟─►│  MASM  │──►║   OBJ   ╟───────┘
  ╚═════════╝  └────────┘   ╚═════════╝

Figure 6:  Example of the TYPE Keyword

TYPE CardType

     PartNumber AS STRING * 20
     Description AS STRING * 40
     UnitCost AS SINGLE
     StockQty AS INTEGER
     ReorderQty AS INTEGER
     CurrentQty AS INTEGER
     Supplier AS SupplyType

Figure 7:  Video Terminal Emulator

' ADM3A Terminal Emulator
' Version 1.0
' This program emulates a Lear Siegler adm3a video terminal.  The
' emulator gives PC users the ability to run full-screen video
' programs on UNIX and XENIX systems, and others that support a
' video terminal interface.
' Author: Augie Hansen
' Released: 1-14-88


'─ Subprogram declarations.
DECLARE SUB BreakSignal ()
DECLARE SUB Delay (Period!)
DECLARE SUB DoCommand (CmdKey$)
DECLARE SUB InitScreen ()
DECLARE SUB ProcessInput (Code$)

'─ Manifest constants.
CONST BLACK = 0, BLUE = 1, GREEN = 2, CYAN = 3
CONST ROWS = 25, COLS = 80

'─ Screen management data.
TYPE WinType
     Left AS INTEGER
     Bottom AS INTEGER
     Right AS INTEGER
     Fgnd AS INTEGER
     Bkgnd AS INTEGER
     Standout AS INTEGER

DIM CmdWin AS WinType
DIM ViewWin AS WinType

CmdWin.Top = 1
CmdWin.Bottom = 1
CmdWin.Left = 1
CmdWin.Right = 80
CmdWin.Fgnd = BLACK
CmdWin.Bkgnd = WHITE
CmdWin.Standout = BROWN + BRIGHT

ViewWin.Top = 2
ViewWin.Bottom = 25
ViewWin.Left = 1
ViewWin.Right = 80
ViewWin.Fgnd = WHITE
ViewWin.Bkgnd = BLUE
ViewWin.Standout = WHITE + BRIGHT

'─ Set cursor-positioning offsets.
RowOffset = SPACE - ViewWin.Top
ColOffset = SPACE - ViewWin.Left

'─ Install an error-recovery mechanism.
ON ERROR GOTO ErrorRecovery

'─ Set up the emulator screen.

'─ Set communications parameters.
Parm$ = ENVIRON$("COMPARMS")            ' Check environment.
IF Parm$ = "" THEN
     Parm$ = "COM2:1200,E,7,1"          ' Use defaults.

Port$ = LEFT$(Parm$, 4)
IF Port$ = "COM1" THEN
     PortAddress = &H3FB
     PortAddress = &H2FB
BreakMask = &H40                        ' Break control bits

'─ Open the communications channel.

' Main communications loop.
' Check the keyboard for input. Send all normal characters typed by
' the user to the communications port for transmission to the remote
' system. If the user presses any of the emulator command keys, run
' the associated procedure.
EscapeFlag = FALSE
     '─ Process keyboard input for commands and characters.
     UserKey$ = INKEY$
     IF LEN(UserKey$) > 1 THEN
          DoCommand UserKey$
     ELSEIF UserKey$ <> "" THEN
          '─ Send the character to the remote system.
          PRINT #1, UserKey$;
     END IF

     '─ Check the communications line for received characters.
          IF EOF(1) THEN
                  EXIT DO
          END IF
          Received$ = INPUT$(1, #1) ' Read a single character.

          '─ Look for cursor-positioning command.
          IF EscapeFlag = TRUE THEN
                  IF Received$ = "=" THEN
                  CursorRow = ASC(INPUT$(1, #1))-RowOffset
                  CursorCol = ASC(INPUT$(1, #1))-ColOffset
                  LOCATE CursorRow, CursorCol
                  PRINT CHR$(27); ' The retained Esc code.
                  PRINT Received$;
                  END IF
                  EscapeFlag = FALSE
                  ProcessInput Received$
          END IF


     RESUME Main

' BreakSignal
' Send a 'break' signal to the communications port.
SUB BreakSignal
     SHARED PortAddress, BreakMask

     '─ Set the break bit.
     OUT PortAddress, (INP(PortAddress) OR BreakMask)

     '─ Mark time for the break period.
     Delay .5

     '─ Clear the break bit.
     OUT PortAddress, (INP(PortAddress) AND NOT BreakMask)

' Delay
' Produce a specified delay.  The delay period is specified in
' seconds as a single-precision number with tenth-second precision.
SUB Delay (Period!) STATIC
     Start! = TIMER

     '─ Loop for specified period.  Abort if clock rolls over.
          Now! = TIMER
          IF (Now! - Start! < Period!) OR (Now! < Start!) THEN
                  EXIT SUB
          END IF

' Dial
' Ask the user for a telephone number and dial it.
SUB Dial
     INPUT "Number: ", Phone$
     PRINT #1, "ATDT" + Phone$

' DoCommand
' Examine the extended key code to see whether it is an Emulator
' program command.  If it is, execute the requested command.  If it
' is not, return to the caller without doing anything.
SUB DoCommand (CmdKey$) STATIC
     SHARED CmdWin AS WinType
     SELECT CASE ASC(RIGHT$(CmdKey$, 1))
          CASE 16 ' Alt+q - Quit the emulator.
                  ' Close the communications channel.
                  ' Restore full screen.
                  VIEW PRINT
                  ' Clear the screen and "home" the cursor.
                  COLOR WHITE, BLACK
                  LOCATE CmdWin.Top, CmdWin.Left, CURSORON
          CASE 32 ' Alt+d - Dial a number
          CASE 48 ' Alt+b - Send break signal.
          CASE 83 ' PC keyboard Del key - Send an ASCII DEL
                  PRINT #1, CHR$(127);
          CASE ELSE
                  ' Unknown command - ignore it.

' InitScreen
' Set up command bar (1 line), guarantee that the cursor is turned
' on, and establish the active terminal display window (24 lines).
     SHARED CmdWin AS WinType, ViewWin AS WinType

     '─ Initialize the screen for text and 80 columns.

     '─ Draw the command window on the top line.
     LOCATE CmdWin.Top, CmdWin.Left, CURSORON
     COLOR CmdWin.Fgnd, CmdWin.Bkgnd
     PRINT SPACE$(CmdWin.Right - CmdWin.Left + 1)

     '─ Display the program banner.
     LOCATE CmdWin.Top, CmdWin.Left + BANNERCOL
     COLOR CmdWin.Standout, CmdWin.Bkgnd

     '─ Display a command summary.
     LOCATE CmdWin.Top, CmdWin.Left + COMMANDCOL
     COLOR CmdWin.Fgnd, CmdWin.Bkgnd
     PRINT "Break (Alt+b)   Dial (Alt+d)   Quit (Alt+q)"

     '─ Initialize the terminal screen.
     VIEW PRINT ViewWin.Top TO ViewWin.Bottom
     COLOR ViewWin.Fgnd, ViewWin.Bkgnd

' ProcessInput
' Check input from the communications line and analyze it.  Act on
' any adm3a terminal commands codes.  Pass anything else unchanged
' to the terminal screen.
SUB ProcessInput (Code$) STATIC
     SHARED EscapeFlag, ViewWin AS WinType
          CASE 8 ' ASCII backspace character
                  IF POS(0) > ViewWin.Left THEN
                    ' Nondestructive backspace
                    LOCATE , POS(0) - 1
                  END IF
          CASE 10 ' ^J - New-line character
                  IF CSRLIN < ViewWin.Bottom THEN
                    LOCATE CSRLIN + 1
                    PRINT Code$;
                  END IF
          CASE 11 ' ^K - Up-line command
                  IF CSRLIN > ViewWin.Top THEN
                    LOCATE CSRLIN - 1
                  END IF
          CASE 12 ' ^L - Form-feed character
                  ' adm3a use as nondestructive space
                  IF POS(0) < ViewWin.Right THEN
                    LOCATE , POS(0) + 1
                  ELSEIF (POS(0) = ViewWin.Right) AND _
                         (CSRLIN < ViewWin.Bottom) THEN
                    LOCATE CSRLIN + 1, ViewWin.Left
                  END IF
          CASE 13
                  LOCATE , ViewWin.Left
          CASE 26 ' ^Z - Clear the screen
          CASE 27 ' Esc - Could be start of cursor sequence
                  EscapeFlag = TRUE
          CASE 30 ' ^^ - Cursor to home position
                  LOCATE ViewWin.Top, ViewWin.Left
          CASE ELSE
                  PRINT Code$;


Debug Microsoft Windows Programs More Effectively with a Simple Utility

Kevin P. Welch☼

Debugging is an indispensable and unavoidable step in the development of
any software program. With conventional MS-DOS(R) programs, debugging
usually starts with the incorporation of numerous print statements into
the program. If this doesn't work, the battle escalates to sessions with
DEBUG, CodeView(R), or even to utilizing a hardware-level debugger.

Unfortunately for Windows programmers, many of these traditional
techniques won't work in the Windows environment. The last few months have
seen significant improvements in Windows debugging──most notably the
announcement of CodeView for Windows──but programmers continue to struggle
with the difficult task of debugging and fine-tuning Windows

Approaches to Debugging

From a theoretical perspective, there are many ways to classify software
debugging methods. One common approach breaks debugging into three
general categories of invasive, noninvasive, and combined techniques.

Invasive debugging typically involves modifying and recompiling
application source code. The debugging tools and their entry points simply
become part of the program, which makes the technique easy to implement.
Two examples of this approach are hard-coded message boxes and print
statements whose output is directed to display or temporary log files.

This approach often suffices to catch a large proportion of coding and
algorithmic errors. With skilled use, this method can serve as the major
debugging tool for large and complicated applications.

However, this approach requires that you have access to the source code in
question (a problem when you are debugging an interaction with a program
you didn't write). In addition, the mere inclusion of the debugging
statements or tools into the application sometimes changes the very
character of the problem, further complicating the situation. This is
especially true with system-related issues like memory management and
interprocess communication.

Noninvasive debugging does not involve modification or recompilation of
an application. The debugging tool and its associated control functions
are external to the application and can usually be applied to an existing
program. Some examples of this approach include message loggers
(essentially a form of application subclassing), journaling hooks, alias
libraries, execution profilers, and even strict hardware-level

If used correctly, the noninvasive approach can provide a great deal of
externally derived diagnostic information. This technique is especially
useful in debugging complex interactions between applications.
Furthermore, using debugging tools that are external to the
application provides a limit on the possible compounding of errors.

On the negative side, this approach is often complicated, it only reports
on externally discernable events, and it tends to generate quantities of
unimportant information surrounding a few vital pieces of data. The
programmer usually has to do the interpreting of events reported, sorting
and searching for what is really important.

Combined debugging uses both invasive and noninvasive techniques. In most
cases, the program need only be recompiled with a different compiler
switch, which generates symbol files or other output useful to the
debugging tool. Some examples of this approach include CodeView, Symdeb,
and other more complicated hardware-debugging tools capable of working
with application internals.

This approach probably represents the most powerful of these debugging
techniques, and is capable of capturing and describing even the most
insidious coding or system error. Also, while tracking the problem the
programmer can usually inspect internal data structures, which provides
even greater insight into a given situation.

To use this approach effectively, the programmer needs to have an
intimate understanding of both the underlying operating system and the
hardware. The power of this technique is offset by a steep learning curve,
and the generation of a great deal of information. Many programmers
hesitate to use it except for the most puzzling and difficult of

Windows Debug Utility

After developing a number of Windows applications and struggling with
the problems of debugging, I decided to develop a simple invasive
debugging tool that would provide reasonable insight into the operations
that occurred within a troublesome application.

This effort resulted in the Windows Debug utility, a small object module
(about 4Kb compiled) that can be conditionally linked into your
program. It provides print statementlike debugging capabilities within the
Windows environment.

Once integrated into your application, the tools provided by the utility
can easily be used to display or record a variety of internal program
events. The entire debugging process is managed with a central control
panel (see Figure 1☼) accessible through the system menu of the
application. Using this control panel you can:

  ■  turn debugging ON or OFF
  ■  filter out unwanted debugging events
  ■  direct debugging output to the display and/or a log file

Debug Utility Functions

The debug module linked into your application is built from three separate
files. Using the files DEBUG (make file for debug module), DEBUG.H (header
file containing definitions and function declarations), and DEBUG.C
(source code for debug utilities), you can create the object module and
integrate it into your program. (Due to space limitations, the code for
the DEBUG utility could not be included. It must be downloaded from any
of our bulletin boards. -Ed.)

The debug module consists of four functions, three of which are referenced
explicitly within your host application.

The first of these is the DebugSetup function, which is responsible for
creating the debug viewport window and appending the Debug... control
panel menu option to the system menu of the main application window. To
call this function you must provide a window handle, a debug control panel
message, and the maximum number of lines to be maintained by the debug

The DebugSetup function assumes that there is a system menu associated
with the main application window handle that you provide. If your main
window does not contain a system menu and you wish to use some other
means to provide access to the debug control panel, you can easily do so.

Whenever the Debug... option is appended to the system menu (see
Figure 2☼), the control panel message number is associated with the option
and is returned. The actual message number is returned in the wParam
portion of the WM_SYSCOMMAND message. To display the control panel you
have to explicitly trap this message and call the DebugControl function.

The parameter specifying maximum viewport lines in DebugSetup limits the
amount of memory consumed by the utility while you view debug statements
in the viewport. The debug viewport consists of a movable listbox
containing a series of formatted debugging statements. Debugging
statements displayed in this viewport are successively appended until
the specified maximum is reached. At that point, each additional
debugging statement displaces an earlier one. Although the maximum
number of debugging statements maintained by the viewport is limited
only by memory, in most situations you should limit the number to less
than 100.

The DebugControl function lets you display the debug control panel and
change the current debugging options. This function is normally called as
part of the WM_SYSCOMMAND message handling code, which is invoked whenever
the user selects the Debug... option from the system menu. This function
calls DebugControlDlgFn using the predefined DebugControl dialog box

DebugControlDlgFn is responsible for processing all the messages related
to the debug control panel dialog box. The initialization section (under
WM_INITDIALOG) of this function translates the current global debug-
options data structure into corresponding button states and edit fields.
The command section (under WM_COMMAND) processes the various button
messages and accordingly enables or disables fields. The termination
section (under BUG_OK) retrieves the current control panel status and
translates it back into the global options data structure. At the same
time it parses (albeit in a crude manner) the filter edit field and
defines a list of accepted debug statement classification codes.

Finally, the Debug function generates formatted debugging statements and
is typically called throughout your application. The first parameter to
this function is the statement classification code, which is used by
the filter section of the Debug function. If the debug filter is active,
this classification code is checked against a list of to-be-filtered
categories. Only messages with codes on this list are displayed or
written to disk, enabling you to filter out messages you're not
interested in.

For example, you could arbitrarily assign a classification code of 1 to
all paint-related events or processes, a code of 2 to clipboard-related
ones, and a code of 3 to all others. If you wished to exclude all but
clipboard- and paint-related debugging statements, you would activate
the filter from the control panel with a range of 1,2. All statements
assigned code 3 would be excluded from the viewport or log-file listing,
yielding an abbreviated execution profile that would let you concentrate
on particular facets of your program.

The second parameter to the Debug function is a format control string
defining the physical contents of the resulting statement. This control
string is identical to that used by the standard C printf function. The
parameters following this format string are used in conjunction with
the format control string and are converted and output accordingly.
Figure 3 illustrates how the Debug function might be used.

Note how the variable number of parameters is handled by the Debug
function. Unless you increase the size of the pass-by-value structure, you
are limited to 64 bytes of parameter storage. You may wish to experiment
with alternative ways of handling the parameter list. One suggestion is
to use the C 5.0 vprintf function along with either the ANSI C or UNIX(R)
System V variants.

Integrating the Debug Utility

Before you can use the tools provided by the debug module to probe the
workings of your application, you must perform several steps.

Copy the following files to the directory where you are creating your


Define a new compiler command line variable in your application make
file. This variable can be used within a conditional compilation
pragma, thus controlling the generation of debugging code. For example,
you might define and use the compiler flags seen in Figure 4.

Modify your application's make file so that the module DEBUG.OBJ is
included whenever you build your program. See Figure 5 for an example.

Define a debug control message number in your application header file.
This message is returned by Windows whenever you select the Debug...
option from the system menu.

Export DebugControlDlgFn by defining it in your application DEF file.
Failure to do this will result in erratic behavior. For example, you might
change the export section of your DEF file from

    TestWndFn    @1

    TestWndFn    @1
    DebugControlDlgFn @2

Add the lines shown in Figure 6 to your application's resource file.

Call the DebugSetup function shortly after creating your main application
window. Be sure your main window has a system menu and that you pass a
valid handle to this function. Note that it is good practice to enclose
this function call within a conditional compilation pragma:

  #if DEBUG
    DebugSetup( hMainWnd, BUG_CONTROL, 100 );

Include the DEBUG.H file in each source code module for which you use
routines from the debug utility. The header file includes function
definitions and calling conventions necessary to the routines.

Define debugging statements throughout your program in areas of particular
interest. Try to categorize your debugging statements.

Note that debugging by default is OFF and that statements encountered
before it is explicitly activated are ignored. One example of this is the
use of debugging statements to probe the CREATEWINDOW message
processing section of your main window function.

Recompile and link your application.

Using the Debug Utility

To get the best understanding of the debug utility let's look at a sample
application like BUGTEST.EXE. Figure 7 shows how to create this
application, with each file containing references to the debug utility.

Throughout the journal listings of these files the sections which
reference the debug utility are clearly identified. You can use these
changes as a template when you incorporate the utility into your own

Pay special attention to the TestWndFn in the BUGTEST.C file (see Figure 8
for the code listings for BUGTEST). This window function contains several
explicit references to the debug utility. First you see a series of
debugging statements that output the contents of several Windows messages.
Note how the messages are classified and remember that you can filter
categories to limit the statements generated.

You should also notice the special handling of the BUG_CONTROL message in
the WM_SYSCOMMAND processing section of the function. This message is
generated whenever you access the Debug... option from the system menu.
The call to DebugControl displays the debug control panel from which you
can activate or deactivate debugging and define various output options.

To create BUGTEST.EXE you need Microsoft C 5.0, Windows 2.03, and the
Windows 2.03 Software Development Kit (SDK). If you don't have the 2.03
SDK, you can probably get everything to work correctly (with perhaps minor
modifications) using the 1.04 SDK. Although the BUGTEST.ICO file is not
included in the code listings, you can easily create your own icon with
the editor in the Windows SDK.

Once you have built the BUGTEST application, you can experiment with the
debug utility. When you first bring up the program, a standard pop-up
window appears. Using either the mouse or the keyboard you can move,
resize, and make this window into an icon just as you would any other
window. Whenever you perform one of these operations, numerous debugging
statements are generated, but they are ignored by the debug utility until
you explicitly turn debugging ON through the control panel.

To access the control panel you select the Debug... option from the system
menu. The debug control panel is displayed. From this dialog box you can
activate debugging and define options. You can check the count and display
boxes, which causes debugging statements to be enumerated and the results
displayed in a pop-up listbox (see Figure 9☼). Using this listbox you can
watch the debugging statements as they are generated by the main
application (see Figure 10☼).

Remember that the filter option allows you to select particular
categories of debugging statements. Also note that this implementation of
the debug utility is set for 32 filter categories. If these are
insufficient, you can easily change the MAX_FILTER definition in DEBUG.C
to a larger number and recompile the utility.

The log-file option works almost identically to the display option, except
that debugging statements are appended to the disk file you specify. You
can simultaneously display and log, although your system will slow
considerably. When using log files I add the following line to the
"extensions section" of my WIN.INI file:


This lets you double-click on any log file and view its contents using the
notepad provided with Windows. Be aware that notepad is not capable of
loading large log files, but this shouldn't be a major problem.


The debug utility is not about to displace the likes of Windows CodeView.
However, for simple debugging it can provide a great deal of insight into
the workings of an application and should be a useful addition to every
programmer's toolbox.

The debug utility is capable of solving a large number of Windows
problems. With the code provided here as a framework, you may extend this
utility into many interesting areas. You could use journaling hooks to
create a general purpose message tracker to explore problems between
applications. You could experiment with multiple debug viewports into a
single application. Or you could move the utility into a dynamically
linked library and debug multiple applications simultaneously. These,
and hopefully many other creative enhancements are left to you.

Figure 3:  A Sample Use of the DEBUG Utility

Debug( 1, "WM_SIZE: [%u,%u], LOWORD(lParam),HIWORD(lParam));
Debug( 2, "WM_MOVE: [%u,%u]", LOWORD(lParam),IWORD(lParam));
Debug( 3, "WM_CHAR: [%c,%u]", wParam, wParam );
Debug( 4, "WM_ACTIVATE: [%s]", (wParam)?"on":"off" );

Figure 4:  Sample Compiler Flags for Compiling the DEBUG Utility Code

STDFLAGS=-c -u -AS -FPa -Gsw -Os -Zep
BUGFLAGS=-c -u -AS -FPa -Gsw -Os -Zep -DDEBUG

bugtest.obj: bugtest.c
cl $(BUGFLAGS) bugtest.c

Figure 5:  Sample Make File to Include DEBUG within an Application

Change your make file from

     bugtest.exe: bugtest.obj bugtest.def bugtest.res
          link4 bugtest /AL:16 /NOE,,,slibw,bugtest.def


     bugtest.exe: bugtest.obj debug.obj bugtest.def bugtest.res
          link4 bugtest+debug /AL:16 /NOE,,,slibw,bugtest.def

Figure 6:  Debug Control Panel Definitions

#include "debug.h"

 CONTROL "OFF - debug &inactive." BUG_OFF,"button",STD_BUTTON,5,3,130,12
 CONTROL "ON - debug &active." BUG_ON,"button",STD_BUTTON,5,15,128,12
 CONTROL "&Count debug events." BUG_COUNT,"button",STD_CHECKBOX,8,26,114,12
 CONTROL "&Display in debug window." BUG_DISPLAY,"button",STD_CHECKBOX,18,38,
 CONTROL "&Filter" BUG_FILTER,"button",STD_CHECKBOX,18,50,34,12
 CONTROL "&Log to" BUG_LOG,"button",STD_CHECKBOX,18,62,36,12
 CONTROL "" -1,"static",SS_BLACKFRAME | WS_CHILD,142,0,1,81
 CONTROL "Ok" BUG_OK,"button",STD_BUTTON,148,4,32,14
 CONTROL "Cancel" BUG_CANCEL,"button",STD_BUTTON,148,21,32,14

Figure 7:  A list of the files required to build the sample test program that
           incorporates the DEBUG utility.

BUGTEST     application make file
BUGTEST.C   application source code
BUGTEST.RC  application resource file
BUGTEST.ICO application icon (referenced by resource file)
BUGTEST.DEF application link definition

Figure 8:

Make File for BUGTEST

STDFLAGS=-c -u -AS -FPa -Gsw -Os -Zep
BUGFLAGS=-c -u -AS -FPa -Gsw -Os -Zep -DDEBUG

bugtest.res: bugtest.rc bugtest.ico
  rc -r bugtest.rc

debug.obj: debug.h debug.c
  cl $(STDFLAGS) debug.c

bugtest.obj: bugtest.c
  cl $(BUGFLAGS) bugtest.c

bugtest.exe: bugtest.obj bugtest.def bugtest.res debug.obj
  link4 bugtest+debug /AL:16 /NOE,,,slibw,bugtest.def
  rc bugtest.res

Code Listing for BUGTEST

 * LANGUAGE    : Microsoft C 5.0
 * MODEL  : small
 * STATUS : operational
 * 12/11/87 1.00 - Kevin P. Welch - initial creation.

#include <windows.h>
#include "debug.h"

/* local definitions */
#define   BUG_CONTROL    201

/* function definitions */


 * This mainline initializes the test program and processes
 * and dispatches all messages relating to the debug test
 * window.

int PASCAL WinMain( hInstance, hPrevInstance, lpsCmd, wCmdShow )
  HANDLE  hInstance;
  HANDLE  hPrevInstance;
  LPSTR   lpsCmd;
  WORD    wCmdShow;
  /* local variables */
  MSG     Msg; /* current system message */

  /* initialization */
  if ( TestInit( hInstance, hPrevInstance, lpsCmd, wCmdShow ) ) {

     /* process system messages until finished */
     while ( GetMessage( (LPMSG)&Msg, NULL, 0, 0 ) ) {
          TranslateMessage( (LPMSG)&Msg );
          DispatchMessage( (LPMSG)&Msg );

     /* terminate application */
     exit( Msg.wParam );

  } else
     exit( FALSE );



 *   TestInit( hInstance, hPrevInstance, lpsCmd, wCmdShow ) : BOOL;
 *        hInstance      current instance handle
 *        hPrevInstance  handle to previous instance
 *        lpsCmd    command line string
 *        wCmdShow  window display flag
 * This utility function performs all the initialization required
 * for testing the debug utility.  Included in this program is
 * the registry and creation of the main window & the installation
 * of the debug utility code.

static BOOL TestInit( hInstance, hPrevInstance, lpsCmd, wCmdShow )
     HANDLE         hInstance;
     HANDLE         hPrevInstance;
     LPSTR          lpsCmd;
     WORD      wCmdShow;
     /* local variables */
     HWND      hWnd;     /* current window handle */
     BOOL      bResult;  /* result of initialization */
     WNDCLASS  WndClass; /* window class */

     /* initialization */
     bResult = FALSE;

     /* register window class */
     if ( !hPrevInstance ) {

          /* define MAZE window class */
          memset( &WndClass, 0, sizeof(WNDCLASS) );
          WndClass.lpszClassName = (LPSTR)"TestWindow";
          WndClass.hCursor = LoadCursor( NULL, IDC_ARROW );
          WndClass.lpszMenuName = (LPSTR)NULL;
          WndClass.style = CS_HREDRAW | CS_VREDRAW;
          WndClass.lpfnWndProc = TestWndFn;
          WndClass.hInstance = hInstance;
          WndClass.hIcon = LoadIcon( hInstance, "BugTestIcon" );
          WndClass.hbrBackground = (HBRUSH)(COLOR_MENU + 1);

          /* register maze window class */
          if ( RegisterClass( (LPWNDCLASS)&WndClass ) ) {

               /* create window */
               hWnd = CreateWindow(
                    "TestWindow",         /* class name */
                    "Debug Test Window",  /* caption */
                    WS_TILEDWINDOW,       /* style */
                    CW_USEDEFAULT,        /* x position */
                    CW_USEDEFAULT,        /* y position */
                    CW_USEDEFAULT,        /* width */
                    CW_USEDEFAULT,        /* height */
                    (HWND)NULL,           /* parent window */
                    (HMENU)NULL,          /* menu */
                    hInstance,            /* application */
                    (LPSTR)NULL           /* other data */

               /* continue if successful */
               if ( hWnd ) {

               /* Here is where the debug utility is
                * installed into the program. A response
                * message number is provided along with the
                * maximum number of debug statements which
                * will be maintained by the listbox. The
                * larger this number, the less global memory
                * available for your application.

               DebugSetup( hWnd, BUG_CONTROL, 100 );

               /* make window visible */
               bResult = TRUE;
               ShowWindow( hWnd, wCmdShow );




     /* return result */
     return( bResult );



 * TestWndFn( hWnd, wMessage, wParam, lParam ) : LONG FAR PASCAL
 *        hWnd      window handle
 *        wMessage       message number
 *        wParam         additional message information
 *        lParam         additional message information
 * This window function processes all the messages related to
 * the debug test window.  Using the system menu the user can
 * display the debug control panel dialog box.

LONG FAR PASCAL TestWndFn( hWnd, wMessage, wParam, lParam )
     HWND      hWnd;
     WORD      wMessage;
     WORD      wParam;
     LONG      lParam;
     /* local variables */
     LONG      lResult;  /* result of message */

     /* initialization */
     lResult = FALSE;

     /* sample debugging output */
     switch( wMessage )
     case WM_MOVE :
          Debug( 1, "WM_MOVE: [%u,%u]", HIWORD(lParam),
          LOWORD(lParam) );
     case WM_SIZE :
          Debug( 1, "WM_SIZE: [%u,%u]", HIWORD(lParam),
          LOWORD(lParam) );
     case WM_CHAR :
          Debug( 2, "WM_CHAR: [%c,%u]", wParam, wParam );
     case WM_ACTIVATE :
          Debug( 3, "WM_ACTIVATE: %s",
          (wParam)?"activate":"inactivate" );
     case WM_ACTIVATEAPP :
          Debug( 3, "WM_ACTIVATEAPP: %s",
          (wParam)?"activate":"inactivate" );
     case WM_PAINT :
          Debug( 4, "WM_PAINT:" );

     default :

     /* process each message */
     switch( wMessage )
     case WM_SYSCOMMAND : /* system command */

          /* In here you need to handle the special case where the
           * user asks for the debug control panel to be
           * displayed.
           * To do so you need to trap the control panel response
           * message you provided when installing the debug
           * utility.

          /* process sub-message */
          switch( wParam )
          case BUG_CONTROL : /* debug control panel */
               DebugControl( hWnd );
          default :
               lResult = DefWindowProc( hWnd, wMessage, wParam,
               lParam );

     case WM_DESTROY :   /* destroy window */
          PostQuitMessage( 0 );
     default : /* send to default */
          lResult = DefWindowProc( hWnd, wMessage, wParam,
          lParam );

     /* return normal result */
     return( lResult );



NAME            BUGTEST

DESCRIPTION    'Debug Test Utility'



STACKSIZE      4096

  TestWndFn    @1
  DebugControlDlgFn     @2

Resource File for BUGTEST

 * LANGUAGE    : Microsoft C 5.0
 * MODEL       : small
 * STATUS      : operational
 * 12/11/87 1.00 - Kevin P. Welch - initial creation.
#include <style.h>
#include "debug.h"

BugTestIcon              ICON    bugtest.ico

CAPTION "Debug Control Panel"
 CONTROL "OFF - debug &inactive." BUG_OFF,"button",STD_RADIO,5,3,130,12
 CONTROL "ON - debug &active." BUG_ON,"button",STD_RADIO,5,15,128,12
 CONTROL "&Count debug events." BUG_COUNT,"button",STD_CHECKBOX,8,26,114,12
 CONTROL "&Display in debug window." BUG_DISPLAY,"button",STD_CHECKBOX,18,38,
 CONTROL "&Filter" BUG_FILTER,"button",STD_CHECKBOX,18,50,34,12
 CONTROL "&Log to" BUG_LOG,"button",STD_CHECKBOX,18,62,36,12
 CONTROL "" -1,"static",SS_FRAME | WS_CHILD,142,0,1,81
 CONTROL "Ok" BUG_OK,"button",DEF_BUTTON,148,4,32,14
 CONTROL "Cancel" BUG_CANCEL,"button",STD_BUTTON,148,21,32,14


An Examination of the Operating Principles of the Microsoft Object Linker☼

Richard Wilton

MS-DOS(R) object modules can be processed in two ways: they can be grouped
together in object libraries or they can be linked into executable files.
All Microsoft language translators are distributed with two utility
programs that process object modules: the Microsoft Library Manager (LIB)
creates and modifies object libraries and the Microsoft Object Linker
(LINK) processes the individual object records within object modules to
create executable files.

The following discussion focuses on LINK because of its crucial role in
creating an executable file.

What LINK Does

The function of LINK is to translate object modules into an executable
program. LINK's input consists of one or more object files (OBJ files)
and, optionally, one or more libraries (LIB files). LINK's output is an
executable file (EXE file) containing binary data that can be loaded
directly from the file into memory and executed. LINK can also generate a
symbolic address map listing (MAP file)──a text file that describes the
organization of the EXE file and the correspondence of symbols declared
in the object modules to addresses in the executable file.

Building an Executable

LINK builds two types of information into an EXE file. First, it extracts
executable code and data from the LEDATA and LIDATA records in object
modules, arranges them in a specified order according to its rules for
segment combination and relocation, and copies the result into the EXE
file. Second, LINK builds a header for the EXE file. The header describes
the size of the executable program and also contains a table of load-time
segment relocations and initial values for certain CPU registers. See
Pass 2 in the LINK Internals section. (See MSJ, Vol. 3 No. 2,
"Exploring the Structure and Contents of the MS-DOS(R) Object Module
Format," for more information on LEDATA, LIDATA, and other object
records supported by LINK-Ed.)

Relocation and Linking

In building an executable image from object modules, LINK performs two
essential tasks: relocation and linking. As it combines and rearranges the
executable code and data it extracts from the object modules it
processes, LINK frequently adjusts, or relocates, address references to
account for the rearrangements (see Figure 1). LINK links object modules
by resolving address references among them. It does this by matching the
symbols declared in EXTDEF and PUBDEF object records (see Figure 2). LINK
uses FIXUPP records in order to determine exactly how to compute both
address relocations and linked address references.

Object Module Order

LINK processes input files from three sources: object files and libraries
specified explicitly by the user (in the command line, in response to
prompts by LINK, or in a response file) and object libraries named in
object module COMENT records.

LINK always uses all of the object modules in the object files it
processes. In contrast, it extracts individual object modules from
libraries-only those object modules needed to resolve references to public
symbols are used. This difference is implicit in the order in which LINK
reads its input files:

  ■  object files that are specified in the command line or in response to
     the Object Modules prompt

  ■  libraries that are specified in the command line or in response to
     the Libraries prompt

  ■  libraries that are specified in COMENT records

The order in which LINK processes object modules influences the
resulting executable file in three ways. First, the order in which
segments appear in LINK's input files is reflected in the segment
structure of the executable file. Second, the order in which LINK
resolves external references to public symbols depends on the order in
which it finds the public symbols in its input files. Finally, LINK
derives the default name of the executable file from the name of the first
input object file.

Segment Order

In general, LINK builds named segments into the executable file in the
order in which it first encounters the SEGDEF records that declare the
segments. (The /DOSSEG switch also affects segment order.) This means
that the order in which segments appear in the executable file can be
controlled by linking object modules in a specific order. In assembly
language programs, it is best to declare all the segments used in the
program in the first object module to be linked so that the segment order
in the executable file is under complete control.

Order in Which References Are Resolved

LINK resolves external references in the order in which it encounters the
corresponding public declarations. This fact is important as it determines
the order in which LINK extracts object modules from libraries. When a
public symbol required to resolve an external reference is declared more
than once among the object modules in the input libraries, LINK uses the
first object module that contains the public symbol. This means that the
actual executable code or data that is associated with a particular
external reference can be varied by changing the order in which LINK
processes its input libraries.

For example, imagine that a C programmer has written two versions of a
function named myfunc() that is called by the program MYPROG.C. One
version of myfunc() is for debugging; its object module is found in
MYFUNC.OBJ. The other is a production version whose object module resides
in MYLIB.LIB. Under ordinary circumstances, the programmer links the
production version of myfunc() by using MYLIB.LIB (see Figure 3). In order
to use the debugging version of myfunc(), the programmer explicitly
includes its object module (MYFUNC.OBJ) when LINK is executed. This causes
LINK to build the debugging version of myfunc() into the executable file
because it finds the debugging version in MYFUNC.OBJ before it finds the
other version in MYLIB.LIB.

To exploit the order in which LINK resolves external references, it is
important to know LINK's library search strategy: each individual library
is searched repeatedly (from first library to last, in the sequence in
which they are input to LINK) until no further external references can be

The example in Figure 4 demonstrates this search strategy. Library
LIB1.LIB contains object modules A and B, library LIB2.LIB contains object
module C, and the object file MYPROG.OBJ contains the object module MAIN;
modules MAIN, A, and C each contain an external reference to a symbol
declared in another module. When this program is linked with


LINK starts by incorporating the object module MAIN into the executable
program. It then searches the input libraries until it resolves all the
external references:

  ■  process MYPROG.OBJ, find unresolved external reference to A
  ■  search LIB1.LIB, extract A, find unresolved external reference to C
  ■  search LIB1.LIB again; reference to C remains unresolved
  ■  search LIB2.LIB, extract C, find unresolved external reference to B
  ■  search LIB2.LIB again; reference to B remains unresolved
  ■  search LIB1.LIB again, extract B
  ■  no more unresolved external references, so end library search

The order in which the modules appear in the executable file thus
reflects the order in which LINK resolves the external references; this,
in turn, depends on which modules were contained in the libraries and on
the order in which the libraries are input to LINK.

Name of the Executable

If no filename is specified in the command line or in response to the Run
File prompt, LINK derives the name of the executable file from the name
of the first object file it processes. For example, if the object files
PROG1.OBJ and PROG2.OBJ are linked with the command


the resulting executable file, PROG1.EXE, takes its name from the first
object file processed by LINK.

Order and Combinations

LINK builds segments into the executable file by applying the following
sequence of rules:

Segments appear in the executable file in the order in which their
SEGDEF declarations first appear in the input object modules.

Segments in different object modules are combined if they have the same
name and class and a public, memory, stack, or common combine type. All
address references within the combined segments are relocated relative to
the start of the combined segment.

  ■  Segments with the same name and either the public or the memory
     combine type are combined in the order in which they are processed by
     LINK. The size of the resulting segment then equals the total size
     of the combined segments.

  ■  Segments with the same name and the stack combine type are overlapped
     so that the data in each of the overlapped segments ends at the same
     address. The size of the resulting segment equals the total size of
     the combined segments. The resulting segment is always paragraph

  ■  Segments with the same name and the common combine type are
     overlapped so that the data in each of the overlapped segments starts
     at the same address. The size of the resulting segment equals the
     size of the largest of the overlapped segments.

Segments with the same class name are concatenated.

If the /DOSSEG switch is used, the segments are rearranged in conjunction
with DGROUP.

These rules allow the programmer to control the organization of
segments in the executable file by ordering SEGMENT declarations in an
assembly language source module, which produces the same order of SEGDEF
records in the corresponding object module, and by placing this object
module first in the order in which LINK processes its input files.

A typical MS-DOS program is constructed by declaring all executable code
and data segments with the public combine type, thus enabling the
programmer to compile the program's source code from separate source
code modules into separate object modules. When these object modules are
linked, LINK combines the segments from the object modules according to
the above rules to create logically unified code and data segments in the
executable file.

Segment Classes

LINK concatenates segments with the same class name after it combines
segments with the same segment name and class. For example, Figure 5 shows
the following compiling and linking:


After MYPROG1.ASM and MYPROG2.ASM have been compiled, LINK builds the
_TEXT and FAR_TEXT segments by combining segments with the same name
from the different object modules. Then, _TEXT and FAR_TEXT are
concatenated because they have the same class name ('CODE'). _TEXT is
before FAR_TEXT in the executable file because LINK encounters the
SEGDEF record for _TEXT before it finds the SEGDEF record for FAR_TEXT.

Segment Alignment

LINK aligns the starting address of each segment it processes according
to the alignment specified in each SEGDEF record. It adjusts the
alignment of each segment it encounters regardless of how that segment is
combined with other segments of the same name or class. (The one
exception is stack segments, which always start on a paragraph

Segment alignment is particularly important when public segments with
the same name and class are combined from different object modules. Note
what happens in Figure 6, where the three concatenated _DATA segments
have different alignments. To enforce the word alignment and paragraph
alignment of the _DATA segments in Module2 and Module3, LINK inserts one
or more bytes of padding between the segments.

Segment Groups

A segment group establishes a logical segment address to which all offsets
in a group of segments can refer, that is, all addresses in all segments
in the group can be expressed as offsets relative to the segment value
associated with the group (see Figure 7). Declaring segments in a group
does not affect their positions in the executable file; the segments in
a group may or may not be contiguous and can appear in any order as long
as all address references to the group fall within 64Kb of each other.

LINK reserves one group name, DGROUP, for use by Microsoft language
translators. DGROUP is used to group compiler-generated data segments and
a default stack segment.

LINK Internals

Many programmers use LINK as a "black box" program that transforms object
modules into executable files. Nevertheless, it is helpful to observe
how LINK processes object records to accomplish this task.

LINK is a two-pass linker; that is, it reads all its input object modules
twice. On Pass 1, LINK builds an address map of the segments and symbols
in the object modules. On Pass 2, it extracts the executable code and
program data from the object modules and builds a memory image-an exact
replica-of the executable file.

The reason LINK builds an image of the executable file in memory, instead
of just copying code and data from object modules into the executable
file, is that it organizes the executable file by segments and not
according to the order in which it processes object modules. The most
efficient way to concatenate, combine, and relocate the code and data is
to build a map of the executable file in memory during Pass 1 and then
fill in the map with code and data during Pass 2.

In Versions 3.52 and later, whenever the /I (/INFORMATION) switch is
specified in the command line, LINK displays status messages at the start
of each pass and as it processes each object module. If the /M (that is,
/MAP) switch is used in addition to the /I switch, LINK also displays the
total length of each segment declared in the object modules. This
information is helpful in determining how the structure of an
executable file corresponds to the contents of the object modules
processed by LINK.

Pass 1

During Pass 1, LINK processes the LNAMES, SEGDEF, GRPDEF, COMDEF, EXTDEF,
and PUBDEF records in each input object module and uses the information
in these object records to construct a symbol table and an address map of
segments and segment groups.

Symbol Table

As each object module is processed, LINK uses the symbol table to resolve
external references (declared in EXTDEF and COMDEF records) to public
symbols. If LINK processes all the object files without resolving all the
external references in the symbol table, it searches the input libraries
for public symbols that match the unresolved external references. LINK
continues to search each library until all the external references in the
symbol table are resolved.

Segments and Groups

LINK processes every SEGDEF record according to the segment name, class
name, and attributes specified in the record. LINK constructs a table of
named segments and updates it as it concatenates or combines segments.
This allows LINK to associate each public symbol in the symbol table with
an offset into the segment in which the symbol is declared.

LINK also generates default segments into which it places communal
variables declared in COMDEF records. Near communal variables are placed
in one paragraph-aligned public segment named c_common, with class name
BSS (block storage space) and group DGROUP. Far communal variables are
placed in a paragraph-aligned segment named FAR_BSS, with class name
FAR_BSS. The combine type of each far communal variable's FAR_BSS segment
is private (that is, not public, memory, common, or stack). As many
FAR_BSS segments as necessary are generated.

After all the object files have been read and all the external references
in the symbol table have been resolved, LINK has a complete map of the
addresses of all segments and symbols in the program. If a MAP file has
been requested, LINK creates the file and writes the address map to it.
Then LINK initiates Pass 2.

Pass 2

In Pass 2, LINK extracts executable code and program data from the LEDATA
and LIDATA records in the object modules. It builds the code and data into
a memory image of the executable file. During Pass 2, LINK also carries
out all the address relocations and fixups related to segment relocation,
segment grouping, and resolution of external references, as well as any
other address fixups specified explicitly in object module FIXUPP records.

If it determines during Pass 2 that not enough RAM is available to
contain the entire image, LINK creates a temporary file in the current
directory on the default disk drive. (LINK Version 3.60 and later use the
environment variable TMP to find the directory for the temporary scratch
file.) LINK then uses this file in addition to all the available RAM to
construct the image of the executable file. (In versions of MS-DOS earlier
than 3.0, the temporary file is named VM.TMP; in Version 3.0 and later,
LINK uses Interrupt 21H Function 5AH to create the file.)

LINK reads each of the input object modules in the same order as it did in
Pass 1. This time it copies the information from each object module's
LEDATA and LIDATA records into the memory image of each segment in the
proper sequence. This is when LINK expands the iterated data in each
LIDATA record it processes.

LINK processes each LEDATA and LIDATA record along with the corresponding
FIXUPP record, if one exists. LINK processes the FIXUPP record, performs
the address calculations necessary for relocation, segment grouping, and
resolving external references, and then stores binary data from the
LEDATA or LIDATA record, including the results of the address
calculations, in the proper segment in the memory image.

The only exception to this process occurs when a FIXUPP record refers to
a segment address. In this case, LINK adds the address of the fixup to a
table of segment fixups; this table is used later to generate the segment
relocation table in the EXE header.

When all the data has been extracted from the object modules and all the
fixups have been carried out, the memory image is complete. LINK now has
all the information it needs to build the EXE header (see Figure 8). At
this point, therefore, LINK creates the executable file and writes the
header and all segments into it.


By using LINK to rearrange and combine segments, a programmer can
generate an executable file in which segment order and addressing serve
specific purposes. Careful use of LINK leads to more efficient use of
memory and simpler, more efficient programs.

LINK's characteristic support for segment ordering, for run-time memory
management, and for dynamic overlays has an impact in many different
situations. Programmers who write their own language translators must
bear in mind the special conventions followed by LINK in support of
Microsoft language translators. Application programmers must be
familiar with LINK's capabilities when they use assembly language or
link assembly language programs with object modules generated by
Microsoft compilers. LINK is a powerful program development tool and
understanding its special capabilities can lead to more efficient

Figure 1:  A simple relocation. Both object modules contain code that LINK
           combines into one logical segment. In this example, LINK appends
           the 50H bytes of code in Module2 to the 64H bytes of code in
           Module1. LINK relocates all references to addresses in the code
           segment so that they apply to the combined segment.

         ╔════════════════════╗          ╔════════════════════╗
         ║    Code segment    ║          ║                    ║
         ║     (64H bytes)    ║          ║    Code segment    ║
         ║                    ║          ║     (50H bytes)    ║
         ║Label1 at offset 10H║          ║                    ║
         ╚════════════════════╝          ╚════════════════════╝
               Module 1                         Module 2
                         ║    Code segment      ║
                         ║    (B4H bytes)       ║
                         ║                      ║
                         ║ Label1 at offset 10H ║
                         ║ Label2 at offset 74H ║
                         ║                      ║
                          Combined code segment

Figure 2:  Resolving an external reference. LINK resolves the external
           reference in Module1 (declared in an EXTDEF record) with the
           address of Label2 in Module2 (declared in a PUBDEF record).

         Module 1                          Module 2
       ╔══════════════════════════╗     ╔══════════════════════════╗
       ║ Code segment             ║     ║ Code segment             ║
       ║    EXTDEF Label2         ║     ║    PUBDEF Label2         ║
       ║       jmp Label2         ║     ║                          ║
       ║            ∙             ║     ║    Label2: ∙             ║
       ║            ∙             ║     ║            ∙             ║
       ║            ∙             ║     ║            ∙             ║
       ╚══════════════════════════╝     ╚══════════════════════════╝
                        Combined code segment
                      ║ Code segment            ║
                      ║          ∙              ║
                      ║          ∙              ║
                      ║          ∙              ║
                      ║                         ║
                      ║   jmp Label2            ║
                      ║          ∙              ║
                      ║          ∙              ║
                      ║          ∙              ║
                      ║                         ║
                      ║ Label2:  ∙              ║
                      ║          ∙              ║
                      ║          ∙              ║

Figure 3:  Ordered object module processing by LINK. (a) With the command
           LINK MYPROG,,,MYLIB, the production version of myfunc() in
           MYLIB.LIB is used. (b) With the command LINK MYPROG+MYFUNC,,,
           MYLIB, the debugging bersion of myfunc() in MYFUNC.OBJ is used.

  ┌────────────────┐     ┌─────────────────────┐
  │ Main ()        │     │                     │
  │ {              ├────►│ EXTDEF for myfunc() ├──┐   ┌───────────────┐
  │  x=myfunc(y) ; │     │                     │  │   │  Executable   │
  │ }              │     └─────────────────────┘  ├──►│ code contains │
  └────────────────┘           MYPROG.OBJ         │   │   myfunc()    │
      MYPROG.C                                    │   │ derived from  │
                                                  │   │    either     │
  ┌────────────────┐     ┌─────────────────────┐  │   │ MUFUNC.OBJ or │
  │ myfunc(a)      │     │                     │  │   │   MYLIB.OBJ   │
  │ int a;         ├────►│ PUBDEF for myfunc() ├──┤   └───────────────┘
  │ {              │     │                     │  │
  │     ∙          │     └─────────────────────┘  │
  │     ∙          │           MYFUNC.OBJ         │
  │     ∙          │                              │
  │ }              │     ┌─────────────────────┐  │
  └────────────────┘     │             ∙       │  │
      MYFUNC.C           │             ∙       │  │
                         │             ∙       │  │
                         │ PUBDEF for myfunc() ├──┘
                         │             ∙       │
                         │             ∙       │
                         │             ∙       │

Figure 4:  Library search order. Modules are incorporated into the
           executable file as LINK extracts them from the libraries to
           resolve external references.

                                                      Start of program─┐
┌─────────────┐  ┌─────────────┐  ┌────────────────┐  ┌──────────────┐┐│
│   ModuleA   │  │   ModuleC   │  │   ModuleMAIN   │  │  ModuleMAIN  │├┘
│   Call C    │  │   Call B    │  │   Call C       │  ├──────────────┤┘
├─────────────┤  └─────────────┘  └────────────────┘  │  ModuleA     │
│   ModuleB   │     LIB2.LIB          MYPROG.OBJ      ├──────────────┤
│             │                                       │  ModuleC     │
└─────────────┘                                       ├──────────────┤
   LIB1.LIB                                           │  ModuleB     │

Figure 5:  Segment order and concatenation by LINK. The start of each file,
           corresponding to the lowest address, is at the top.

     ┌──────────────────────────────┐   MYPROG1.OBJ
     │ _TEXT SEGMENT public 'CODE'  │  ┌───────────────────┐
     ├──────────────────────────────┤  │SEGDEF for_TEXT    │
     │FAR_TEXT SEGMENT public 'CODE'├─►│SEGDEF for FAR_TEXT├───┐
     ├──────────────────────────────┤  │SEFDEF for_DATA    │   │
     │ _DATA SEGMENT public 'DATA'  │  └───────────────────┘   │
     └──────────────────────────────┘                          │
      MYPROG2.ASM                                              │
     ┌──────────────────────────────┐   MYPROG2.OBJ            │
     │ _TEXT SEGMENT public 'CODE'  │  ┌───────────────────┐   │
     ├──────────────────────────────┼─►│SEGDEF for_TEXT    │   │
     │FAR_TEXT SEGMENT public 'CODE'│  │SEGDEF for FAR_TEXT├───┤
     └──────────────────────────────┘  └───────────────────┘   │
         │    MYPROG1.EXE
         │   ┌────────────┐─┐           ─┐
         │   │            │ ├─ _TEXT     │
         │   │            │ │  segment   │
         └──►│            │ │            │
             ├────────────┤═╡            ├─ 'CODE' class
             │            │ │            │
             │            │ ├─ FAR_TEXT  │
             │            │ │  segment   │
             │            │ │            │
             ├────────────┤═╡           ─┘
             │            │ ├─ _DATA
             └────────────┘─┘  segment

Figure 6:  Alignment of combined segments. LINK enforces segment alignment by
           padding combined segments with unitialized data bytes.

         Module 1              Module 2                Module 3
 ╔═══════════════════╗  ╔════════════════════╗  ╔════════════════════╗
 ║ DATA SEGMENT byte ║  ║ DATA SEGMENTS word ║  ║ DATA SEGMENTS para ║
 ║ public 35H bytes  ║  ║ public 35H bytes   ║  ║ public 35H bytes   ║
 ╚═══════════════════╝  ╚════════════════════╝  ╚════════════════════╝

       │         Module 1            │ ├─35H bytes (byte aligned)
       │         Module 2            │ ├─35H bytes (word aligned)
       │         Module 3            │ ├─35H bytes (paragraph aligned)
            Resulting _DATA segment
               in .EXE file

Figure 7:  Example of group addressing. The first MOV loads the value 00H
           into AX (the offset of TestData relative to DataSeg2); the second
           MOV loads the value 100H into AX (the offset of TestData relative
           to the group DataGroup).

DataGroup GROUP     DataSeg1,DataSeg2
CodeSeg   SEGMENT   byte public 'CODE'
          ASSUME    cs:CodeSeg

          mov       ax,offset DataSeg2:TestData
          mov       ax,offset DataGroup:TestData

CodeSeg   ENDS

DataSeg1  SEGMENT   para public 'DATA'
          DB        100h dup(?)
DataSeg1  ENDS

DataSeg2  SEGMENT   para public 'DATA'
TestData  DB        ?
DataSeg2  ENDS

Figure 8:  How LINK Builds an EXE File Header

Offset  Contents                           Comments

00H     'MZ'                               EXE file signature

02H     Length of executable              ┐
        image MOD 512                     │
                                          │  Total size of all segments
04H     Length of executable image in     │  plus EXE file header
        512-byte pages, including last    │
        partial page (if any)             ┘

06H     Number of run-time                 Number of segment fixups
        segment relocations

08H     Size of the EXE header in          Size of segment relocation table
Offset  Contents                           Comments
08H     Size of the EXE header in          Size of segment relocation table
        16-byte paragraphs

0AH     MINALLOC: Minimum amount           Size of uninitialized data and/or
        of RAM to be allocated above       stack segments at end of program
        end of the loaded program (in      (0 if /HI switch is used)
        16-byte paragraphs)

0CH     MAXALLOC: Maximum amount           0 if /HI switch is used; value
        of RAM to be allocated above       specified with /CP switch;
        end of the loaded program (in      FFFFH if /CP and /HI switches
        16-byte paragraphs)                are not used

0EH     Stack segment (initial value for   Address of stack segment relative
        SS register); relocated by         to start of executable image
        MS-DOS when program is loaded

10H     Stack pointer (initial             Size of stack segment in bytes
        value for register SP)

Offset  Contents                           Comments

12H     Checksum                           One's complement of sum of all
                                           words in file, excluding
                                           checksum itself

14H     Entry point offset (initial       ┐
        value for register IP)            │
                                          │  MODEND object record that
16H     Entry point segment (initial      │  specifies program start address
        value for register CS);           │
        relocated by MS-DOS when          │
        program is loaded                 ┘

18H     Offset of start of segment
        relocation table relative
        to start of EXE header

1AH     Overlay number                     0 for resident segments;
                                           >0 for overlay segments

Offset  Contents                           Comments

1CH     Reserved


Vol. 3 No. 4 Table of Contents

DARWIN: Merrill Lynch Develops a New Workstation Based on Windows 2.0

In order to easily access data from numerous incompatible databases and
provide a graphical interface with report writing capabilities, Merrill
Lynch turned to the Windows development environment and created DARWIN
(Data Access and Reporting Through Windows).

CodeView(R) for Windows Provides an Interactive Debugging Environment

An interactive debugger allows immediate interaction with an executing
program. CodeView for Windows (CVW) is a special version of the Microsoft
debugger that understands such Windows specific concepts as messages, the
Windows Memory Manager and dynamic-link libraries.

OS/2 Graphics Programming Interface: An Introduction to Coordinate Spaces

Coordinate spaces, which define drawing areas, are an important component of
the OS/2 Presentation Manager. Transforms allow the mapping of objects from
one coordinate space to the next. Coordinate spaces under the micro-PS and
some of the GPI functions for handling transforms are explored.

Microsoft(R) Macro Assembler Version 5.1 Simplifies Macros and Interfacing

Macro Assembler 5.1 provides significant new features that enable the
programmer to easily develop macros and interface to high-level languages.
Other new features include the addition of high-level constructs such as
ELSE extensions, local labels, and type directives.

Color Mixing Principles and How Color Works in the Raster Video Model

The underlying principles of color generation are complex. Exploring some
common color mixing models such as the RGB (Red-Green-Blue) and HSV
(Hue-Saturation-Value) models leads to a better understanding of the methods
employed for creating colors on PC video displays.

Creating User-Defined Controls for Your Own Windows Applications

The ability to define your own controls functionally extends the Windows
user interface, giving your applications greater visual appeal and the user
greater program control. Palette, a color mixing program, utilizes several
such controls, as provided in a program called Spectrum.

SQL Server Brings Distributed DBMS Technology to OS/2 Via LAN Manager

SQL Server, in combination with the OS/2 LAN Manager, brings mainframe
and minicomputer power to networked personal computers. It adds stored
procedures, triggers, and an advanced transaction-oriented kernel,
essential features for assuring data integrity, to SQL.

Ask Dr. Bob


What does OS/2 mean for the professional programmer? For one thing, it means
new ways of thinking about how programs are developed. By itself,
multitasking is a boon to programmers; it's a real time-saver to have an
editor, compiler, and debugger all running concurrently.

Debugging under OS/2 with the Presentation Manager (PM) offers some new
alternatives that just weren't feasible under DOS. In the last issue we
presented a debugging utility for Windows applications consisting of about
600 lines of code. Building an equivalent debugging tool under Presentation
Manager is a trivial task.

Let's assume that you have at your disposal a UNIX(R)-like grep function
that will locate whatever strings you might be looking for. Within your PM
application, whenever you want to generate debugging information you simply
include a printf statement, for example:

  printf('17 BeginPaint returned 0x%04x\n', usResult);

You would then start your PM application from a windowed instance of

  start myPMapp | grep '.*'

which would bring up the PM application in a window, while the printf output
ends up being piped to the instance of grep running in the CMD.EXE window.
With grep, you could, for example, filter out everything but type 17
messages simply by starting the PM application as follows:

  start myPMapp | grep '^17'

The appropriate messages would then be filtered out. Of course, you would
need a much more elaborate OS/2 VIO-based filter for significantly greater

The multitasking/windowing facilities available under OS/2 and Presentation
Manager present programmers with a far richer set of options, in many cases
making formerly complex tasks much simpler. The successful OS/2 programmer
will be the one with the vision to put these new advantages to work.──Ed.


Editor and Publisher


Technical Editor

Assistant Editor

Production Editor

Staff Editor

Editorial Assistant


Art Director

Associate Art Director


Circulation Manager

Assistant to the Publisher

Administrative Assistant

Copyright(C) 1988 Microsoft Corporation. All rights reserved; reproduction
in part or in whole without permission is prohibited.

Microsoft Systems Journal is a publication of Microsoft Corporation, 16011
NE 36th Way, Box 97017, Redmond, WA 98073-9717. Officers: William H.
Gates, III, Chairman of the Board and Chief Executive Officer; Jon Shirley,
President and Chief Operating Officer; Francis J. Gaudette, Treasurer;
William Neukom, Secretary.

Microsoft Corporation assumes no liability for any damages resulting from
the use of the information contained herein.

Microsoft, MS, and CodeView are registered trademarks of Microsoft
Corporation. CompuServe is a registered trademark of CompuServe, Inc. dBASE,
dBASE III, and dBASE III PLUS are registered trademarks of Ashton-Tate
Corporation. EtherNet is a registered trademark of Xerox Corporation. IBM is
a registered trademark of International Business Machines Corporation.
Token-Ring is a trademark of International Business Machines Corporation.
Intel is a registered trademark of Intel Corporation. Above is a trademark
of Intel Corporation. AST and RAMpage! are registered trademarks of AST
Research, Inc. SQLBase is a registered trademark of Gupta Technologies, Inc.
UNIX is a registered trademark of American Telephone and Telegraph


DARWIN: Merrill Lynch Develops a New Workstation Based on Windows 2.03

Tony Rizzo and Karen Strauss

What do you do when you need information from a wide variety of
databases, each with its own access method, and you want to ensure vendor
independence in workstation hardware and applications software, but you
also want to provide a common, graphics-oriented user interface with
report-writing capabilities? You turn to the Windows development
environment and write a snappy application that uses all of the tools
that Windows puts at your disposal.

Corporate MIS, a relatively new division of Merrill Lynch & Co., Inc., is
responsible for providing headquarters management with financial control
and human resources information. In the past, this information has resided
in many separate database systems, each with its own access method,
usually incompatible with the others. Because of the difficulties in
combining them, about a year ago Corporate MIS began considering new
ways of accessing data through personal computers.

A key issue for Corporate MIS was the development of a strategy to allow
different programming teams within the division to work independently
of each other, yet adhere to common design specifications. For example,
the database design group would work separately from the information
presentation group, but each would develop applications to be
compatible and consistent across all groups.

Corporate MIS came up with a plan to base all future organization of its
data on the relational model and use Structured Query Language (SQL) as
the means of accessing the data. Its database designers would be free to
select the relational technology best suited to any given task. The
database engine could be based on a LAN, a mainframe, or a database
machine──the only requirement would be that the technology had to be able
to execute "standard" SQL queries.

Workstation designers directly concerned with how data is presented on a
PC to the end user would only have to deal with generating standard SQL
requests for data and handling data returned to the workstation from the
database engine.

On the user side, the Corporate MIS plan required that workstation
designers use a common interactive graphical interface. With a PC-based
graphical interface a designer no longer had to be concerned with
building applications that rely, for example, on asynch or 3270-emulation
windows. Also, Corporate MIS wanted to provide users with a common
report writer facility to simplify formatting data.

This plan envisioned a common development tool that any workstation
designer could incorporate into an application whenever that application
had to access and format data. It was at this point that Corporate MIS
turned to its PC Software Development group. "Our charter was to design
a method for querying SQL databases using icons or pictures of what the
user wanted to do," says Anthony Pizi, an assistant vice president within
Corporate MIS responsible for PC software development.

Pizi decided to use the Microsoft(R) Windows Version 2.03 environment as
his tool's common graphical user interface; after all, Microsoft Excel,
with its integrated, Windows-based interface, had been an instant success
with Merrill Lynch analysts. "This was our first major development effort
using Windows, so the project took on a subtheme demonstrating that
Windows really could deliver the graphical interface and software
integration that we needed," says Pizi. "And so far, so good."

As the software begins to take shape, according to Pizi, Merrill Lynch is
simultaneously reorganizing its PC hardware, under which most PCs will
eventually be connected to various LAN systems, such as EtherNet(R) and

Defining the System

Pizi had to implement and integrate three specific functions into his

First, a front-end system of menu options, dialog boxes, and pop-ups would
let a user formulate complex requests by simply pointing and clicking on
various fields and commands with a mouse. Second, the application had to
be able to take requests formulated by the user, translate them into SQL
requests, send them to the appropriate database subsystem, and handle
the returned data. Finally, the application required the ability to
format data easily in any manner.

This set of tools would not be tied to any specific database. The original
model might, for example, be based on Gupta Technologies' SQLBase(R), but
any engine could later be integrated into the system.

Furthermore, the entire application would be written to be NetBIOS-
compatible, permitting it to run on any workstation attached to any LAN
supporting NetBIOS. The workstation/LAN environment would be hooked into a
SQL Network Gateway, providing links to such mainframe databases as
Merrill Lynch's IBM(R) DB2 system.

For end users the resulting applications and workstations would provide:

   ■  consistent hardware configurations
   ■  a consistent user interface
   ■  remote access to many corporate databases
   ■  a simple means of generating SQL queries
   ■  substantial report writer capabilities

These new systems would improve Corporate MIS's ability to leverage
existing systems and human resources, offering:

   ■  reduced costs
   ■  increased productivity
   ■  improved information flow
   ■  enhanced and more informed decision making
   ■  better control of the PC environment

Building DARWIN

Pizi's group started to experiment with the Windows development
environment and to develop the strategies for communicating with
remote back-end database systems. Once the group had demonstrated their
ability to talk with a database through various front-end models they put
together under Windows, Pizi felt they were ready to begin.

The design for Data Access and Reporting Through Windows (DARWIN) was
turned over to Steve Canny and Brian Hayden, two senior
programmer/analysts. "On December 15 of last year the first line of
DARWIN code was written," says Pizi. Five months later DARWIN was
virtually complete.

As Canny tells it, "In the beginning, building DARWIN was a classic
example of design on the fly. Neither Brian [Hayden] nor I were Windows
masters. We would see some portion of what needed to be done and go off
and see if we could get Windows to do it. It was kind of like a jigsaw
puzzle, where you find pieces one at a time. Occasionally two or three
pieces would fall into place together.

"One of us would come up with the basics of an idea, we'd discuss it, then
implement it in the next version of the software. Since I had the most
experience with graphics programming, I developed the user interface.
Brian knew the database end of things and designed the link to the
database server. Brian also developed the code for all of the DARWIN
dialog boxes that present data to the user. Eventually, we were able to
get the entire thing to compile, and the beta version of DARWIN was born."

DARWIN and Microsoft Excel

Originally, DARWIN was to be responsible for displaying and printing the
query results. "We had managed to put together a low-level stub for
handling the display and were sweating about the thought of writing a
display and print manager that had reasonable features," Canny says.

"Fortunately, it was just about then that Tony [Pizi] came in shaking his
hands over his head and talking fast, the way he usually does when he's
had a particularly good brainstorming session. He told us about his idea
to send DARWIN's output directly to Microsoft Excel, and we told him he
was crazy. Two days later we began to think that maybe it wasn't so crazy.
Then we became absolutely convinced that sending the output directly to
Microsoft Excel was the way to go," says Canny.

A user goes into DARWIN, builds a query, and the data goes directly into a
Microsoft Excel spreadsheet. This idea resulted in a substantial
breakthrough, and the third tool for which DARWIN was responsible began to
fall into place. The design changed so that Microsoft Excel became, in a
sense, a part of DARWIN; the spreadsheet would serve as DARWIN's report
writer (see Figure 1) and would, in fact, be responsible for bringing up
DARWIN itself.


To bring up DARWIN, the user executes a Microsoft Excel macro, which
supplies DARWIN with its menu bar and hides Microsoft Excel. Once DARWIN
is running, the macro controls whether the user is in DARWIN itself or in
Microsoft Excel by monitoring the appropriate menu option provided in
the two applications. The user doesn't even have to know how to start
Microsoft Excel or run a macro if a program such as the hDC Computer
Corporation ClickStart applications organizer is used──simply clicking on
an icon will start DARWIN.

DARWIN establishes the underlying connections to the required database.
The database can be a server on a network, or it can be a mainframe
database that is linked to via a SQL Network Gateway (see Figure 2).
Gupta's SQLBase supplies the necessary software to make this mainframe
link look like just another local network-based server to DARWIN.

DARWIN offers the user database choices. Tables of data are grouped
together as categories; each category consists of preestablished
collections and groupings of data tables. The user first selects the
desired database category. Once a selection is made, DARWIN provides a
list of the fields in each table and menus of tools for extracting the
required data (see Figures 3, 4, and 5☼).

DARWIN's client area, which is an iconic representation of the query
being assembled, is divided into two child windows. The top window
containing the short horizontal scroll bar is the Choice List window; the
other is the Database Context window.

The Choice List area corresponds to the SQL SELECT clause; it displays
the columns that will make up the result table, that is, the information
for which the database is being queried. The Database Context window
corresponds to the FROM and WHERE clauses; it shows the tables from which
the selected columns come and the constraints and joins that are detailed
in the WHERE clause. [For more information on SQL see "SQL: A Short
Primer" in this issue──Ed.]

To the user of the DARWIN query tool, the tables of data are represented
on the screen as boxes, and they consist of one or more columns represented
on the screen as rows, not to be confused with rows of data. These boxes are
simple child windows (windows without scroll bars, menu bars, and so on)
that can be collapsed by clicking on the minimize arrow and dragged around
the client area with a mouse.

Creating Joins

Joins are created by clicking the left mouse button on the first data
item, then clicking the right mouse button on a second data item in
another table. The left-right button combinations are analogous to setting
up a "from (the left button) ... to (the right button)" relationship.
After the right button is clicked DARWIN draws a line from one table to
another (see Figure 6☼). The join that is formed by default is an equi-
join, the most commonly used join.

The type of join created is easily modified by the user (see Figure 7☼).
Join constraints are established by clicking on an appropriate column and
using the dialog box that is then presented. Several constraint options,
such as the mathematical symbols for greater than, less than, less than
or equal to, and so on, are offered. The user establishes data constraints
through a similar process (see Figure 8☼).

The user is thus able to formulate multitable queries based on joins
made only through mouse manipulations and menu choices presented by
DARWIN. With a visual summary of database tables displayed on screen,
users can easily discover relationships between collections of data that
they didn't realize they had access to. The user is provided with all the
information and tools necessary to build a detailed query, based on
standard Windows objects: windows, listboxes, and pull-down/pop-up
menus. Figure 9☼ shows the resulting graphical representation of a join
with a data constraint.

DARWIN takes the user-supplied information, which is stored in a single
dynamic local data structure, and creates a standard SQL query. Once the
actual SQL statements are created, the user clicks on the RUN! command,
and DARWIN does the rest, going out to the necessary tables, collecting
the data, and writing it out to a CE_BIFF file, which is the Microsoft
Excel file format.

When DARWIN indicates that a query has been completed and a result table
thus created, the user simply toggles back to Microsoft Excel by clicking
on the View Output option under the Window pull-down menu (see Figure 10☼)
and finds the data, supplied through the CE_BIFF file that has just been
created, waiting in a standard Microsoft Excel spreadsheet (see Figure
11☼). According to Pizi, because Microsoft Excel was designed with the
capacity to handle database information, the spreadsheet already had some
of the most important tools for becoming a report writer.

Data can be modified and formatted and statistics generated by using all
of Microsoft Excel's spreadsheet, formatting, and charting features. The
user can even click on the A1 cell, and Microsoft Excel will return
information related to the underlying SQL request made in DARWIN (see
Figure 12☼).

The user can also view any actual SQL query statement that is in the
process of being built, and experienced users are even able to modify the
query through a feature called SQL EDIT (see Figures 13 and 14☼). At this
point, according to Pizi, more complex nested queries are not yet
supported, but a knowledgeable user can build more complex requests
through SQL EDIT.

Tight Integration

Microsoft Excel and DARWIN are very tightly integrated. "We like people
to ask, 'Are you in DARWIN or Microsoft Excel?'" Hayden says. "It is so
seamless, it is hard to tell where one ends and the other begins [see
Figure 15]. Basically, when the table screens are up, you are in DARWIN.
When you have the data you see in the report, you are in Microsoft Excel."

Hayden credits Microsoft Excel for this now-you-see-it, now-you-don't
quality. "Microsoft Excel has an incredibly rich program language and allows
you to use Windows function calls via macros that make Windows environment
calls. The macro we've developed in Microsoft Excel maximizes an
application, in our case DARWIN, and adds DARWIN's menu bar," he says.

"The command is 'minimize Excel, maximize DARWIN,' so DARWIN comes up on
the screen. Microsoft Excel is the landing pad for the data DARWIN
provides," says Hayden. When the user wants to view the data in Microsoft
Excel, the process is reversed. Figure 16 shows an early version of the
macro; a close inspection of the macro demonstrates how simple it is to go
from the spreadsheet to DARWIN and back.

Powerful Environment

The power resulting from the integration of DARWIN and Microsoft Excel was
not immediately obvious to Canny and Hayden. Canny says, "When we first
explored Microsoft Excel as a possibility, it took a while to assimilate
the technology. Over time, as we better understood the environment we
realized what a really great opportunity there was for developing a unique

For example, the format in which data returned by DARWIN would be
represented required much thought, and various scenarios were drawn up.
Canny says, "At first, there was a good deal of experimentation. We didn't
have all the specs on Microsoft Excel and were waiting around for that.
The CE_BIFF format was not our first choice; we arrived at it after trying
a number of different alternatives. Our first attempt was to have DARWIN
write out a simple comma-delimited ASCII text file.

"But then the Microsoft Excel development people clued us in to the
CE_BIFF format. The availability of different fonts and the ability to set
results in a single cell, as well as the accuracy of the CE_BIFF format,
made it immediately apparent that CE_BIFF was the way to go. It was
straightforward and well documented by Microsoft. With two or three days
of research and two or three days of coding, we had it working."

Canny now considers Microsoft Excel to be a cooperative environment
because it can be used as a Windows programming tool or an adjunct. "You
write programs for things that Microsoft Excel does not specialize in, and
you send to Microsoft Excel the jobs it does specialize in," he says.
"People want calculations, editing, and graphs to make it nice for the
boss. Microsoft Excel already does this very well, and we didn't see
reinventing those tasks in DARWIN."

For all its strengths, the Microsoft Excel environment has its
frustrations as well, Hayden says. "Microsoft Excel appears to break
Microsoft's own recommended standard that you never hog memory, that the
application give it up as soon as it finished. Microsoft Excel goes out
and asks, 'How much memory is here? Give me all of it.' It is breaking
the friendliness standards."

In contrast to Microsoft Excel, which uses all the available memory,
DARWIN uses about 175Kb of memory. It takes up the most memory (just over
300Kb) when you are using the constraint dialog box, the pull-down menu
that lets users set parameters on their queries.

Windows Challenges

Hayden says he felt challenged by the need to reconcile himself to the
device-independent nature of the Windows environment. Canny concurs:
"Windows is a very different programming environment from the old
sequential model, in which program control flow goes to the first function,
the second function, the third function, goes back to the second function,
and finishes. With Windows, you have a series of independent, often
asynchronous processes, triggered by another process. That is a very new
concept for most people who work in the field." Hayden adds, "You've got to
follow the modular specs. If you start playing tricks, you're dead."

Hayden feels that Windows is an important step forward in application
development. "Windows is like C at the very beginning: a beautiful
language, but with no libraries. That means initially developing an
application is very difficult. You have to rebuild repetitive routines,
although people are starting to release some Windows libraries to make
things easier. But once you understand it, Windows is really good. It is a
liberating mindset."


The design team appreciates SQL because of the rigorous mathematical
underpinning established by E.F. Codd in his original paper on relational
databases ("A Relational Model of Data for Shared Data Banks,"
Communications of the ACM 13, No. 6, June 1970). According to Pizi, "This
mathematical basis is our assurance that a database designed with the
techniques of normalization will serve the emerging and changing needs
of the organization in the future. It gives us a firm foundation on which
to build."

This appreciation of SQL is reflected in some of the design considerations
that went into DARWIN. As Pizi points out, he wanted to remain true to what
SQL actually is, namely a rigorously defined query language. Therefore, when
DARWIN generates a SQL query, it creates a pure set of SQL statements that
have no other purpose than to go out and extract data.

DARWIN itself has no ability to format data or set up column headings; a
number of the current implementations of SQL include user shells to
handle such things. Pizi feels that this is a report writer issue, the
execution of which should be handled locally at the user's "personal"
machine. According to Pizi, "An IBM 3090, for example, should not be
spending any of its time formatting data and worrying about where to put
it; you shouldn't use the mainframe to manipulate the cursor." This
reflects the design team's view of what distributed processing is all

This approach isolates DARWIN from the data it is working with, which
could come from any database. According to Hayden, "DARWIN itself doesn't
know anything about the database." This isolation allows Corporate MIS's
database designers to use any database technology they want.


DARWIN speaks to a SQL Applications Programming Interface (SQL API) that
Hayden wrote. The interface translates DARWIN's query output into whatever
vendor-specific APIs may be out there. From the workstation designer's
perspective, DARWIN will become a tool that offers the designer a virtual
SQL interface to all of Merrill Lynch's relational database subsystems.

According to Pizi, eventually "Windows' Dynamic Data Exchange [DDE]
facility will be the pipeline to these APIs." Says Hayden, "DDE fits in
where DARWIN talks to another application that is doing the database
querying, in this case the SQL API. In the future, when we go to different
databases simultaneously, DARWIN as a program does not have to be
re-released." The SQL API will simply be modified as necessary for each
different database. It sits on one end of DARWIN, and DDE will handle the
interface between the two.

"Since DARWIN is growing into a large and complex system with more than
10,000 lines of code, the question of managing complexity has to be
raised," says Canny. According to Pizi, the current design falls into
several categories: 48 percent of the code is devoted to Windows
overhead, 32 percent makes up the SQL API, 18 percent is dedicated to
graphics, and 2 percent of the code deals with creating CE_BIFF files.

The one piece of advice that Canny has for other Windows developers is to
"make sure that all Windows code is built in a modular fashion. With
DARWIN, we need to be able to make additions to it without upsetting what
already exists. The program needs to be organized into separate modules,
with simple input and simple output that is well defined."

The SQL API is an example of this design goal. "Our SQL API isn't really
part of Windows," said Hayden. "It is more like a device driver. It breaks
DARWIN off from the database." Based on the SQL library of Gupta's LAN-
based SQLBase product, the core of the SQL API is founded on seven "shell"
functions, which are shown in Figure 17.

According to Pizi, this ability to insulate DARWIN from the particular SQL
engine is almost as important as DARWIN's point-and-click queries. It
relieves the database administrator of the need to support multiple
proprietary report writers, which are typically tied to specific database
engines. Most importantly, everything conforms to only one standard.

The DARWIN-Microsoft Excel combination anticipates in some ways the
integration promised by the MS(R) OS/2 SQL Server and the IBM OS/2 Extended
Edition. The Merrill Lynch developers say they will enhance the SQL API,
which now interfaces to Gupta Technologies' SQLBase engine, to support
SQL Server and the IBM product as they become available. They expect most
leading database vendors to respond to user demand for a standardized
interface between SQL engines and front-end user interfaces.

Virtual Data Dictionary

In order to communicate with any given database, DARWIN must understand
that system's data dictionary. DARWIN also needs certain information that
existing data dictionaries do not provide, such as printname, defined
column widths, and standard data types. "Our virtual dictionary is a
group of seven tables that supplement all the other data dictionaries. All
DARWIN requires is that these seven tables be present in those data
dictionaries," says Canny. If the tables are present in a given data
dictionary, DARWIN can communicate with them (see Figure 18 for an
example of one of these tables).

What DARWIN Really Is

As Pizi puts it, DARWIN is another tool that the application programmer
will add to his or her collection of development tools. It simplifies the
designer's work because the interface to the data and the user are already
in place. It frees the designer to spend more time dealing with what the
end user of an application actually needs and provides a standard means
for creating a design.

But what about end users? "It's not completely clear whether DARWIN is a
tool for end users or application developers. It falls somewhere in
between," Pizi says. He does not expect DARWIN to be released as a
standalone product, though he knows that plenty of people will ask for it.
However, Pizi does think that users will eventually push the product
further ahead with their own Microsoft Excel macros. "There are rich
possibilities here that we intend to explore," he says. Even Microsoft
Excel's developers were excited when they saw a demonstration of DARWIN
working with the spreadsheet, and now they closely follow the Merrill
Lynch product's development, according to Pizi. "We're taking a tool
that they produced and actually using it the way they intended."

Canny and Hayden say DARWIN is just a first step toward Merrill Lynch's
goal of vendor independence in both workstation hardware and application
software by moving the company's development platform to a common,
stable, graphics-oriented user interface: Windows 2.03 and Windows/386
today, OS/2 Presentation Manager in the future. In fact, Pizi and his
team eagerly await the official release of the Presentation Manager. They
look forward to its rich set of graphical tools and especially to the
freedom from the DOS memory limitations that OS/2 will offer.

Despite the somewhat steep learning curve, DARWIN is now falling into
place, and future development looks promising. As Canny says, "We are now
in the process of sizing up what we have, cleaning it up, and applying a
more structured approach. We don't move forward as quickly, but we no
longer step backward either. Also, this approach allows us to be more
predictable with our releases. This is very important now that we've
gotten management's attention; they ask us for promises and expect us to
deliver on them. We've made the transition from a 'no questions asked'
R&D group to a more traditional and responsible systems development
group." Concludes Pizi, "We're producing real tools. This isn't a toy
any longer."

Figure 1:  DARWIN acts as an intermediary, or "front end" between Merrill
           Lynch's SQL databases and the analysis and reporting applications
           of its users.

┌───────────────┐                    ┌───────────────────────────────────┐
│▓▓▓▓▓▒▒▒▒▒░░░░░│█                   │─██────────────Darwin───────────↑↓─│█
│▓▓▓ Merrill  ░░│█◄─────Queries──────┤┌────────────────────────────────┐│█
│▓▓▓  Lynch   ░░│█                   ││ DARWIN                         │▒│█
│▓▓▓ Database ░░│█                   ││                                │▒│█
│▓▓▓ Universe ░░│█                   ││    Data Acces and Reporting    │▒│█
│▓▓▓▓▓▒▒▒▒▒░░░░░│█──Requested Data──►││        Through Windows         │▒│█
└───────────────┘█                   ││                                │▒│█
 ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀                   │└────────────────────────────────┘▼│█
                                     │ ◄≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡► ╝│█
┌───────────────────────────────────┐ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀│▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
├─██───────Microsoft Excel───────↑↓─│█              Formatted
│┌──┬─────┬─────┬─────┬─────┬─────┐│█               Output
│├──┼─────┼│  Microsoft  │──┼─────┤▒│█
│├──┼─────┼│    Excel    │──┼─────┤▒│█
│├──┼─────┼─────┼─────┼─────┼─────┤▒│█──User Formatted Reports──►
│ ◄≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡► ╝│█

Figure 2:  A detailed view of Merril Lynch's proposed front-end/back-end

         Host Computer
│             MVS             │
│     │Teradata│     │  CICS  │
│ TSO │Director│ DB2 ├────────┤
│     │Program │     │SQL Host│
   │      │       └────┘   └─────────────────────LU 6.2 APPC/PC────┐
   │      └─────────────────────────────────────────────┐          │
   └──────────────────────────────────┐                 │          │
          ┌────────────────┐    ┌─────▼──────┐    ┌─────▼──────┐   │
      ┌───┤ Communications │    │ Interface  │    │ Interface  │   │
      │   │ Processor      │    │ Processors │    │ Processors │   │
      │   └───────┬────────┘    └─────┬──────┘    └─────┬──────┘   │
      │           │                   │                 │          │
      │   ┌───────┴───────────────────┴─────────────────┴──────┐   │
      │   │                       Ynet                         │   ≈
      │   └───┬──────────────┬──────────────┬──────────────┬───┘   │
      │       │              │              │              │       │
      │ ┌─────┴─────┐  ┌─────┴─────┐  ┌─────┴─────┐  ┌─────┴─────┐ │
      │ │ Access    │  │ Access    │  │ Access    │  │ Access    │ │
      │ │ Module    │  │ Module    │  │ Module    │  │ Module    │ │
      │ │ Processor │  │ Processor │  │ Processor │  │ Processor │ │
      │ └─────┬─────┘  └─────┬─────┘  └─────┬─────┘  └─────┬─────┘ │
      │       │              │              │              │       │
      │  ▓▒░░░░░░▒▓     ▓▒░░░░░░▒▓     ▓▒░░░░░░▒▓     ▓▒░░░░░░▒▓   │
      │  ▓▒░Data░▒▓     ▓▒░Data░▒▓     ▓▒░Data░▒▓     ▓▒░Data░▒▓   │
      │  ▓▒Storage▓     ▓▒Storage▓     ▓▒Storage▓     ▓▒Storage▓   │
      │  ▓▒░Units▒▓     ▓▒░Units▒▓     ▓▒░Units▒▓     ▓▒░Units▒▓ ┌─┴───────┐
      │  ▓▒░░░░░░▒▓     ▓▒░░░░░░▒▓     ▓▒░░░░░░▒▓     ▓▒░░░░░░▒▓ │ SQL     │
      │                                                          │ Network │
   TCP/IP                                                        │ Gateway │
   OSI                 ┌─────────────┐                           └─┬───────┘
   XNS (June           │ DARWIN      │                             │
   1988)               │ Workstation │                             │
 ┌────┴───────┐        └──────┬──────┘                             │
 │ Function   ├────────┬──────┴─────NetBIOS LAN──────┬─────────────┴─┐
 │ Call       │ ┌──────┴──────┐┌──────┴──────┐┌──────┴──────┐  ┌─────┴────┐
 │ Translator │ │ DARWIN      ││ DARWIN      ││ DARWIN      │  │ SQL      │
 └────────────┘ │ Workstation ││ Workstation ││ Workstation │  │ Database │
                └─────────────┘└─────────────┘└─────────────┘  │ Server   │

Figure 15:  DARWIN: Top Level Dataflow Diagram

                   │ Standard SQL │
                   │ Text Queries │
┌───────────────┐   │            │   ┌───────────────────────────────────┐
│▓▓▓▓▓▒▒▒▒▒░░░░░◄───┘            └───┼─██────────────Darwin───────────↑↓─│█
│▓▓▓ Merrill  ░░│█                   │┌────────────────────────────────┐│█
│▓▓▓  Lynch   ░░│█◄──────────────────┤│ DARWIN                         │▒│█
│▓▓▓ Database ░░│█  NetBIOS Network  ││                                │▒│█
│▓▓▓ Universe ░░│█──────────────────►││    Data Acces and Reporting    │▒│█
│▓▓▓▓▓▒▒▒▒▒░░░░░│█                   ││        Through Windows         │▒│█
└───────────────┘█      ┌────────────►│                                │▒│█
 ▀▀▀▀▀▀▀▀▀▀▀▀│▀▀▀▀      │            │└────────────────────────────────┘▼│█
            ┌▼──────────┴┐       ┌───┤ ◄≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡► ╝│█
            │   Result   │       │   └─────────────────────────────────┘█
            │   Table    │       │     ▀▀│▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀│▀▀▀▀▀▀│▀│▀▀▀
            └────────────┘       │   Mouse and            Graphical  │ │
                           ┌─────┴─┐ Keyboard Input        Display   │ │
                           │Results│   ╔═╧══════════════╗     │      │ │
                       ┌───┤ Table │   ║ ░░░░░░░░░░░░░░ ◄─────┘      │ │
                       │   └───────┘   ║ ░░░░░░░░░░░░░░ ║█         Control
                       │               ║ ░░░░░░░░░░░░░░ ║█           │ │
                 ▓▓▓▒░░▼░░▒▓▓▓         ║ ░░░░░░░░░░░░░░ ║█         Messages
                 ▓▓▓▒░░░░░▒▓▓▓       ╔═╝────────────────╚═╗          │ │
                 ▓▓▓CE_BIFF▓▓▓       ║ ‼‼‼‼‼‼‼‼‼‼‼‼‼‼‼‼‼  ║█         │ │
                 ▓▓▓ Files ▓▓▓       ╚═══════════════════╝█         │ │
                 ▓▓▓▒░░░░░▒▓▓▓         ▀▀▀▀▀▀▀▀▀▀▀▀▀▀│▀│▀▀▀▀         │ │
                 ▓▓▓▒░░░░░▒▓▓▓                 User  │ │             │ │
                       │                             │ │             │ │
                       │                             │ │             │ │
                       │            ┌────────────────┴─▼─────────────▼─┴┐
                       │            ├─██───────Microsoft Excel───────↑↓─│█
   ┌───────────┐       └────────────►┌──┬─────┬─────┬─────┬─────┬─────┐│█
   │≡≡≡≡≡≡≡≡≡≡≡├┐                   │├──┼─────┼┌─────────────┐──┼─────┤▒│█
   │≡≡≡≡≡≡≡≡≡≡≡│├┐                  │├──┼─────┼│  Microsoft  │──┼─────┤▒│█
   │≡≡≡≡≡≡≡≡≡≡≡│││                  │├──┼─────┼│    Excel    │──┼─────┤▒│█
   │≡≡≡≡≡≡≡≡≡≡≡│││◄─User Formatted──┤├──┼─────┼└─────────────┘──┼─────┤▒│█
   │≡≡≡≡≡≡≡≡≡≡≡│││     Reports      │├──┼─────┼─────┼─────┼─────┼─────┤▒│█
   │≡≡≡≡≡≡≡≡≡≡≡│││                  │└──┴─────┴─────┴─────┴─────┴─────┘▼│█
   └┬──────────┘││                  │ ◄≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡► ╝│█
    └┬──────────┘│                  └───────────────────────────────────┘█
     └───────────┘                   ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀

Figure 16:  DARWIN.XLM

 1  DARWIN Macro
 2  PC Software Development
 8  =ADD.MENU(1,B6:C8)
 9  =ADD.MENU(2,B6:C8)
10  =ADD.MENU(3,B6:C8)
11  =ADD.MENU(4,B6:C8)
12  =ADD.MENU(5,B6:C8)
13  =ADD.MENU(6,B6:C8)
15  =HIDE()
16  =RETURN()
23  =COLUMN WIDTH(22,"C1")
24  =COLUMN WIDTH(22,"C2")
25  =COLUMN WIDTH(22,"C3")
26  =COLUMN WIDTH(22,"C4")
29  =RETURN()
34  =SEND.KEYS("%{F10}",TRUE)
36  =RETURN()
44  =SEND.KEYS("%{F10}",TRUE)
46  =RETURN()

Figure 17:  DARWIN SQLBase Interface Shell Functions


 BOOL SQLExecute(string, bindstring);

     LPSTR  string;      /* null terminated SQL statement */
     LPSTR  bindstring;  /* null terminated bind data */


 TRUE if completed correctly.
 FALSE if error.

  The bindstring may be NULL, indicating that there is no bind value.


 int SQLFetchNextRow();


 > 0 : number of fields in the row
 = 0 : no more information to fetch (past EOF)
 = ──1 : ERROR occurred

  This routine fetches the next logical row from the database into a SQL
  API internal structure for later use by SQLGetField.


 LPSTR SQLGetField(fieldnumber, datatype, flag);

     int  fieldnumber;       /* desired field (first field=1) */
     LPINT       datatype;   /* datadictionary/ metaphore datatypes */
     BOOL        flag;       /* TRUE=remove leading & trailing blanks */


 LPSTR, which must be cast to the correct type for usage.
 NULL if field is null.

  Setting flag = TRUE is suggested because SQLBase pads fields to maximum
  field width.

  If returned data type =

   string   ->   cast as LPSTR
   long     ->   cast as LPLONG
   date     ->   cast as LPDOUBLE
   double-> cast as LPDOUBLE

   typedef double FAR *LPDOUBLE /* defined in SQLAPI.H */
   typedef long int FAR *LPLONG /* defined in SQLAPI.H */


 LPSTR SQLGetFieldAsText(fieldnumber, datatype, flag);


 LPSTR to a NULL-terminated string.
 NULL if field is null.

  Setting flag = TRUE is suggested because SQLBase pads fields to maximum
  field width. Numericals are padded to 27 digits in ASCII representation.

If returned data type =

 string   ->   use as LPSTR
 long     ->   use as LPLONG
 date     ->   use as LPDOUBLE
 double-> use as LPDOUBLE

 typedef double FAR *LPDOUBLE /* defined in SQLAPI.H */
 typedef long int FAR *LPLONG /* defined in SQLAPI.H */


 BOOL SQLtoDlgListBox(string, bindstring, hDlg, DlgItem);

     LPSTR        string;     /* SQL SELECT statement */;
     LPSTR        bindstring; /* null terminated bind data */
     HWND hDlg;               /* handle of a dialog box */
     WORD DlgItem;            /* item number of destination list box */


 TRUE if completed successfully.
 FALSE if error.

  This function allows one function call (from a dialog box) to perform a
  complete SQLBase execute and fetch sequence. The fields are laid out as a
  single list box item with the fields separated by a blank space. Field
  width is maintained as returned by SQLBase as strings.


 int SQLToBiffFile(select, bindstring, bifffilename);

     LPSTR     select;        /* SQL SELECT statement */
     LPSTR     bindstring;    /* null-terminated bind data */
     LPSTR     bifffilename;  /* filename for BIFF file */


 Number of rows written.
 ─1 if error occurred.

  This function allows one function call to perform a complete SQLBase
  execute-and-fetch sequence. The fields are laid out as cells within a new
  Excel Biff file.

Bindstring and bifffilename may be NULL.


 int SQLErrorNum();


An integer defining the type of error, if any, that occurred.

  To maintain the design that hides SQLBase specifics, the SQLBase internal
  error number and information will be translated into DARWIN internal
  values yet to be defined in SQLAPI.H.

Figure 18:  DAR_COLUMNS contains information on the columns that comprise a
            table. For each column in a table there is one row in
            DAR_COLUMNS. Each row in the table is a column in DAR_COLUMNS.

┌─────COLUMN NAME─────┐
┌─────COLUMN NAME─────┐

Tablename      Internal         Char (15)     The internal name of the
               Table Name                     table that contains the

Columnname     Internal         Char (15)     The internal name  of the
               Column Name                    column.

Printname      Column Print     Char (30)     The print name or external
               Name                           name of the column as it
                                              appears in DARWIN's
                                              graphical representation
                                              of the data.

Datatype       Data Type        Integer       The data type of the column.
                                               0 = Integer
                                               1 = Real (floating point)
                                               2 = Character
                                               3 = Date
┌─────COLUMN NAME─────┐
                                               3 = Date

Precision      Field Width      Integer       Contains the maximum
                                              number of digits or
                                              characters for datatypes
                                              INTEGER, REAL, or
                                              CHARACTER. If the
                                              column is a date datatype
                                              the field contains a 0.

Fraction       Fraction Size    Integer       The number of digits to the
                                              right of the decimal point.
                                              This is used for real
                                              (floating point) numbers.

Resolution     Data             Integer       This column is not
               Interpretation                 supported. The field = 0.

Keyusage       Key?             Integer       Indicates if the column is
┌─────COLUMN NAME─────┐
Keyusage       Key?             Integer       Indicates if the column is
                                              part of the primary key.
                                               0 = Column is not part of
                                                  primary key
                                               1 = Column is part of
                                                  primary key

Domain         Domain           Integer       This column is not used.
                                              The field = 0.

Navalue        N/A Value        Char (30)     This column is not used.
                                              The field = 0.

Naused         N/A Used         Integer       This column is not used.
                                              The field = 0.

Dorder         Display Order    Integer       The order in which the
                                              columns in the
                                              table are displayed.
┌─────COLUMN NAME─────┐
                                              table are displayed.

Description    Description      Char (200)    A text description of the
               of Column                      column.


CodeView for Windows Provides an Interactive Debugging Environment

Paul Yao and David Durant☼

Microsoft(R) Windows and CodeView(R) are two products that have made a
significant impact on the PC world, mostly because of their highly
interactive user interfaces. The CodeView user interface is based on
character-oriented windows and makes a debugging tool accessible in a way
that was not previously available on microcomputers. The Windows user
interface is a suggested style and a set of powerful tools used to
implement this style.

This article will examine CodeView for Windows (CVW), a product that is
familiar to many DOS programmers, but, until now, has not been "Windows-
aware." First, we'll look at why an interactive debugger is useful to
programmers, followed by an examination of the major stumbling blocks of
Windows programming and how to avoid some of them. After a brief
discussion of CVW's hardware requirements, we will relate our
experiences using a beta copy of CVW. Finally, a sample debugging
session will trace the messages involved in the creation of an
application's main window.

CodeView employs many of the features found in Windows applications (see
Figure 1☼), including drop-down menus, dialog boxes, windows, and mouse
support. It is paradoxical that Windows programmers have for so long been
unable to use a tool so akin to their own environment.

There is a simple explanation for this: the two products were created
independently by different development groups within Microsoft. CodeView
was not originally developed with Windows applications in mind; it was
geared toward the development of DOS applications.

Although its interface is similar to that of Windows, CVW is not itself
a Windows application in the usual sense. Instead, it is a special version
of the original CodeView that has been made aware of the way in which the
Windows memory manager juggles memory. What strikes you first when using
CVW is that it does not share the screen with Windows; rather, it sends
output to its own display monitor in exactly the same way that Symdeb,
another interactive debugger, does.

Interactive Debugger

In the Windows programming classes that we teach, we are constantly
astounded by the number of people who have never used an interactive
debugger. There are certainly useful alternatives to an interactive
debugger, such as the embedding of calls to MessageBox to display data
and the inclusion of calls to fprintf to output trace information to a
communications port. However, these techniques are cumbersome when
compared with the power and flexibility available in an interactive
debugger, where you have total control of your computer while your
program is processing.

Without an interactive debugger, you must stop, rewrite, and recompile
your code before you can begin execution. An interactive debugger offers
you the flexibility to decide how to investigate the operation of
your program. It allows for spontaneous interaction with a running
program at a level not otherwise possible.

Getting started with an interactive debugger is no trivial task,
however. Setting up a debugging environment takes time, and you must
achieve a minimum proficiency with a debugger before it is truly useful.
With the tight development schedules that programmers face, the
preparation of a debugging environment somehow never gets done. Still, it
is definitely worth the time; people who start working with an
interactive debugger reap enormous benefits almost immediately.

Development Problems

Even with an interactive debugger, debugging a Windows application
presents some unique challenges. It would be bad enough if the dynamic
loading, movement, and discarding of memory objects were the only source
of problems. However, the structure of Windows programs, the lack of
hardware memory protection, and the need to be good at so many things,
such as Windows Software Development Kit (SDK) routines, Intel(R)
architecture, and C programming, contribute to the difficulties
encountered when coding and debugging Windows applications.

For some of these problems, the only solution is time and experience.
Windows programming takes everything you know about programming and
turns it on its ear──as the saying goes, everything you know is wrong.
Taking advantage of the power of Windows/Presentation Manager
programming requires you to learn a new way of thinking. Six months of
full-time Windows programming, assuming prior experience with C, is a fair

While you are gaining this experience, however, there is error checking
available for free, provided by the compiler and Windows, itself. It
requires only that you flip the right switches. Here are a number of
suggestions for overcoming some common Windows development problems by
using these free services.

Always use the highest level of compiler warning switches (W2 or W3), and
aim for a clean compile.

Prototype every routine that you write. For examples of prototyping,
just look at the WINDOWS.H file, where you will see declarations like the
one shown below:

  TextOut (HDC, short, short, LPSTR, short);

This declaration gives the compiler some very useful information, such
as the return value (in the example above, Boolean), the calling
convention (FAR and PASCAL), the number of parameters (5), and the type
of each parameter.

If you prototype your routines, you can quickly and easily eliminate some
C programming errors. For example, in the course of development, it is
common to add parameters to various routines. If, however, you fail to
change every call to a routine that has one more (or one less) parameter,
your application will contain a bug.

This bug may bite you by causing your application to fail immediately. In
this case, you will probably search until you find the problem. Or the bug
may sit and wait, creating sporadic and unpredictable problems in your
program. Because tthis type of bug can be found almost instantly,
prototyping can save you much wasted time and effort and should always be
used with a W2 or W3 compiler warning switch.

Avoid casting. It is redundant in many situations, and can hide bugs in
others. Let the compiler tell you about type mismatches, and in those
cases respond by casting.

The following casting (the two LPPAINTSTRUCTs and the LPSTR) is

  hDC = BeginPaint (hWnd, (LPPAINTSTRUCT)&ps);
  TextOut  (hDC, 10, 10, (LPSTR)"It's a Long Way to Tipperary",28);
  EndPaint (hWnd, (LPPAINTSTRUCT)&ps);

It was included in the earliest examples of Windows programs, however,
because the C compiler did not support prototyping at that time. Every
version of the Microsoft C compiler since C Version 4.0 supports prototyping
and eliminates the need to cast in many circumstances. Prototyping enables
the compiler to promote a near pointer to a far pointer, based on the
prototypes in WINDOWS.H.

Be sure that you include every window procedure, dialog box procedure,
enumeration procedure, callback procedure, and subclass procedure found
in the EXPORTS list of your application's module definition (DEF) file.
This is a very common error and wastes much time. The lack of compiler and
linker error checking in this regard makes this problem difficult to find.

Install and regularly use a debugging version of Windows, not to be
confused with a Windows-compatible debugger, such as CodeView. You create
this by using special copies (from the SDK) of KERNEL.EXE, USER.EXE, and

A debugging version of Windows performs error checking not available in
the retail version of Windows. It catches problems like bad handles and
wild pointers and notifies you via a debugging terminal, which you must
have ready to receive the fatal exit messages, sometimes referred to as
"RIP" (Rest In Peace) codes. Be sure to include the symbol files,
KERNEL.SYM, USER.SYM, and GDI.SYM, which are used to display stack trace
information when RIP code is generated.

With a debugging version of Windows installed, be sure to set the
following switches in your WIN.INI file:


This causes all free bytes to be filled with the value CH. This protects
you in two ways: first, if a wild pointer from your program causes a
write to occur in a free block, a fatal exit message is sent to your
debugging terminal. Second, you are protected if your program tries to
jump to a free block. The value CCH translates into the machine
instruction INT 3, which is a call to the debugger. A wild jump, then,
causes the debugger to come alive, allowing you to display a stack
trace and determine the part of your program from which execution
departed. Because of these benefits, the use of these switches is strongly

If you are using Expanded Memory Specification (EMS) memory (or running
Windows/386, which has built-in EMS support), make sure you set the
following switch in WIN.INI:


This switch makes sure the debugger is told when EMS pages are banked in
and banked out and lets you set breakpoints within the discardable code
segments of dynamic-link libraries, where many Windows SDK routines

Hardware Setup

CVW has some unique hardware requirements, such as the need for a second
monitor. We will review the choices available and discuss the need for
EMS 4.0 memory. If you have been using Symdeb, the setup of the second
monitor is the same, and you are ready for CVW.

You have two choices: a monochrome display adapter (MDA) with a monochrome
monitor or an asynchronous terminal with a null-modem adapter. The first
choice is the preferable one, because you can then use CVW in full-screen
mode. An asynchronous terminal is useful, though, since you can take
advantage of the access to local variables that CVW provides. However, you
must operate in line mode without full-screen support.

When operating in full-screen mode, CVW lets you use the mouse to select
menu items, and control its operation (when CVW has control). In this
situation, CVW cooperates nicely by sharing the mouse with Windows.
However, you must load a DOS mouse driver, which CVW will then use for its
mouse support. Even if you don't plan on using a mouse (in line mode, for
example), you still must install a driver for it. Otherwise, the Windows
mouse freezes up, although you can still operate Windows with the keyboard
interface commands. We hope this problem is corrected before CVW is

EMS 4.0 Memory

CVW requires you to have EMS memory present. CVW takes up 138Kb of main
system memory and then employs EMS memory for its code, data, and symbol
space. The available options depend on the specific Windows product that
you are using. With Windows 2.x, you must have an EMS board and an EMS 4.0
or later driver. We experimented with an Intel Above(TM) Board, as well as
with an AST(R) RAMpage!(R) board. Both worked well, but you must make sure
that you have the correct version of the driver.

EMS 4.0 support is built into Windows/386, which CVW can use. There are
two things to keep in mind here. First, you must have a minimum of 640Kb
main memory and 512Kb extended memory in order for EMS support to be
available under Windows/386. Second, you must play a trick on Windows/386
so that its EMS support is loaded first; you then start CVW before
starting Windows. This process is described in more detail in the

CodeView and Windows

Now that CodeView is available for debugging Windows applications,
Windows programmers may ask if its debugging capabilities can really be
adapted to the unique and extremely complex Windows environment. The
answer is yes, but it is a qualified yes. Let's look at CodeView's
strengths and weaknesses in the Windows environment──remember that our
experience is based on a beta release of the software.

Windows Programs

As every Windows programmer quickly learns, a Windows application
program does not control its own destiny; it lives at the mercy of
messages sent by the user, by Windows, by other programs, and by itself.
Windows programs do not always have control of the processor; instead
they spend most of their time suspended, waiting for a message to arrive.

When a message is delivered to a Windows program, it gains control of the
processor and must respond to the particular message. The message is often
delivered to the GetMessage processing loop. From there control is
transferred to the appropriate window procedure. The decision is based
on a piece of information that is part of a message, the window handle.

Once a window procedure receives a message, it determines the message
type and typically branches to a subprocedure referred to as a message
procedure. Each message procedure is independent of the others in the
sense that the processing of one message cannot assume that some other
message has already been received or will subsequently be received. It can
only check the current state of the application, such as the currently
active cell of a spreadsheet, and react accordingly.

The classic example of the extent to which Windows applications must
react to messages is the WM_PAINT message. In response to a WM_PAINT
message, a window procedure must be capable of recreating the contents
of the client area at any time, for any reason, and under any
circumstances. A Windows program, then, tends to be reactive; it is the
user and not the program that controls the flow of processing.

A Windows program can be thought of as a state machine, with the state
stored in external variables. Windows programs also differ from normal
programs in that they are object-oriented. Window classes, windows,
display contexts, GDI objects, and loaded resources are all objects in a
general sense: they are identified by a handle, you do not have to be
concerned with the underlying data structures, and a given type of
object, such as a window, can be manipulated in a standard set of ways
(any window can be moved, sized, repainted, sent a message, and so on).

Also, Windows is a library-oriented environment, specifically dynamic-
link libraries. All the toolkit routines are contained in dynamic-link
libraries. You can use dynamic-link libraries to share code, resources,
data, and to control the allocation of some limited resource, such as
system memory, or a local area network link.

CodeView Debugging

Programs have two parts: code and data. One of the chief uses of an
interactive debugger like CodeView is to enable you to follow the path
that your program takes through the code. During the course of such a
"trace" operation, you can view and even modify the data on which your
code operates. CodeView offers three methods of tracing execution:
executing the program one line at a time, executing the program in "slow
motion," and executing the program at normal speed but halting its
execution at predetermined points within the code or based on prespecified
data conditions.

Executing the program one line at a time means either one source line or
one object instruction at a time, depending on the mode that CodeView is
currently in. Running in slow motion means that approximately four
instructions per second will be executed and that the display of code and
data are synchronized with program execution, continuously updated to
reflect the current program location and data values.

At any time you are running in slow motion, execution of the program can
be halted to permit other interactions with CodeView. Hereafter, running
the program in slow motion will be referred to as executing the program,
while running the program at normal speed, which requires the Go command,
will be referred to as just running the program.

The third technique, running the program at normal speed and halting its
execution at predetermined points, consists of setting breakpoints,
watchpoints, and tracepoints. Setting a breakpoint means specifying a
point in the code at which program execution is to halt. This is done by
finding that line of code in the code display window and clicking upon it
with a mouse or by using the breakpoint set command, bp. When you do
this, the line will be displayed in high intensity. You can remove the
breakpoint by clicking on the line a second time, which will return it to
normal intensity. As many as 19 breakpoints can be set at a time.

A brief word about an idiosyncrasy of CVW: CVW indicates that a
breakpoint has been set by highlighting a line of code. However,
highlighting also indicates the current line when tracing execution. These
two unrelated uses make it difficult to determine if the current line is
set as a breakpoint or not. To find out, you simply have to trace
execution to another line.

Just as breakpoints are execution halts set within the code, watchpoints
and tracepoints are execution halts based on data values. The watchpoint
is a conditional statement involving one or more program variables, such
as "character count==80". Program execution halts at the instruction and
causes the value of watchpoint to become true.

A tracepoint is also a statement involving one or more program variables,
such as iIndex, but it need not be a conditional statement. This is
because the program is going to halt anytime the value of a tracepoint
changes, regardless of what value it changes to.

One nice thing about breakpoints, watchpoints, and tracepoints is that
they remain in effect even after the program is finished executing. Thus
you can set the watchpoints and breakpoints and run the program several
times without having to reset them.

One not-so-nice thing about watchpoints and tracepoints concerns local
variables. Local variables, as most programmers know, are in effect only
while the routine in which they are defined is actually being executed
and are thus known only to CodeView at that time. Therefore, if you wish
to set a watchpoint or a tracepoint involving a local variable, you must
first set the breakpoint and execute the program up to that breakpoint so
that the halted execution is now within the routine that contains the
local variable. Once you have done this you can then define the
watchpoints and tracepoints that involve that local variable, remove the
breakpoint, and continue to execute or run the program.

A limitation of CodeView or any interactive debugger is that input is
shared between your program and your debugger. Your program needs input,
of course, which it receives in the form of mouse and keyboard input, and
debuggers also require input. In the case of CodeView, both keyboard and
mouse input can be used. However, if you wish to debug mouse or keyboard
input, switching into a debugger can be annoying. There are ways around
this, of course, and we mention them not to detract from CodeView, but
merely to identify an area that requires special attention when using an
interactive debugger.

For example, setting a breakpoint to trace through WM_MOUSEMOVE
processing can be difficult, if not impossible. Another limitation
inherent in any interactive debugger is that, with the interruptions
caused by breakpoints and the ability to trace program execution, time-
dependent operations can be difficult to debug. Thus, CVW is limited in
its ability to work with programs dependent on WM_TIMER messages for their
normal operation. Also, if there are external, real-time events that your
Windows program must track, the time-dependent portions of your program
may be impossible to debug with an interactive debugger.

Windows Debugging

Given that Windows programs react to messages, can be viewed as state
machines, and are in an object-oriented and dynamic-link library
environment, how can CVW help in debugging Windows programs? CVW handles
the reactive nature of a Windows application program very well because of
its ability to trace the flow of execution through code. By setting
breakpoints, watchpoints, and tracepoints, you can see which message types
are being received and what a program does in response to any given
message. Later on, we will do just that by tracing the message traffic
associated with the creation of an application's main window.

CVW also fits well with the state-machine nature of a Windows application
program. Since the state of the application is maintained in program
variables and the user can watch those program variables change as the
program runs, even halting execution when the variables reach some
unexpected value, the user can remain aware of the state of the program as
it is running and detect problems in that area.

CVW is of limited use in dealing with the object-oriented nature of a
Windows program. This is not so much a flaw in CodeView as simply a result
of the fact that objects are kept outside the program's data segment. Pens,
brushes, fonts, bitmaps, globally allocated memory objects, and the data
structures that describe a window are beyond easy access for CodeView.

If you wish to look at the contents of a memory object, a cursor, icon, or
bitmap, HeapWalker is your best bet. If you want to find out about a GDI
object, you can call the GetObject routine to transfer values into a
program variable that can then be viewed with CodeView. If you wish to see
information on any other object, such as a window, use the appropriate
toolkit routine, GetWindowWord for example, to transfer the
information to a program variable.

CVW is helpful in an object-oriented environment because you can look at
handles and pointers, which are, after all, located within your program's
variables. By setting tracepoints you can detect changes in the value of
handles, and you will be able to trap events like the unintentional
overwriting of a handle or pointer value. Also, you can determine when a
null handle has been returned and be certain that a failure has

CVW is also useful in working with dynamic-link libraries. CVW permits
you to perform source-level tracing into your dynamic-link libraries,
giving you a more complete picture of what is happening during the execution
of your application.

If you want to view the code contained in one of the Windows dynamic-link
libraries, such as Kernel, User, or GDI, CVW lets you do this. However,
its usefulness depends on whether you understand 8086 assembly language
or not. Because you do not have the source code to these dynamic-link
libraries, you can only trace through them on a machine-language level.
If you are reasonably proficient in reading 8086 assembler, this can be
invaluable for determining exactly what an SDK routine is doing under a
particular set of circumstances.

Sample Session

We are now ready to walk through a sample CVW debugging session. Our
purpose is to show you how setting a breakpoint on a window procedure's
switch statement allows you to watch the messages associated with the
creation of a window.

Getting Started

The program that we will trace is the "Hello Windows" program from the
Sample Source Code disk of the Windows Version 2.03 SDK, which you should be
able to copy from the Windows SDK to walk through this debugging session

The first step is to modify the program's make file so that it has the
appropriate compiler and linker switches. This involves adding the Zi and
Od switches to the compile line and the /CO switch to the link line. The
complete make file is shown in Figure 2.

Once you have recreated HELLO.EXE, you start CVW by using one of the two
command lines that are shown in Figure 3. If you are using an MDA adapter
with a monochrome display, which enables full-screen mode debugging, use
the first command line shown in Figure 3. If you are using an
asynchronous terminal with a null-modem adapter as your debugging
workstation, which enables line-mode debugging, use the second command line.

In the rest of this sample debugging session, full-screen debugging mode
is used. When CVW starts, you will see the two screens shown in Figures 4
and 5.☼In Figure 4, you see that CodeView has started. In Figure 5
however, you see that Windows itself has not yet started.

Setting Breakpoints

First, set a breakpoint at the window procedure's switch statement, which
involves locating a source line and then either clicking with the left
button or using the bp command to indicate the necessary source line.
Figure 6☼ shows a breakpoint set with the bp command highlighted.


Next, start execution. When you issue the Go command, you first notice
that Windows starts running. Figure 7☼ shows the display screens after
Windows has started, when the system has encountered the breakpoint set at
the switch statement in the Hello program's window procedure, HelloWndProc.
Note that the Windows display contains the familiar blue of the desktop,
although nothing currently occupies it. In the CodeView display, you'll see
that you have mouse control and are able to issue CodeView commands.

Adding to the Watch List

At this point, we would like to add the local variable "message" to the
watch list. Because we want the value to be displayed in hexadecimal, a type
modifier is added to the w? command. We add message to the watch list by
typing the following at the CodeView prompt:

  > w? message, x

Figure 8☼ shows that the "watch window" has been opened and that the
current value of message (0024H) is being displayed. A quick look at
WINDOWS.H reveals that this value represents the WM_GETMINMAXINFO message.
Even though a message has been received for our window, it has not been
processed yet. To allow the message to process, we again issue the g
(GO) command, after which another breakpoint is encountered, and the
display appears as shown in Figure 9☼. Notice that the CVW display shows
that the value of message is 0081H and that nothing seems to have changed
in the Windows display.

The messages next received are:


The Nonclient Area

The first message that causes anything to appear on the Windows display
is the message WM_NCACTIVATE. The NC part of the message stands for
nonclient. As Figure 10☼ shows, this simply draws the border of the window,
along with the System Box and the Minimize and Maximize Boxes.

The very next message, WM_GETTEXT, seems to cause the caption to be drawn.
In fact, we suspect that the prior WM_NCACTIVATE sent a WM_GETTEXT message
to retrieve this Windows message so as to properly display the caption
information (see Figure 11☼).

Another group of messages appear that affect the display. These messages


Erasing the Background

Figure 12☼ demonstrates the effect of the message WM_ERASEBKGND. Clearly,
the background of the window has been erased with the main window's default
background brush, which is a white brush. A pair of messages then appears:


You should note that the size and the move messages appear at window
creation time, which is useful to know if you plan to respond to these two
messages. Think of these messages as saying, "Your window is changing in
size from nothing to something and moving from nowhere to somewhere."

Hello Windows!

Finally, when a WM_PAINT message appears, the familiar "Hello Windows"
message, shown in Figure 13☼, is displayed. It seems that so many messages
have flown by as part of the window's creation process, but there are still
two messages coming in, namely:


The last message to appear is the WM_MOUSEMOVE message. This concludes
the immediate window creation message traffic. In all, 20 messages were
received by this window procedure before the window creation process was

At this point, if you move the mouse cursor, another WM_MOUSEMOVE message
is sent to the application's window procedure. You need to disable the
breakpoint; otherwise you would be trapped forever by mouse movement
messages. You do this either by clicking again on the breakpoint (to
toggle the breakpoint) or by issuing the following command:

  > bc 0

Quitting CodeView

Before you quit CodeView, you need to close down Windows. This is because
Windows modifies the system, including several system vectors that must be
restored if the system is to operate in a normal fashion. Once this is
done, you exit CodeView with the Quit command:

  > quit

A Breakthrough

When it becomes available, CodeView for Windows will be a breakthrough for
Windows developers. Its interface is intuitive and easy to use. Its ability
to access local variables, and to display data structures and evaluate
complex C expressions gives it power that puts it in a class by itself.

However, its main memory requirement (138Kb) and the necessity for EMS
memory may limit its usefulness for some developers. In contrast, Symdeb
(43Kb), although lacking the full-screen user interface and access to
local variables that are part of CodeView, will always be useful for certain
Windows developers. Also, the copy of CodeView that we looked at did not
have some of the internal Windows hooks that Symdeb has, such as the ability
to dump the global heap, dump the free list, and so on. This is a minor
issue, however, when compared with the many great advantages that CodeView's
user interface provides.

Figure 2:  The complete make file for compiling the Hello program, to be
           used with CodeView.

# Standard command line definitions
cp=cl -d -c -AS -Gsw -Od -Zpie
# Standard inference rules
#.c.obj:$(cp) $
# The C File List

hello.obj: hello.c hello.h

hello.res: hello.rc hello.ico hello.h
   rc -r hello.rc

hello.exe: hello.obj hello.res hello.def
   link4 hello/CO,/align:16,,slibw/NOE,hello.def
   rc hello.res

Figure 3:  CodeView for Windows can be started in several ways, depending
           depending on what type of secondary monitor or terminal is in use.

Command when using an MDA adapter:

  C> cvw /2 /L hello.exe \windows\win.com hello.exe

Command when using an asynchronous terminal and null modem adapter:

  C> cvw /C=com1 /T /L hello.exe \windows\win.com hello.exe


OS/2 Graphics Programming Interface: An Introduction to Coordinate Spaces

Charles Petzold☼

        You can't draw with them.
        You can't draw without them.

Pixels are the single greatest impediment to device-independent graphics
programming. Part of the problem is that different output devices have
different pixel resolutions. A 150-pixel-high image is 5 inches tall on a
CGA display but only a half inch on a 300-dots-per-inch laser printer.
Moreover, many display output devices have different horizontal and
vertical resolutions. If you are working with pixels, you will have
trouble drawing round circles and square squares.

To avoid these problems, many high-level graphics programming languages
give the programmer a means to draw in units other than pixels. The
programmer specifies coordinate positions in terms of inches,
millimeters, or other convenient units. The graphics system then converts
these coordinates to pixels when drawing.

The conversion of points from one coordinate system to another is called a
"transform." At the very least, transforms should hide from the program
the resolution of the output device and any differences in horizontal and
vertical resolution. But transforms also play an important role in
"modeling"─the construction of images from individual elements.
Transforms provide the means to manipulate these elements before they are

The GPI (Graphics Programming Interface) component of the OS/2
Presentation Manager supports transforms both for working in units other
than pixels and for more sophisticated modeling tasks. In fact, the GPI
contains so many transform functions that the subject can be quite
confusing to the GPI newcomer.

This article discusses the GPI transforms that are supported under the
"micro-presentation space" (or micro-PS). As I discussed in "The Graphics
Programming Interface: A Guide to OS/2 Presentation Spaces," (MSJ, Vol.
3 No. 3), the micro-PS supports only a subset of GPI functions. However,
working with a subset can certainly be an advantage when learning a new
topic because you can simply ignore everything not included in the

I Point, Therefore I Am

When you draw with GPI functions, you specify a two-dimensional point
using the POINTL structure. The POINTL structure contains two fields named
x and y. GPI maps the point you specify in the POINTL structure to a point
on the output device (which can be the screen, a printer, or a plotter).

At first, this seems like simple analytic geometry. The points you use in
GPI functions are simply points in a Cartesian coordinate system, as shown
in Figure 1. We can represent a point in this coordinate space by the
notation (x,y). The point (0,0) is the center where the horizontal and
vertical axes meet.

This is good for starters, but its simplicity is deceptive. Let's pose a
few basic questions:

  ■  Where is the origin? That is, when you use a POINTL structure with
     the x and y fields both set to zero, where is that point mapped to on
     the output device?

  ■  How do units of x and y in the POINTL structure correspond to the
     natural pixel units of the output device? Do increasing values of x
     really go to the right? Do increasing values of y really go up?

  ■  Is it necessary for values of x to represent a position on the
     horizontal axis and for values of y to represent a position on the
     vertical axis? Can the entire coordinate system be rotated in some
     way to look like Figure 2?

  ■  Is it even necessary for the x and y axes to be at right angles to
     each other? Or can the coordinate system look like the one that is
     shown in Figure 3?

The answers to these questions are: whatever you want them to be. Using
GPI transforms, you control exactly how the points you specify in GPI
functions are mapped to the output device.

In fact, these four sets of questions are related to the four basic types
of transforms:

  ■  Translation: Where the point (0,0) is mapped to on the output

  ■  Scaling: How units of x and y correspond to pixels.

  ■  Rotation: How the x and y axes are oriented.

  ■  Shear: Whether the two axes are at right angles to each other or

We'll examine these four types of transforms in more detail later in this

Visualizing Transforms

One way of thinking about graphics transforms is to imagine the axes
shown in Figures 1, 2, and 3 as being superimposed on an output device,
such as the video display. The points you use in GPI functions specify a
point in the coordinate space. That point corresponds to a particular
pixel position on the screen.

Using GPI transform functions, you can effectively shift the axes so that
the origin is at a different location on the screen. You can stretch or
compress an axis so that the units you use relate to pixels in different
ways. You can rotate the axes so that objects you draw appear to be
rotated. You can tilt one or both axes so that objects are distorted.

But you'll find that this visual approach is not the best way to think
about GPI transforms. It's relatively easy if you're dealing with only two
coordinate systems (the coordinate space in which you draw and the
natural coordinate space of the output device), but the micro-PS subset of
GPI defines three additional coordinate systems between these two. You'll
find that the visual approach is inadequate and that instead you must rely
on formulas. These formulas govern how a point is mapped from one
coordinate space to the next.

Five Coordinate Spaces

The five micro-PS coordinate spaces are shown in the table in Figure 4.
The "Transform" column of the table shows the name of the transform that
maps between each pair of coordinate spaces. The last column shows the GPI
function that lets you set or change the transformation formula.

The points you specify in GPI drawing functions are in world coordinate
space. These points are eventually mapped to the output device, which is
the media coordinate space.

The Media and the Device

The best place to start is at the bottom of Figure 4 with the windowing
system transform. This is the easiest transform because it's one you can't
control and therefore do not have to worry about. The windowing system
transform is handled within the Presentation Manager.

Media coordinate space is dependent on the hardware of the output device.
The video display, for example, is organized in rows and columns of
pixels. Each pixel is one unit in media space. For convenience, the
origin-the point (0,0)-is defined to be the lower-left corner of the
display. Increasing values of x go to the right; increasing values of y
go up.

But you never use media space coordinates in GPI functions. To draw on
the display, you need a handle to a presentation space. The presentation
space is always identified with a particular window. The window has its own
coordinate system known as device space. The origin is at the lower-left
corner of the window. Like media space, units are pixels. Increasing values
of x go to the right; increasing values of y go up.

The windowing system transform maps from device space to media space.
This transform is based on the position of the window relative to the screen.
pixels from the left side of the screen and 100 pixels from the bottom.
The windowing system transformation formulas are:

  xmedia = xdevice + 50
  ymedia = ydevice + 100

Thus, the point (0,0) in device space is mapped to the point (50,100) in
media space. These are simple transformation formulas that effectively
shift the origin of the coordinate space. This type of transform is called
a "translation."

The Presentation Page

The presentation page (also known more simply as the page) is the
coordinate space in which a picture is constructed for display. You can
visualize the page as precisely that──a page of paper. On the screen, you
view the whole page or a part of the page in a program's window.

The units of the page can be pixels or other units of measure (such as
inches or millimeters) or anything else that is convenient for the
application. The lower-left corner of the page usually coincides with the
lower-left corner of device space (the window), but it doesn't have to.

The presentation page maps to device space through the device transform.
This is the transform that allows a program to work in units other than

When you create a presentation space by calling the function
GpiCreatePS, the device transform is set for you. In the GpiCreatePS
function, you specify the units of the presentation page (for example, by
using the identifier PU_LOMETRIC for units of 0.1 millimeter or the
identifier PU_LOENGLISH for units of 0.01 inch) and the size of the page
in those units.

The size of the page in page units is specified in a SIZEL structure that
is then passed to GpiCreatePS. The SIZEL structure has two fields named
cx and cy. For example, if you decide to use PU_LOENGLISH page units and
you want the page to be 81/2 by 11 inches, set cx to 850 and cy to 1100.
If you use 0 values in the SIZEL structure, the size of the page is set
equal to the size of the output medium. For a window on the display, this
is the size of the screen.

The device transform is defined in terms of a RECTL (rectangle) structure.
After you call GpiCreatePS to create a presentation space, you can obtain
the device transform rectangle by calling

  GpiQueryPageViewport ( hps, &rcl) ;

where rcl is a structure of type RECTL. You will find that the values are
set as is shown in Figure 5.

The device transform formulas, shown in Figure 6, are rather ugly. The
rcl variable (a structure of type RECTL) is the device transform
rectangle, and sizl (a structure of type SIZEL) contains the page size in
page units.

If the xLeft and yBottom fields of the RECTL structure are 0 (as they are
when you first create the presentation space), then these two formulas
reduce to those shown in Figure 7.

This transform does exactly what we expect: it transforms the page units
(which could be 0.1 millimeter or 0.01 inch) into pixels by multiplying them
by a factor that relates those units to the pixel size. This type of
transform is known as "scaling." The complete formula also allows for

The size of the page that you specify in the GpiCreatePS function does
not affect the device transform. Whatever page size you choose, GPI will
calculate a device transform appropriate to it. However, the page size
affects clipping because the graphics field is initially set to the size of
the page.

If you create a presentation space using GpiCreatePS, you will probably
want to leave the device transform alone. However, you can do some tricks
with it, such as setting different scaling factors or translating the
origin of the presentation page to someplace other than the lower-left
corner of the window.

For example, if you wish to put the origin in the middle of the window,
first obtain the device transform rectangle by calling
GpiQueryPageViewport, add half the width of the client window to the
xLeft and xRight fields and half the height of the client window to the
yBottom and yTop fields, and then set the new device transform by calling
GpiSetPageViewport. You'll also need to change the graphics field
clipping area to encompass the whole window in page units.

Windows programmers should note that the function GpiSetPageViewport
does the job of several Windows functions: SetViewportOrg,
SetWindowOrg, and SetViewportExt, in addition to SetWindowExt.
However, it is not quite as easy to use as the Windows functions.

Cached Micro-PS

If you use a cached micro-PS, the presentation page is always in units of
pixels. You can use the device transform to change that. For example,
suppose you want to draw in units of 0.01 inch. You first obtain a handle
to the window device context:

  hdc = WinOpenWindowDC(hwnd) ;

You then make four calls to DevQueryCaps to determine the dimensions of
the screen in pixels and the number of pixels per meter:

  DevQueryCaps (hdc, CAPS_WIDTH, 1L, &xScreenPixels) ;
  DevQueryCaps (hdc, CAPS_HEIGHT, 1L, &yScreenPixels) ;
  DevQueryCaps (hdc, CAPS_VERTICAL_RESOLUTION, 1L, &yPixelsPerMeter) ;
  DevQueryCaps (hdc, CAPS_HORIZONTAL_RESOLUTION, 1L, &xPixelsPerMeter) ;

Because the size of the cached micro-PS page is the size of the screen,
you define the fields of a RECTL structure like this:

  rcl.xLeft  = 0 ;
  rcl.yBottom = 0 ;
  rcl.xRight =
     ((xScreenPixels - 1) * xPixelsPerMeter * 254 + 500000) / 1000000 ;
  rcl.yTop  =  ((yScreenPixels - 1) * yPixelsPerMeter * 254
     + 500000) / 1000000 ;

The ratio 254/1,000,000 converts meters to 0.01 inch. The number 500,000
is for rounding. Thus, the xRight and yTop fields are set to the width and
height of the screen in 0.01 inch. You then call:

  GpiSetPageViewport (hps, &rcl) ;

You also need to set the graphics field to the size of the window in
page units. To obtain this, you can use the GpiConvert function to convert
from device space to page coordinates:

  WinQueryWindowRect (hps, &rcl) ;
  GpiConvert (hps, CVTC_DEVICE, CVTC_PAGE, 2L, (PPOINTL) &rcl) ;
  GpiSetGraphicsField (hps, &rcl) ;

And you're done.

Obviously the device transform is not easy to work with. It's easiest to
create a micro-PS using GpiCreatePS and specify the page size and page
units you want. That defines the device transform once and for all.

Constructing the Picture

If you don't use a transform, other than the device transform as it is
set by GpiCreatePS, then everything you draw using GPI functions is in
page units-the units you selected in the GpiCreatePS function. (Actually,
there are some exceptions: GPI regions are always defined in device
units, for example.) The origin is located in the lower-left corner of
the window. Increasing values of x move to the right; increasing values of
y move up.

As you construct a picture on the presentation page, you also have
available two additional transforms: the modeling transform and the
default viewing transform. The points you use in GPI functions are in
world coordinate space. The modeling transform converts those points to
model coordinate space. The default viewing transform converts the points
in model space to points in the presentation page space.

For some simple purposes, the modeling and default viewing transforms are
interchangeable. But a general rule to follow is this: the transforms
closest to the hardware (that is, the transforms at the bottom of
Figure 4) should be changed less frequently than the transforms furthest
from the hardware. The simplest approach is to use the default viewing
transform to apply to the whole picture and different modeling transforms
to apply to pieces of the picture.

Suppose you want to work with a coordinate system in which the center of
the window is the origin. You use the default viewing transform to
accomplish that. (The TRANSFRM program discussed later does exactly
that.) Suppose you want to draw a picture in which a particular object
appears in several different places. You can draw the object with
different modeling transforms. The same points are passed to the GPI
drawing functions, but the modeling transform causes those points to be

Transformation Matrix

Both the modeling transform and the default viewing transform are based
on a standard two-dimensional "transformation matrix" common in many
high-level graphics languages such as GKS (Graphical Kernel System).

In GPI, you specify a transformation matrix as a structure of type
MATRIXLF. (The LF part of the structure name stands for LONG and FIXED,
the two data types used for the structure fields.) The MATRIXLF structure
is defined in the PMGPI.H header file, like this:

  typedef struct _MATRIXLF
    FIXED fxM11 ;
    FIXED fxM12 ;
    LONG lM13 ;
    FIXED fxM21 ;
    FIXED fxM22 ;
    LONG lM23 ;
    LONG lM31 ;
    LONG lM32 ;
    LONG lM33 ;

At first glance, the jumble of numbers in the field names looks like a
real mess. But it's actually quite simple. The nine fields of this
structure are the nine elements of a 3-by-3 matrix:

  │ fxM11  fxM12  lM13 │
  │                    │
  │ fxM21  fxM22  lM23 │
  │                    │
  │ lM31   lM32   lM33 │

The M in the field names stands for "matrix