PCjs Machines

Home of the original IBM PC emulator for browsers.

Logo

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
V/386

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.


EDITOR'S NOTE

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.


Masthead

JONATHAN D. LAZARUS
Editor and Publisher

EDITORIAL

TONY RIZZO
Technical Editor

CHRISTINA G.DYAR
Associate Editor

JOANNE STEINHART
Production Editor

GERALD CARNEY
Staff Editor

KIM HOROWITZ
Editorial Assistant

ART

MICHAEL LONGACRE
Art Director

VALERIE MYERS
Associate Art Director

CIRCULATION

WILLIAM B. GRANBERG
Circulation Manager

L. PERRIN TOMICH
Assistant to the Publisher

DONNA PUIZINA
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
environments.

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

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

"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
user."


───────────────────────────────────────────────────────────────────────────
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
shipped.

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

   ╔═P═╗
   ║ 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
              box

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


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
WM_KEYDOWN, along with WM_KEYUP, WM_SYSKEYDOWN, and WM_SYSKEYUP have all
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.


Initialization

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

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;
WM_CREATE, WM_DESTROY, WM_SIZE, WM_PAINT, WM_TIMER, and WM_COMMAND. One very
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
provide.)

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

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


Conclusion

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

#  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

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

SWP.LNK

swp
saywhatp/align:16
saywhatp/map
wincalls doscalls mlibc286/NOD
swp.def


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

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


NAME    SayWhat


DESCRIPTION 'Say What!'


STUB    'WINSTUB.EXE'

CODE    MOVEABLE
DATA    MOVEABLE MULTIPLE

HEAPSIZE  128
STACKSIZE 4096

EXPORTS
    SayAboutDlgProc     @1
    SayWhatDlgProc      @2
    SayWhatWndProc      @3


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

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

NAME    SayWhat

DESCRIPTION 'Say What!'

STUB    'OS2STUB.EXE'

CODE    MOVEABLE
DATA    MOVEABLE MULTIPLE

HEAPSIZE  128
STACKSIZE 8192

EXPORTS
    SayAboutDlgProc     @1
    SayWhatDlgProc      @2
    SayWhatWndProc      @3


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

SW.RC

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

STRINGTABLE
BEGIN
    STR_NAME,    "SayWhat!"
    STR_TITLE,   "Say What!"
    STR_WHAT,    "Hello Windows!"
END

SayWhat!    MENU
BEGIN

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

END

DLG_ABOUT DIALOG 19, 17, 130, 83
STYLE WS_DLGFRAME | WS_POPUP
BEGIN
  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
END

DLG_WHAT DIALOG 49, 41, 177, 103
CAPTION "Say What!"
STYLE WS_CAPTION | WS_SYSMENU | WS_VISIBLE | WS_POPUP
BEGIN
 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 "&Flicker"     ITEM_FLICKER,  "Button",    BS_AUTORADIOBUTTON,
 CONTROL "Enter"        IDOK,          "Button",    BS_DEFPUSHBUTTON | WS_GRO
 CONTROL "Esc=Close"    IDCANCEL,      "Button",    BS_PUSHBUTTON | WS_TABSTO
END


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

SWP.RC

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

STRINGTABLE
{
  STR_NAME,    "SayWhat!"
  STR_TITLE,   "Say What!"
  STR_WHAT,    "Hello Windows!"
}

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

DLGTEMPLATE DLG_ABOUT
{
  DIALOG "", DLG_ABOUT, 19, 17, 130, 83, FS_DLGBORDER |
WS_SAVEBITS | WS_VISIBLE
  {
    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
  }
}

DLGTEMPLATE DLG_WHAT
{
  DIALOG "Say What!", DLG_WHAT, 49, 41, 177, 103,
FS_TITLEBAR | FS_SYSMENU | FS_BORDER | WS_VISIBLE
  {
  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 */
#endif

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

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

    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(
                hDC,
                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 )
        return;

    SetScrollPos( hWndBar, SB_CTL, nPos, TRUE );

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

    switch( idBar )
    {
      case ITEM_DISTBAR:
        nDistance = nPos;
        break;

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

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

    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;

    LoadString(
        hInstance, STR_NAME,  szAppName, sizeof(szAppName)
    );
    LoadString(
        hInstance, STR_TITLE, szTitle,   sizeof(szTitle)
    );
    LoadString(
        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 {

        GetInstanceData(
            hPrevInstance, (NPSTR)&hbrBkgd, sizeof(hbrBkgd)
        );
    }

    hWndWhat = CreateWindow(
        szAppName,
        szTitle,
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, 0,
        CW_USEDEFAULT, 0,
        (HWND)NULL,
        (HMENU)NULL,
        hInstance,
        (LPSTR)NULL
    );

    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 {
        SetDlgItemInt(
            hWndDlg,
            idBar+1,
            GetScrollPos( hWndBar, SB_CTL ),
            FALSE
        );
    }
}

/*  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 );
            GetDlgItemText(
                hWndDlg, ITEM_WHAT, szText, sizeof(szText)
            );
            if( strlen(szText) = = 0 )
                LoadString(
                    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) )
            SayDoBarMsg(
                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;
        SetWindowPos(
            hWndDlg,
            (HWND)NULL,
            rcWin.left,
            rcWin.top,
            0, 0,
            SWP_NOSIZE | SWP_NOZORDER |
                SWP_NOREDRAW | SWP_NOACTIVATE
        );
        SetDlgItemText( hWndDlg, ITEM_WHAT, szText );
        SendDlgItemMessage(
            hWndDlg, ITEM_WHAT,
            EM_LIMITTEXT, sizeof(szText)-1, 0L
        );
        SayInitBar(
            hWndDlg, ITEM_INTBAR,
            nInterval, MIN_INTERVAL, MAX_INTERVAL
        );

        SayInitBar(
            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;
{
    PAINTSTRUCT ps;

    BeginPaint( hWnd, &ps );

    SetTextColor( ps.hdc, rgbTextColor );

    SayLimitTextPos( hWnd );

    if( bCleanPaint ) {

        /* Clean painting, avoid redundant erasing */

        TextOut(
            ps.hdc,
            rcText.left,
            rcText.top,
            pText,
            bIconic ? 1 : strlen(szText)
        );

        SayFillRect(
            ps.hdc,
            ps.rcPaint.left,
            ps.rcPaint.top,
            rcText.left,
            ps.rcPaint.bottom
        );

        SayFillRect(
            ps.hdc,
            rcText.left,
            ps.rcPaint.top,
            rcText.right,
            rcText.top
        );

        SayFillRect(
            ps.hdc,
            rcText.left,
            rcText.bottom,
            rcText.right,
            ps.rcPaint.bottom
        );

        SayFillRect(
            ps.hdc,
            rcText.right,
            ps.rcPaint.top,
            ps.rcPaint.right,
            ps.rcPaint.bottom
        );

    } else {

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

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

        TextOut(
            ps.hdc,
            rcText.left,
            rcText.top,
            pText,
            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 );
            DialogBox(
                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(
                    hInstance,
                    MAKEINTRESOURCE(DLG_WHAT),
                    (HWND)NULL,
                    lpfnDlgProc
                );
                if( ! hWndPanel )
                    FreeProcInstance( lpfnDlgProc );
            }
        }
        break;

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

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

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

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

          default:
            return 0L;
        }
        SayInvalidateText( hWnd );
        nDistLeft = nDistance;
        return 0L;

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

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

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

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

          case TIMER_CHAR:
            SayAdvanceTextChar( hWnd );
            break;
        }
        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 ) )
            continue;

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

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

ULONG FAR PASCAL SayAboutDlgProc( HWND, USHORT, ULONG, ULONG );
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 );
ULONG FAR PASCAL SayWhatDlgProc( HWND, USHORT, ULONG, ULONG );
VOID             SayWhatPaint( HWND );
ULONG FAR PASCAL SayWhatWndProc( HWND, USHORT, ULONG, ULONG );
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 );
            break;
        }
    }
    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 )
        return;

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

    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(
            hPS,
            WinQuerySysColor( hAB, SCLR_WINDOWTEXT ),
            0L
        );

    } else {
        lWindow = GpiQueryColorIndex(
            hPS,
            WinQuerySysColor( hAB, SCLR_WINDOW ),
            0L
        );
        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 =
        (SHORT)WinSendDlgItemMsg(
            hWndDlg, idBar, SBM_QUERYPOS, 0L, 0L
        );

    lRange =
        WinSendDlgItemMsg(
            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 )
        return;

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

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

    switch( idBar )
    {
      case ITEM_DISTBAR:
        nDistance = nPos;
        break;

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

        WinInvalidateRect( hWndWhat, NULL );
        break;
    }
}

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

        return;

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

    WinFillRect(
        hPS,
        &rcFill,
        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;

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

    bOK = WinRegisterClass(
        hAB,
        szAppName,
        (LPFNWP)SayWhatWndProc,
        CS_SYNCPAINT,
        0,

        NULL
    );
    if( ! bOK )
        return FALSE;

    hWndWhatFrame = WinCreateStdWindow(
        (HWND)NULL,
        FS_TITLEBAR | FS_SYSMENU |
            FS_MENU | FS_MINMAX | FS_SIZEBORDER,
        szAppName,
        szTitle,
        0L,
        (HMODULE)NULL,
        MENU_WHAT,
        &hWndWhat
    );

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

    WinSendDlgItemMsg(
        hWndDlg,
        idBar,
        SBM_SETSCROLLBAR,

        (LONG)nValue,
        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 =
        WinSendDlgItemMsg(
            hWndDlg, idBar, SBM_QUERYRANGE, 0L, 0L
        );

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

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

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

                WinWindowFromID( hWndDlg, ITEM_WHAT ),
                sizeof(szText),
                szText
            );
            if( strlen(szText) = = 0 )
                WinLoadString(
                    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 );
            break;

          case IDCANCEL:
            WinDestroyWindow( hWndDlg );
            break;

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

                hWndDlg, ITEM_CLEAN,
                BM_QUERYCHECK, 0L, 0L
            );
            break;
        }
        break;

    case WM_DESTROY:
        hWndPanel = NULL;
        break;

    case WM_HSCROLL:
        SayDoBarMsg(
            hWndDlg, LOUSHORT(lParam1),
            HIUSHORT(lParam2), LOUSHORT(lParam2)
        );
        break;

    case WM_INITDLG:
        WinQueryWindowRect( hWndDlg, &rcWin );
        WinMapWindowPoints(
            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;
        WinSetWindowPos(
            hWndDlg,
            (HWND)NULL,
            rcWin.xLeft,
            rcWin.yBottom,
            0, 0,
            SWP_MOVE
        );
        WinSetWindowText(
            WinWindowFromID( hWndDlg, ITEM_WHAT ), szText
        );
        WinSendDlgItemMsg(
            hWndDlg, ITEM_WHAT, EM_SETTEXTLIMIT,
            (LONG)sizeof(szText)-1, 0L
        );
        SayInitBar(
            hWndDlg, ITEM_INTBAR,  nInterval,
            MIN_INTERVAL, MAX_INTERVAL
        );
        SayInitBar(
            hWndDlg, ITEM_DISTBAR, nDistance,
            MIN_DISTANCE, MAX_DISTANCE
        );
        WinSendDlgItemMsg(
            hWndDlg, ITEM_CLEAN, BM_SETCHECK, (LONG)TRUE, 0L
        );
        break;
    }
    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 );

        GpiCharStringAt(
            hPS,
            &gptText,
            (LONG)( bIconic ? 1 : strlen(szText) ),
            npszText
        );

        SayFillRect(
            hPS,
            rcPaint.xLeft,
            rcPaint.yBottom,
            rcText.xLeft,
            rcPaint.yTop
        );

        SayFillRect(
            hPS,
            rcText.xLeft,
            rcText.yTop,
            rcText.xRight,
            rcPaint.yTop
        );

        SayFillRect(
            hPS,
            rcText.xLeft,
            rcPaint.yBottom,
            rcText.xRight,
            rcText.yBottom
        );

        SayFillRect(
            hPS,
            rcText.xRight,
            rcPaint.yBottom,
            rcPaint.yRight,
            rcPaint.yTop
        );

     } else {

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

        WinFillRect(
            hPS,
            &rcPaint,
            WinQuerySysColor( hAB, SCLR_WINDOW )
        );

        GpiCharStringAt(
            hPS,
            &gptText,
            (LONG)( bIconic ? 1 : strlen(szText) ),
            npszText
        );
     }

     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 )
            break;
        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 )
            break;
        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:
        if(
            ( LOUSHORT(lParam1) & KC_KEYUP )  ||
            ! ( LOUSHORT(lParam1) & KC_VIRTUALKEY )

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

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

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

          case CMD_EXIT:

            WinDestroyWindow( hWndWhatFrame );
            return 0L;

          case CMD_WHAT:
            if( hWndPanel ) {
                WinSetWindowPos(
                    hWndPanel,
                    HWND_TOP,
                    0, 0, 0, 0,
                    SWP_ZORDER | SWP_ACTIVATE
                );
            } else {
                hWndPanel = WinLoadDlg(
                    (HWND)NULL,
                    (HWND)NULL,
                    (LPFNWP)SayWhatDlgProc,
                    NULL,
                    DLG_WHAT,
                    NULL
                );
            }
        }

        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;

      case WM_ERASEBACKGROUND:
        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 );
            else
                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 );
                break;
            case TIMER_CHAR:
                SayAdvanceTextChar( hWnd );
                break;

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

            continue;

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

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


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

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


Portability

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.


Performance

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.


Conclusions

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

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

main()
{
 int i, j;
 int fd;

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

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


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

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

 /* Remove the File */
 unlink('scratch');
 exit(0);
}


Figure 6:  Cpubound.c

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

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

main()
{
 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);
    }
 }
 exit(0);
}
 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-);

  return(t);
 }


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

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.


Using HEXCALC

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

  MAKE HEXCALCW

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_OVERLAPPED, WS_CAPTION, WS_SYSMENU, and WS_MINIMIZEBOX. Except for
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)
high.

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

  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
    STYLE WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZE
    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",
    WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZE,
    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
2PM-6PM.

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

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
     STYLE WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX
     CLASS "HexCalcW"
     CAPTION "Hex Calculator"
     BEGIN
          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
     END


Figure 3PM:  HEXCALCP.RC Resource Script

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

WINDOWTEMPLATE ID_HEXCALC
    {
    FRAME "Hex Calculator", 1, 110, 40, 102, 122, WS_VISIBLE |
         FS_TITLEBAR | FS_SYSMENU | FS_MINBUTTON | FS_BORDER
        {
        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'
CODE           MOVEABLE
DATA           MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      4096
EXPORTS        WndProc


Figure 4PM:  HEXCALCP.DEF Module Definition File

NAME           HEXCALCP
DESCRIPTION    'Hexadecimal Calculator(C) Charles Petzold
1987'
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) ;
                    }
               else
                    {
                    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)) ;
                    else
                         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) ;
                    }
               else
                    {
                    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)) ;
                    else
                         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"

ULONG EXPENTRY ClientWndProc (HWND, USHORT, ULONG, ULONG) ;

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

      hWnd = WinLoadDlg (HWND_DESKTOP, HWND_DESKTOP, NULL, 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 :
 ULONG_MAX ;
           case '%' : return lNum ? lFirstNum % lNum :
 ULONG_MAX ;
           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) ;
                else
                     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)) ;
                     else
                          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
output.

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

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

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

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

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

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

  while(p++<q)
     ∙
     ∙
     ∙
  for(p=head;p<tail;p++)
    ∙
    ∙
    ∙

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

main()
{
    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

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

main(c,v)
char *v[];
{
     unsigned nelems;
     char far * f1, far * f2;

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

     f1 = (char far *)_fmalloc(nelems * sizeof(char));
     if (f1 == (char far *)0) {
         perror("_falloc failed:");
         exit();
     }
     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",
            (long)(unsigned)(f2-f1));
}


Figure 4:  Pointer Arithmetic Using Huge Pointers

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

main(c,v)
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]);
         exit();
     }
     nelems = atol(v[1]);

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

     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",
            (unsigned)(long)(h2-h1));
     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
            long)(h2-h1));
     printf("7. %12ld == (long)(unsigned)(h2-h1)\n",
            (long)(unsigned)(h2-h1));
}


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

main()
{
     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
                        long)FP_OFF(p))

#define NELEM 5

main()
{
     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
#else
#define PTYPE
#define MALLOC malloc(NELEM * sizeof(unsighned) )
#define DTYPE unsigned
#endif

main()
{
    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 *
huge2far(n)
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
applications.

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

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

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

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

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


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

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

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
Windows/386.

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:

  [kernel]
  EnableEMSDebug=1

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.


Conclusion

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       │
                                             ├─────────────────┤─┐16Kb
                                             │      L-14       │ │logical
                                             ├─────────────────┤─┘page
                                             │      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
                        Space


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

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

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

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


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

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

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
system/environment.

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

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

The Disable OS/E Function Set subfunction disables Functions 26, 28, and
30.

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

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

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

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

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

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

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
                                registers.
Function Name              No.  Description
                                registers.

PREPARE EXPANDED          29    Prepares the expanded memory MEMORY HARDWARE
                                FOR hardware for an impending WARM BOOT warm
                                boot.

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,
      emm_handle_name);
   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
           NUMBER.

Using EMM Function 7 GET.VERSION NUMBER

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");
       exit(1);
     }
   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
           handle.

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

   return(0);

}


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;
   else
     log_phys_pages->log_page_number = 0xFFFF ;

   log_phys_pages++ ;

 }

 inregs.x.ax = MAP_UNMAP_MULTIPLE_PAGES ;
 inregs.x.dx = handle;
 inregs.x.cx = 4;
 inregs.x.si = FP_OFF(temp_ptr);
 segregs.ds  = FP_SEG(temp_ptr);
 int86x(EMM_INT,&inregs,&outregs,&segregs);
 if (outregs.h.ah != 0) return(1);
 return(0);
}


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

   check_emm_version_number();

   search_for_handle(emm_handle_name, &emm_handle);

   /* Exchange screens*/
   move_exchg_to_expanded(EXCHANGE_MEMORY_REGION,video_ram_ptr,
                          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 */
   move_exchg_to_expanded(EXCHANGE_MEMORY_REGION,video_ram_ptr,
                          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 ()
{


   check_emm_version_number();

   result = get_expanded_memory(&emm_ptr, pages_needed, &emm_handle,
                                emm_handle_name);
   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);

   exit(0);
}


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,
                 handle)
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);
  int86x(EMM_INT,&inregs,&outregs,&segregs);
  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.

DOSSEG

data SEGMENT PUBLIC 'DATA'
Data  ends

CODE SEGMENT PUBLIC 'CODE'
     ASSUME CS:CODE, DS:DATA
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
exit:
     pop     ds
     ret
start endp
CODE ENDS
data SEGMENT PUBLIC 'DATA'
;
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)

DOSSEG

data SEGMENT PUBLIC 'DATA'
Data  ends

CODE SEGMENT PUBLIC 'CODE'
     ASSUME CS:CODE, DS:DATA
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
     ret
start endp
CODE ENDS

data SEGMENT PUBLIC 'DATA'
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
Format

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.


EDITOR'S NOTE

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

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.

Masthead

JONATHAN D. LAZARUS
Editor and Publisher

EDITORIAL

TONY RIZZO
Technical Editor

CHRISTINA G.DYAR
Associate Editor

JOANNE STEINHART
Production Editor

GERALD CARNEY
Staff Editor

KIM HOROWITZ
Editorial Assistant

ART

MICHAEL LONGACRE
Art Director

VALERIE MYERS
Associate Art Director

CIRCULATION

WILLIAM B. GRANBERG
Circulation Manager

L. PERRIN TOMICH
Assistant to the Publisher

DONNA PUIZINA
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
units.

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


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


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

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


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 •        │                       │
         7E►└───────────────────────┘
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
program.

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

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

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

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


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.


Semaphores

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


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

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


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"

MENU ID_RESOURCE
     {
     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
          MENUITEM "~Abort",  IDM_ABORT, MIA_DISABLED
          }
     }

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

ULONG EXPENTRY ClientWndProc (HWND, USHORT, ULONG, ULONG) ;

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

#endif


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,
                    WS_VISIBLE | FS_SIZEBORDER | FS_TITLEBAR
                               | 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 ;

                    default:
                         break ;
                    }
               break ;

          case WM_PAINT:
               PaintWindow (hwnd, iStatus, iCalcRep, lElapsedTime) ;
               break ;

          default:
               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,
                    WS_VISIBLE | FS_SIZEBORDER | FS_TITLEBAR
                               | 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 ;

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

          default:
               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,
                    WS_VISIBLE | FS_SIZEBORDER | FS_TITLEBAR
                               | 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,
                                                  PM_NOREMOVE))
                                   {
                                   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 ;
                         else
                              iStatus = STATUS_DONE ;

                         WinInvalidateRect (hwnd, NULL) ;

                         EnableMenuItem (hwnd, IDM_START, TRUE) ;
                         EnableMenuItem (hwnd, IDM_ABORT, FALSE) ;
                         break ;

                    case IDM_ABORT:
                         bContinueCalc = FALSE ;
                         break ;

                    default:
                         break ;
                    }
               break ;

          case WM_PAINT:
               PaintWindow (hwnd, iStatus, iCalcRep, lElapsedTime) ;
               break ;

          default:
               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,
                    WS_VISIBLE | FS_SIZEBORDER | FS_TITLEBAR
                               | 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
                                    cThreadStack))

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

                    case IDM_ABORT:
                         bContinueCalc = FALSE ;

                         EnableMenuItem (hwnd, IDM_ABORT,
                                         FALSE) ;
                         break ;

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

          default:
               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) ;
          else
               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
               ObjectWndProc

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)
#define WM_OBJECT_DESTROYED   (WM_USER + 5)

VOID  FAR SecondThread (VOID) ;
ULONG EXPENTRY ObjectWndProc (HWND, USHORT, ULONG, ULONG) ;

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,
                    WS_VISIBLE | FS_SIZEBORDER | FS_TITLEBAR
                               | 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 ;

                    default:
                         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) ;
               else
                    WinPostMsg (hwnd, WM_QUIT, 0L, 0L) ;
               break ;

          case WM_OBJECT_DESTROYED:

               WinPostMsg (hwnd, WM_QUIT, 0L, 0L) ;
               break ;

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

          default:
               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
configuration.

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.


APIs

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

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.


Protocol-Independent

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.


Backward-Compatible

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.


IPC

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.


Portability

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


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


Auditing

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.


Security

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.


Administration

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.


Performance

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.


Tradeoffs

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

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


Trends

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
Messaging
Alerter
Error log
Network error reporting
Network statistics

Input/Output Devices and Pipes
Named pipes (redirected pipes)
Spoolers
Character devices
NetBIOS
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
           network.

                                             User
  ╔══════════════╗                  ┌──────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)

NetBIOS API Call

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 */
  DosConnectNmPipe(handle)
  DosRead(handle, RpcReqBuf)
  Do the remote procedure call
  DosWrite(handle, RpcRespBuf)
  DosDisconnectNmPipe(handle)
endloop

NetBIOS API Call

Begin_Loop
  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
    GotoBegin_Loop
  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
End_Loop

████████████████████████████████████████████████████████████████████████████

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

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


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

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

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

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


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.


Summary

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


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

 offset
      0┌──────────────────────────────────────────┐
       │    link to next driver header, offset    │
      2├──────────────────────────────────────────┤
       │   link to next driver header, selector   │
      4├──────────────────────────────────────────┤
       │          device attribute word           │
      6├──────────────────────────────────────────┤
       │       offset, strategy entry point       │
      8├──────────────────────────────────────────┤
       │                 reserved                 │
    0AH├──────────────────────────────────────────┤
       │                                          │
       │         name or units (8 bytes)          │
       │                                          │
    12H├──────────────────────────────────────────┤
       │                                          │
       │            reserved (8 bytes)            │
       │                                          │
       └──────────────────────────────────────────┘


Figure 3:  The device attribute word in the device driver header, which
           describes the characteristics and capabilities of the associated
           driver.

╓┌─────────┌─────────────────────────────────────────────────────────────────╖
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.

      0┌──────────────────────────────────────────┐
       │         length of request header         │
      1├──────────────────────────────────────────┤
       │         block device unit number         │
      2├──────────────────────────────────────────┤
       │    command code (driver subfunction)     │
      3├──────────────────────────────────────────┤
       │             returned status              │
       │                                          │
      5├──────────────────────────────────────────┤
       │         reserved area (4 bytes)          │
       │                                          │
      9├──────────────────────────────────────────┤
       │         queue linkage (4 bytes)          │
       │                                          │
    0DH├──────────────────────────────────────────┤
       │                                          │
       │  command-specific data (length varies)   │
       │                                          │
       └──────────────────────────────────────────┘


Figure 5:  Command Codes and Their Associated Driver Functions

╓┌────────┌──────────────────┌───────────────────────────────────────────────►
Command
Code     Subfunction Name   Subfunction Action
Command
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
Command
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
                            possible.
Command
Code     Subfunction Name   Subfunction Action
                            possible.

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

Command
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
Command
Code     Subfunction Name   Subfunction Action
                            another driver with the same logical device
                            name.

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

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

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

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

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

QueueFlush           10H    K  I  U     Resets the pointers to the specified
                                        buffer.

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

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

DevHlp               DevHlp             DevHlp
Name                 Code   K  I  U  N  Action

DeRegister           21H    K           Removes a jprocess from a monitor
                                        chain.

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

SemClear             07H    K  I  U     Clears a semaphore, restarting any
                                        threads that were blocking on the
                                        semaphore.
DevHlp               DevHlp             DevHlp
Name                 Code   K  I  U  N  Action
                                        semaphore.

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

GetDOSVar            24H    K        N  Returns a bimodal pointer to a table
                                        of useful kernel addresses and
                                        variables.

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

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'
.286c

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

        ret

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
        ret

MediaChk endp

BuildBPB proc   near            ; function 2 = Build BPB

        mov     ax,0100h        ; return 'done' status
        ret

BuildBPB endp

Read    proc    near            ; function 4 = Read (Input)

        mov     ax,0100h        ; return 'done' status
        ret

Read    endp

NdRead  proc    near            ; function 5 = Nondestructive Read

        mov     ax,0100h        ; return 'done' status
        ret

NdRead  endp

InpStat proc    near            ; function 6 = Input Status

        mov     ax,0100h        ; return 'done' status
        ret

InpStat endp

InpFlush proc   near            ; function 7 = Flush Input Buffers

        mov     ax,0100h        ; return 'done' status
        ret

InpFlush endp

Write   proc    near            ; function 8 = Write (Output)

        mov     ax,0100h        ; return 'done' status
        ret

Write   endp

WriteVfy proc   near            ; function 9 = Write with Verify

        mov     ax,0100h        ; return 'done' status
        ret

WriteVfy endp

OutStat proc    near            ; function 10 = Output Status

        mov     ax,0100h        ; return 'done' status
        ret

OutStat endp

OutFlush proc   near            ; function 11 = Flush Output Buffers

        mov     ax,0100h        ; return 'done' status
        ret

OutFlush endp

DevOpen proc    near            ; function 13 = Device Open

        mov     ax,0100h        ; return 'done' status
        ret

DevOpen endp

DevClose proc   near            ; function 14 = Device Close

        mov     ax,0100h        ; return 'done' status
        ret

DevClose endp

RemMedia proc   near            ; function 15 = Removable Media

        mov     ax,0100h        ; return 'done' status
        ret

RemMedia endp

GenIOCTL proc   near            ; function 16 = Generic IOCTL

        mov     ax,0100h        ; return 'done' status
        ret

GenIOCTL endp

ResetMed proc   near            ; function 17 = Reset Media

        mov     ax,0100h        ; return 'done' status
        ret

ResetMed endp

GetLogDrv proc  near            ; function 18 = Get Logical Drive

        mov     ax,0100h        ; return 'done' status
        ret

GetLogDrv endp

SetLogDrv proc  near            ; function 19 = Set Logical Drive

        mov     ax,0100h        ; return 'done' status
        ret

SetLogDrv endp

DeInstall proc  near            ; function 20 = DeInstall Driver

        mov     ax,0100h        ; return 'done' status
        ret

DeInstall endp

PartFD  proc    near            ; function 22 = Partitionable
                                ;               Fixed Disks
        mov     ax,0100h        ; return 'done' status
        ret

PartFD  endp

FDMap   proc    near            ; function 23 = Get Fixed Disk
                                ;               Logical Unit Map
        mov     ax,0100h        ; return 'done' status
        ret

FDMap   endp

Error   proc    near            ; bad command code in request header

        mov     ax,8103h        ; error bit + 'done' status
                                ; + "Unknown Command" code
        ret

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
        ret

Init    endp

_TEXT   ends

        end


Figure 9:  Sample Module Definition File

;
; module definition file for
; TEMPLATE OS/2 character device driver
;
LIBRARY TEMPLATE
PROTMODE


Figure 10:  MASM TEMPLATE

MASM TEMPLATE;
LINK
TEMPLATE,TEMPLATE.SYS,,DOSCALLS,TEMPLATE;


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   │◄───┘
                                                    └────────────────┘

                              StartNextRequest
                                     │
                          ┌──────────▼────────────┐
                          │      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
Microsoft.

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

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

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

  main()
  {
      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).


Terminology

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

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.


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

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

  ■  Binary data (executable code and program data) is contained in the
     LEDATA and LIDATA records.

  ■  Address binding and relocation information is contained in FIXUPP
     records.

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

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


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

                                END


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

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

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

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

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.


Documentation

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


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

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

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.


Help

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

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


Macros

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


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.


Speed

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

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.


Conclusions

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

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.


Documentation

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


Macros

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,

  mymacro()
    {
    int   bufid;
    bufid = create_buffer("tempbuf");
    show_buffer(bufid);
           ∙
           ∙
           ∙
    close_window();
    delete_buffer(bufid);
    }

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

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


Printing

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.


Documentation

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.


Setup

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

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


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

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

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

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.


Printing

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


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

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

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.


Conclusion

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;

init()
{
  assign_key("compile",    ALT_C);
  assign_key("next_error", CTRL_N);
}

compile()
{
  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!!!    */
      return;
    }
  }
  else            /* the buffer has no extension, so it's an error */
  {
    get_tty_str(errmsg);
    return;
  }

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


next_error()
{
  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
internally.


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

  create_database("my_db");

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.


───────────────────────────────────────────────────────────────────────────
DEVELOPERS
───────────────────────────────────────────────────────────────────────────

BRIEF
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

KEDIT
Mansfield Software Group, Inc.
P.O. Box 432
Storrs, CT 06268
(203) 429-8402

ME
Magma Software Systems
138-23 Hoover Ave.
Jamaica, NY 11435
(201) 792-3954

MICRO/SPF
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

PC/VI
Custom Software Systems (CSS)
P.O. Box 678
Natick, MA 01760
(617) 653-2555

PMATE
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

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

XTC
Wendin
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.


EDITOR'S NOTE

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.


Masthead

JONATHAN D. LAZARUS
Editor and Publisher

EDITORIAL

TONY RIZZO
Technical Editor

KAREN STRAUSS
Assistant Editor

JOANNE STEINHART
Production Editor

GERALD CARNEY
Staff Editor

KIM HOROWITZ
Editorial Assistant

ART

MICHAEL LONGACRE
Art Director

VALERIE MYERS
Associate Art Director

CIRCULATION

WILLIAM B. GRANBERG
Circulation Manager

L. PERRIN TOMICH
Assistant to the Publisher

DONNA PUIZINA
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
product.

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.

"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
typing.

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


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

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


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


Subclassing

"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
structure."

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


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

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

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

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

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


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

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

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

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

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


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

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

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

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

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

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

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

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

NAME           CACHEDPS
DESCRIPTION   'Demonstrates Cached Micro-PS(C) Charles Petzold, 1988'
PROTMODE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        ClientWndProc

CACHEDPS.C-Demonstrates Cached Micro-PS

#define INCL_GPI

#include <os2.h>
#include <stddef.h>

MRESULT EXPENTRY ClientWndProc (HWND, USHORT, MPARAM, MPARAM) ;

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,
                    WS_VISIBLE | FS_SIZEBORDER | FS_TITLEBAR
                               | 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 ;

          default:
               return WinDefWindowProc (hwnd, msg, mp1, mp2) ;
          }
     return FALSE ;
     }


Figure 4:

MICROPS Make File

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'
PROTMODE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        ClientWndProc

MICROPS.C-Demonstrates Micro-PS

#define INCL_WIN
#define INCL_GPI

#include <os2.h>
#include <stddef.h>

MRESULT EXPENTRY ClientWndProc (HWND, USHORT, MPARAM, MPARAM) ;

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,
                   WS_VISIBLE | FS_SIZEBORDER | FS_TITLEBAR
                              | 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 ;

         default:
              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 Make File

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

NAME           NORMALPS
DESCRIPTION   'Demonstrates Normal-PS(C) Charles Petzold, 1988'
PROTMODE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS       ClientWndProc

NORMALPS.C-Demonstrates Normal-PS

#define INCL_WIN
#define INCL_GPI

#include <os2.h>
#include <stddef.h>

MRESULT EXPENTRY ClientWndProc (HWND, USHORT, MPARAM, MPARAM) ;

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,
                    WS_VISIBLE | FS_SIZEBORDER | FS_TITLEBAR
                               | 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 ;

          default:
               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     │            │█
  └──────────┴─────────────┴─────────────────┴────────────┴────────────┘█
    ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀


Serialization

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

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.


Signaling

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

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

  DosSemClear(&mutexSem);
  DosSemSet(&emptySem);
  DosSemClear(&fullSem);
  head=tail=0;

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

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.


Scheduling

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.


Conclusion

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))
  if( rc == ERROR_ALREADY_EXISTS)
       DosOpenSem( &sysSem, &semName) ;
  else
          <...error...>

<...use the semaphore...>

DosCloseSem( sysSem) ;


Figure 2:  Two Threads Serializing Access to a Resource

long resourceSem = 0;

thread1()
{
                                    ∙
                                    ∙
                                    ∙
     DosSemRequest( &resourceSem, -1L);

     <... read/modify resource ...>

     DosSemClear( &resourceSem);
                                    ∙
                                    ∙
                                    ∙
}

thread2()
{
                                    ∙
                                    ∙
                                    ∙
     DosSemRequest( &resourceSem, -1L);

     <... read/modify resource...>

     DosSemClear( &resourceSem);
                                    ∙
                                    ∙
                                    ∙
}


Figure 3:  ExitList Processing for a Fast-Safe Ram Semaphore

main()
{
                                    ∙
                                    ∙
                                    ∙
     DosExitList( 1, &Cleanup);    /* add to exit list */
                                    ∙
                                    ∙
                                    ∙
}

Cleanup()
{
   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);

thread1()
                                    ∙
                                    ∙
                                    ∙
     F1
     DosSemClear( sigSem);
                                    ∙
                                    ∙
                                    ∙
}

thread2()
                                    ∙
                                    ∙
                                    ∙
     DosSemWait( sigSem, -1L);
     F2
                                    ∙
                                    ∙
                                    ∙
}


Figure 5:  Using DosSemSetWait

thread1()
                                    ∙
                                    ∙
                                    ∙
     while(1)
     {
          F1
          DosSemClear( sigSem) ;
     }
                                    ∙
                                    ∙
                                    ∙
}

thread2()
                                    ∙
                                    ∙
                                    ∙
     while(1)
     {
          DosSemSetWait( segSem, -1L) ;
          F2
     }
                                    ∙
                                    ∙
                                    ∙
}


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;

thread1()
                                    ∙
                                    ∙
                                    ∙
     F1
     DosSemClear( sigSem1) ;
                                    ∙
                                    ∙
                                    ∙
}

thread2()
                                    ∙
                                    ∙
                                    ∙

     F2
     DosSemClear( sigSem2) ;
                                    ∙
                                    ∙
                                    ∙
}

thread3()
                                    ∙
                                    ∙
                                    ∙
     F3
     DosSemClear( sigSem3) ;
                                    ∙
                                    ∙
                                    ∙
}

thread4()
                                    ∙
                                    ∙
                                    ∙

     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

thread1()
                                    ∙
                                    ∙
                                    ∙
     <... 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 */
     if((head==tail)||((tail==0)&&(head==bufSize-1)))
          DosSemSet( &fullSem);    /* set if full */
     DosSemClear( &emptySem);      /* not empty */
     DosSemClear( &mutexSem);
                                    ∙
                                    ∙
                                    ∙
}

thread2()
                                    ∙
                                    ∙
                                    ∙
     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

P()
{
     <... wait until classicCountSem > 0 ...>
     classicCountSem-;
}

V()
{
     classicCountSem++;
}


Figure 9:  Simulating a Counting Semaphore under OS/2

 P()
 {
     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 */
     }
 }

 V()
 {
     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.

  >USE DYNAMIC-LINK LIBRARY

I see no DLL here.

  >MAKE DLL

I don't know how to make a DLL.

  >INVENTORY

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

  >USE TOOLS IN TOOLKIT TO MAKE DLL

Huh?

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

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

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

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


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

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

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

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


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

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

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

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

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

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

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

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.


Conclusion

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)            ║
│GLOBAL DATA SEGMENT DESCRIPTORS      │ │   ║DATA SEGMENT DESCRIPTORS      ║
├─────────────────────────────────────┤ │   ║EXECUTABLE SEGMENT DESCRIPTORS║
│GLOBAL EXECUTABLE SEGMENT DESCRIPTORS│ │   ║CALL GATE DESCRIPTORS         ║
├─────────────────────────────────────┤ │   ║TASK GATE DSCRIPTORS          ║
│GLOBAL CALL GATE DESCRIPTORS         │ │   /               :              /
├─────────────────────────────────────┤ │   ║               :              ║
│GLOBAL TASK GATE DESCRIPTORS         │ │┌─►║            LDT(N)            ║
├─────────────────────────────────────┤ ││  ║DATA SEGMENT DESCRIPTORS      ║
│GLOBAL TASK STATE SEGMENT DESCRIPTORS│ ││  ║EXECUTABEL SEGMENT DESCRIPTORS║
├─────────────────────────────────────┤ ││  ║CALL GATE DESCRIPTORS         ║
│(LOCAL DESCRIPTOR TABLE DESCRIPTORS) ├─┘│  ║TASK 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']
[load][readonlyexecuteonly][iopl][conforming][shared]

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
                    OS/2.

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

REALMODE            The opposite of PROTMODE, this indicates the program
                    can only be run in real mode, and is an aid to the
                    linker.

EXETYPE             Insures that the specified operating system is the
                    current one for the program. You can specify OS2,
                    WINDOWS.

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

IMPLIB COM_STUF.LIB COM_INP.DEF COM_OUT_DEF


Figure 7:

───────────────────────────────────────────────────────────────────────────
Also see:
  An overview of the way DLL_CHAT interfaces to its dynamic-link library
───────────────────────────────────────────────────────────────────────────

DEF File Format for DLL_CHAT

IMPORTS CHATLIB.login
IMPORTS CHATLIB.get_msg_cnt
IMPORTS CHATLIB.send_msg
IMPORTS CHATLIB.logout
IMPORTS CHATLIB.get_msg

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()
{
    logout(my_id);
    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.
********************************************************************/

main()
{
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)
                sprintf(msg_array[msg_cnt++],"(%d)%s",my_id,tmp_buf);
    }

        if    (ok_to_disp())
        {
            for (lp_cnt = 0 ; lp_cnt < msg_cnt; lp_cnt++)
                disp_msg(msg_array[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");
        }
    }
}

disp_msg(ptr)
char    *ptr;
{
    printf("%s", ptr);
    fflush(stdout);
}

extern far pascal dosgetinfoseg();

ok_to_disp()
{
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:

DEF File for CHATLIB

LIBRARY CHATLIB INITGLOBAL

DATA SINGLE SHARED

EXPORTS login
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{
    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)
        return(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;

    return(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");
        exit(1);
    }

    new_msg_struct(master_ptr);
}

/********************************************************************
*    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");
        initialize();
    }

    my_getseg();

    GET_SEM;
    for (log_id= 0 ; log_id < 16 ; log_id++)
    {
        tmp_msk = mask(log_id);
        if  (flag_word & tmp_msk)
        {
            flag_word &= ~tmp_msk;
            RLS_SEM;
            return(log_id);
        }

    }
    RLS_SEM;

    printf("Login slots all used up!\n");
    exit(1);
}

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

    GET_SEM;
    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)
                tmp_cnt++;
    }

    RLS_SEM;
    return(tmp_cnt);
}

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

    GET_SEM;
    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);
        RLS_SEM;
        return(NOT_OK);
    }

    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");
            free_msg(tmp_ptr);
            RLS_SEM;
            return(NOT_OK);
        }
    }

    RLS_SEM;
    return(OK);
}

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

    GET_SEM;
    for(tmp_ptr = master_ptr; tmp_ptr; tmp_ptr = tmp_ptr->next_ptr)
        mark_msg(id, tmp_ptr);

    flag_word |= mask(id);

    RLS_SEM;

    printf("In logout ... Hit a Key:");fflush(stdout);
    getch();
    printf("\n\n\n\n");

    return(0);
}

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


    GET_SEM;
    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);
            RLS_SEM;
            return(TRUE);
        }
    }
    RLS_SEM;
    return(FALSE);
}

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

free_msg(MSG *ptr)
{
  if (ptr->msg_ptr)
    my_free(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);
            dossemclear(&memory_semaphore);
            return(NULLP);
        }

       if   (stat = dossubset(major_selector, 1, 0))
        {
            printf("Error in dossubset:%d\n", stat);
            dossemclear(&memory_semaphore);
            return(NULLP);
        }
    }

    selector = 0;
    if  (stat = dossuballoc(major_selector, &selector, sizeit))
    {
        printf("dossuballoc error:%d\n", stat);
        dossemclear(&memory_semaphore);
        return(NULLP);
    }

    dossemclear(&memory_semaphore);
    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.
*
********************************************************************/

my_free(ptr)
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);
        exit(1);
    }
    dossemclear(&memory_semaphore);
}

/********************************************************************
*    my_getseg()
*
*    Causes the memory affilaited with the major_selector to
*    become accessible to this process.
********************************************************************/

my_getseg()
{
int    stat;

if  (stat=dosgetseg(major_selector))
        printf("Error on getseg:%d\n", stat);
    return(stat);
}

}


Figure 9:  A DLL Initialization Routine

EXTRN     INITROUTINE:FAR
          ASSUME    CS:_TEXT
_TEXT     SEGMENT BYTE PUBLIC 'CODE'
START     PROC FAR
          call INITROUTINE    ; the real initialization routine
          ret
START     ENDP
_TEXT     ENDS
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
semaphores."
████████████████████████████████████████████████████████████████████████████

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.


Features/Enhancements

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$
function.

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

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

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

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

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

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


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


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.

ts
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
for.

Using symbolic names for constant values is a better practice. Thus, the
definition

  CONST BLACK = 0, _BLUE = 1, ..., CYAN = 3

lets you write a statement like

  COLOR CYAN, BLUE

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

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

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

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 │
       ├───────│─────────────────────────PARSER────────────├────────┤
       │       │                           ▼               │        │
       │       │          ╔═════════════════════╗          │   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
END TYPE


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


DEFINT A-Z

'─ Subprogram declarations.
DECLARE SUB BreakSignal ()
DECLARE SUB Delay (Period!)
DECLARE SUB Dial ()
DECLARE SUB DoCommand (CmdKey$)
DECLARE SUB InitScreen ()
DECLARE SUB ProcessInput (Code$)

'─ Manifest constants.
CONST TRUE = -1, FALSE = NOT TRUE
CONST BLACK = 0, BLUE = 1, GREEN = 2, CYAN = 3
CONST RED = 4, MAGENTA = 5, BROWN = 6, WHITE = 7
CONST BRIGHT = 8, BLINK = 128
CONST CURSOROFF = 0, CURSORON = 1
CONST BUFSIZE = 512
CONST SPACE = 32
CONST ROWS = 25, COLS = 80
CONST BANNERCOL = 4, COMMANDCOL = 33
CONST TESTMODE = 0

'─ Screen management data.
TYPE WinType
     Top AS INTEGER
     Left AS INTEGER
     Bottom AS INTEGER
     Right AS INTEGER
     Fgnd AS INTEGER
     Bkgnd AS INTEGER
     Standout AS INTEGER
END TYPE

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

'─ Set communications parameters.
Parm$ = ENVIRON$("COMPARMS")            ' Check environment.
IF Parm$ = "" THEN
     Parm$ = "COM2:1200,E,7,1"          ' Use defaults.
END IF

Port$ = LEFT$(Parm$, 4)
IF Port$ = "COM1" THEN
     PortAddress = &H3FB
ELSE
     PortAddress = &H2FB
END IF
BreakMask = &H40                        ' Break control bits

'─ Open the communications channel.
OPEN Parm$ FOR RANDOM AS #1 LEN = BUFSIZE

'
' 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.
'
Main:
EscapeFlag = FALSE
DO
     '─ 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.
     DO
          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
                  ELSE
                  PRINT CHR$(27); ' The retained Esc code.
                  PRINT Received$;
                  END IF
                  EscapeFlag = FALSE
          ELSE
                  ProcessInput Received$
          END IF
     LOOP
LOOP

END

ErrorRecovery:
     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)
END SUB

'
' 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.
     DO
          Now! = TIMER
          IF (Now! - Start! < Period!) OR (Now! < Start!) THEN
                  EXIT SUB
          END IF
     LOOP
END SUB

'
' Dial
'
' Ask the user for a telephone number and dial it.
'
'
SUB Dial
     INPUT "Number: ", Phone$
     PRINT #1, "ATDT" + Phone$
END SUB

'
' 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.
                  CLOSE
                  ' Restore full screen.
                  VIEW PRINT
                  ' Clear the screen and "home" the cursor.
                  COLOR WHITE, BLACK
                  CLS
                  LOCATE CmdWin.Top, CmdWin.Left, CURSORON
                  END
          CASE 32 ' Alt+d - Dial a number
                  Dial
          CASE 48 ' Alt+b - Send break signal.
                  BreakSignal
          CASE 83 ' PC keyboard Del key - Send an ASCII DEL
                  PRINT #1, CHR$(127);
          CASE ELSE
                  ' Unknown command - ignore it.
     END SELECT
END SUB

'
' InitScreen
'
' Set up command bar (1 line), guarantee that the cursor is turned
' on, and establish the active terminal display window (24 lines).
'
'
SUB InitScreen STATIC
     SHARED CmdWin AS WinType, ViewWin AS WinType

     '─ Initialize the screen for text and 80 columns.
     SCREEN TEXTMODE
     WIDTH COLS, ROWS
     COLOR WHITE, BLACK
     CLS

     '─ 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
     PRINT "ADM3A EMULATOR";

     '─ 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
     CLS
END SUB

'
' 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
     SELECT CASE ASC(Code$)
          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
                  ELSE
                    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
                  CLS
          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$;
     END SELECT
END SUB

████████████████████████████████████████████████████████████████████████████

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


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

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


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

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

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

  DEBUG.H
  DEBUG.C

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

  EXPORTS
    TestWndFn    @1
to

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

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

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:

  LOG=NOTEPAD.EXE ^.LOG

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.


Conclusion

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

to

     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"

DebugControl DIALOG LOADONCALL MOVEABLE DISCARDABLE 8, 20, 185, 81
STYLE WS_BORDER | WS_CAPTION | WS_DLGFRAME | WS_POPUP
BEGIN
 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 "" BUG_FILTERLIST,"edit",STD_EDITFIELD,55,50,78,12
 CONTROL "&Log to" BUG_LOG,"button",STD_CHECKBOX,18,62,36,12
 CONTROL "" BUG_LOGFILE,"edit",STD_EDITFIELD ,55,63,78,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
END


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

/*
 * DEBUG TEST PROGRAM - SOURCE CODE
 *
 * 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 */
LONG FAR PASCAL     TestWndFn( HWND, WORD, WORD, LONG );

/**/

/*
 * MAINLINE - BUG TEST PROGRAM
 *
 * 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.
                */
#if DEBUG

               DebugSetup( hWnd, BUG_CONTROL, 100 );
#endif

               /* make window visible */
               bResult = TRUE;
               ShowWindow( hWnd, wCmdShow );

               }

          }

     }

     /* return result */
     return( bResult );

}

/**/

/*
 * TEST WINDOW MESSAGE PROCESSING PROCEDURE
 *
 * 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;

#if DEBUG
     /* sample debugging output */
     switch( wMessage )
          {
     case WM_MOVE :
          Debug( 1, "WM_MOVE: [%u,%u]", HIWORD(lParam),
          LOWORD(lParam) );
          break;
     case WM_SIZE :
          Debug( 1, "WM_SIZE: [%u,%u]", HIWORD(lParam),
          LOWORD(lParam) );
          break;
     case WM_CHAR :
          Debug( 2, "WM_CHAR: [%c,%u]", wParam, wParam );
          break;
     case WM_ACTIVATE :
          Debug( 3, "WM_ACTIVATE: %s",
          (wParam)?"activate":"inactivate" );
          break;
     case WM_ACTIVATEAPP :
          Debug( 3, "WM_ACTIVATEAPP: %s",
          (wParam)?"activate":"inactivate" );
          break;
     case WM_PAINT :
          Debug( 4, "WM_PAINT:" );
          break;

     default :
          break;
     }
#endif

     /* 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 )
               {
#if DEBUG
          case BUG_CONTROL : /* debug control panel */
               DebugControl( hWnd );
               break;
#endif
          default :
               lResult = DefWindowProc( hWnd, wMessage, wParam,
               lParam );
               break;
          }

          break;
     case WM_DESTROY :   /* destroy window */
          PostQuitMessage( 0 );
          break;
     default : /* send to default */
          lResult = DefWindowProc( hWnd, wMessage, wParam,
          lParam );
          break;
     }

     /* return normal result */
     return( lResult );

}

DEF File for BUGTEST

NAME            BUGTEST

DESCRIPTION    'Debug Test Utility'

STUB           '\BIN\WINSTUB.EXE'

CODE           MOVEABLE
DATA           MOVEABLE MULTIPLE

HEAPSIZE  4096
STACKSIZE      4096

EXPORTS
  TestWndFn    @1
  DebugControlDlgFn     @2

Resource File for BUGTEST

/*
 * DEBUG TEST PROGRAM - RESOURCE FILE
 *
 * 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

DebugControl DIALOG LOADONCALL MOVEABLE DISCARDABLE 8, 20, 185, 81
CAPTION "Debug Control Panel"
STYLE WS_BORDER | WS_CAPTION | WS_DLGFRAME | WS_POPUP
BEGIN
 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 "" BUG_FILTERLIST,"edit",STD_EDITFIELD,55,50,78,12
 CONTROL "&Log to" BUG_LOG,"button",STD_CHECKBOX,18,62,36,12
 CONTROL "" BUG_LOGFILE,"edit",STD_EDITFIELD ,55,63,78,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
END

████████████████████████████████████████████████████████████████████████████

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

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 MYPROG,,,LIB1+LIB2

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

  LINK  PROG1+PROG2;

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

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

  MASM MYPROG1;
  MASM MYPROG2;
  LINK MYPROG1+MYPROG2;

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

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.


Summary

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


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() ├──┘
                         │             ∙       │
                         │             ∙       │
                         │             ∙       │
                         └─────────────────────┘
                               MYLIB.LIB


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     │
                                                      └──────────────┘
                                                         MYPROG.EXE


Figure 5:  Segment order and concatenation by LINK. The start of each file,
           corresponding to the lowest address, is at the top.

      MYPROG1.ASM
     ┌──────────────────────────────┐   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   ║
 ╚═══════════════════╝  ╚════════════════════╝  ╚════════════════════╝

    00H┌─────────────────────────────┐─┐
       │         Module 1            │ ├─35H bytes (byte aligned)
    35H├─────────────────────────────┤─┘
       │█████████████████████████████│
    36H├─────────────────────────────┤─┐
       │         Module 2            │ ├─35H bytes (word aligned)
    6BH├─────────────────────────────┤─┘
       │█████████████████████████████│
       │█████████████████████████████│
    70H├─────────────────────────────┤─┐
       │         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
          END


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


EDITOR'S NOTE

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
CMD.EXE:

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

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.

Masthead

JONATHAN D. LAZARUS
Editor and Publisher

EDITORIAL

TONY RIZZO
Technical Editor

KAREN STRAUSS
Assistant Editor

JOANNE STEINHART
Production Editor

GERALD CARNEY
Staff Editor

KIM HOROWITZ
Editorial Assistant

ART

MICHAEL LONGACRE
Art Director

VALERIE MYERS
Associate Art Director

CIRCULATION

WILLIAM B. GRANBERG
Circulation Manager

L. PERRIN TOMICH
Assistant to the Publisher

DONNA PUIZINA
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
Company.

████████████████████████████████████████████████████████████████████████████

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
Token-Ring(TM).


Defining the System

Pizi had to implement and integrate three specific functions into his
design.

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.


Using DARWIN

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
application."

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


SQL

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

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.


SQL API and DDE

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

         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

╓┌───┌───────────────────────────────────────────────────────────────────────►
                       A
 1  DARWIN Macro
 2  PC Software Development
 3
 4
 5
 6  =APP.MAXIMIZE()
 7  =ECHO(FALSE)
 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)
14  =EXEC("C:\WINDOWS\DARWIN\DARWIN.EXE",3
15  =HIDE()
16  =RETURN()
17
18  OUTPUT
19  =ECHO(FALSE)
20  =IF(GET.DOCUMENU(1,"UNNAMED.XLS")="UNNAMED.XLS",GOTO(A29),GOTO(A21))
                       A
20  =IF(GET.DOCUMENU(1,"UNNAMED.XLS")="UNNAMED.XLS",GOTO(A29),GOTO(A21))
21  =DIRECTORY("C:\WINDOWS\DARWIN")
22  =OPEN("UNNAMED.XLS")
23  =COLUMN WIDTH(22,"C1")
24  =COLUMN WIDTH(22,"C2")
25  =COLUMN WIDTH(22,"C3")
26  =COLUMN WIDTH(22,"C4")
27  =DISPLAY(FALSE,FALSE,TRUE,TRUE,0)
28  =FULL(TRUE)
29  =RETURN()
30
31  LAYOUT
32  =IF(GET.DOCUMENT(1,"UNNAMED.XLS")="UNNAMED.XLS",GOTO(CLOSE),GOTO(DARWIN))
33  =APP.ACTIVATE("DARWIN",FALSE)
34  =SEND.KEYS("%{F10}",TRUE)
35  =APP.MINIMIZE()
36  =RETURN()
37
38
39
                       A
39
40  CLOSE
41  =ACTIVATE("UNNAMED.XLS")
42  =CLOSE(FALSE)
43  =APP.ACTIVATE("DARWIN",FALSE)
44  =SEND.KEYS("%{F10}",TRUE)
45  =APP.MINIMIZE()
46  =RETURN()



Figure 17:  DARWIN SQLBase Interface Shell Functions

 1

 BOOL SQLExecute(string, bindstring);

     LPSTR  string;      /* null terminated SQL statement */
     LPSTR  bindstring;  /* null terminated bind data */

Returns:

 TRUE if completed correctly.
 FALSE if error.

───────────────────────────────────────────────────────────────────────────
Note:
  The bindstring may be NULL, indicating that there is no bind value.
───────────────────────────────────────────────────────────────────────────

 2

 int SQLFetchNextRow();

Returns:

 > 0 : number of fields in the row
 = 0 : no more information to fetch (past EOF)
 = ──1 : ERROR occurred

───────────────────────────────────────────────────────────────────────────
Note:
  This routine fetches the next logical row from the database into a SQL
  API internal structure for later use by SQLGetField.
───────────────────────────────────────────────────────────────────────────

 3

 LPSTR SQLGetField(fieldnumber, datatype, flag);

     int  fieldnumber;       /* desired field (first field=1) */
     LPINT       datatype;   /* datadictionary/ metaphore datatypes */
     BOOL        flag;       /* TRUE=remove leading & trailing blanks */

Returns:

 LPSTR, which must be cast to the correct type for usage.
 NULL if field is null.

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

 4

 LPSTR SQLGetFieldAsText(fieldnumber, datatype, flag);

Returns:

 LPSTR to a NULL-terminated string.
 NULL if field is null.

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

 5

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

Returns:

 TRUE if completed successfully.
 FALSE if error.

───────────────────────────────────────────────────────────────────────────
Note:
  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.
───────────────────────────────────────────────────────────────────────────

 6

 int SQLToBiffFile(select, bindstring, bifffilename);

     LPSTR     select;        /* SQL SELECT statement */
     LPSTR     bindstring;    /* null-terminated bind data */
     LPSTR     bifffilename;  /* filename for BIFF file */

Returns:

 Number of rows written.
 ─1 if error occurred.

───────────────────────────────────────────────────────────────────────────
Note:
  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.

 7

 int SQLErrorNum();

Returns:

An integer defining the type of error, if any, that occurred.

───────────────────────────────────────────────────────────────────────────
Note:
  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─────┐
INTERNAL       EXTERNAL         DATA TYPE     DESCRIPTION
┌─────COLUMN NAME─────┐
INTERNAL       EXTERNAL         DATA TYPE     DESCRIPTION

Tablename      Internal         Char (15)     The internal name of the
               Table Name                     table that contains the
                                              column.

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─────┐
INTERNAL       EXTERNAL         DATA TYPE     DESCRIPTION
                                               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─────┐
INTERNAL       EXTERNAL         DATA TYPE     DESCRIPTION
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─────┐
INTERNAL       EXTERNAL         DATA TYPE     DESCRIPTION
                                              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
estimate.

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:

  BOOL FAR PASCAL
  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
redundant.

  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
GDI.EXE.

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:

  [kernel]
  EnableHeapChecking=1
  EnableFreeChecking=1

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

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:

  [kernel]
  EnableEMSDebug=1

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


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


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


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

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

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.


Go

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:

  WM_GETMINMAXINFO
  WM_NCCREATE
  WM_CALCSIZE
  WM_CREATE
  WM_SHOWWINDOW
  WM_SETVISIBLE
  WM_ACTIVATEAPP


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

  WM_ACTIVATE
  WM_SETFOCUS
  WM_NCPAINT
  WM_SYNCPAINT


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:

  WM_SIZE
  WM_MOVE

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:

  WM_NCHITTES
  WM_SETCURSOR

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

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) $
.c
#
# 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☼

PIXELS.
        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
displayed.

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


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

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

We'll examine these four types of transforms in more detail later in this
article.


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

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

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


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

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," and the numbers are simply
the row and column positions of each field within the matrix.

The letter l prefix on some of the fields stands for LONG. The fx prefix
stands for FIXED. The FIXED data type is a 32-bit signed long integer that
is interpreted by GPI as a 16-bit signed integer and a 16-bit unsigned
fraction. The FIXED data type allows GPI to use fractions without
resorting to time-consuming floating-point operations. Figure 8 shows
various examples of FIXED values.

In some of the following examples I use normal decimal point notation for
FIXED numbers. Keep in mind that these are really 32-bit integers.

You can convert a FIXED variable to a float or double variable by dividing
by 65536.0:

  dNumber = (fxNumber / 65536.0) ;

You can convert a float or double variable to FIXED by multiplying by
65536 and casting the result:

  fxNumber = (FIXED) (65536 * dNumber) ;

The transformation matrix is actually slightly simpler than the version
shown above. When you use a MATRIXLF structure in the GPI transform
functions, the lM13 and lM23 fields must be set equal to 0 and the lM33
field must be set equal to 1:


  │ fxM11  fxM12  0 │
  │                 │
  │ fxM21  fxM22  0 │
  │                 │
  │ lM31   lM32   1 │

GPI uses a matrix multiplication to convert a point (x,y) to a point
(x',y'). First, GPI constructs a 1-by-3 matrix from the original point
(x,y) by appending a 1. Then that matrix is multiplied by the 3-by-3
transformation matrix. The resultant 1-by-3 matrix contains the points
x', y', and 1 and is shown in Figure 9.

If (x,y) is a point in world coordinate space, then GPI applies the
modeling transform to determine the point (x',y') in model coordinate
space. If (x,y) is a point in model coordinate space, then GPI applies the
default viewing transform, and the resultant point (x',y') is in
presentation page units.

The transformation matrix can also be represented as a pair of formulas
that describe the matrix multiplication:

  x' = (fxM11) x + (fxM21) y + lM31
  y' = (fxM12) x + (fxM22) y + lM32

Note that the points x' and y' are functions of both x and y. This is not
the case in the device transform and windowing system transform.

For a new presentation space, the modeling and default viewing
transformation matrices are both identity matrices. Only the fxM11, fxM22,
and lM33 fields are set to 1. The other fields are set to 0:

  │ 1.0  0.0  0 │
  │             │
  │ 0.0  1.0  0 │
  │             │
  │ 0    0    1 │

Thus, the transformation formulas look like this:

  x' = x
  y' = y


The TRANSFRM Program

The TRANSFRM program shown in Figure 10 allows you to experiment with the
GPI transformation matrix.

The micro-PS is created in ClientWndProc during the processing of the
WM_CREATE message. It uses page units of PU_LOENGLISH, or 0.01 inch.
During the WM_PAINT message, the window procedure draws a ruler and a
stick figure enclosed in a box, as shown in Figure 11☼. The ruler has tick
marks every inch (100 units). The figure is enclosed in a box that is 2
inches high and 2 inches wide.

TRANSFRM sets the default viewing transform while processing the WM_SIZE
message. This sets the origin of model space to the center of the window.
The ruler is always drawn with a default modeling transform, which is set
during the WM_PAINT message with the following function call:

  GpiSetModelTransformMatrix ( hps, 0L, NULL, TRANSFORM_REPLACE) ;

The figure is drawn with a modeling transform that you define in a dialog
box (see Figure 12☼). This transform is set using the MATRIXLF structure
named matlfModel:

  GpiSetModelTransformMatrix ( hps, 9L, &matlfModel, TRANSFORM_REPLACE) ;

In all cases the figure is drawn from the DrawFigure function using the
same array of POINTL structures. Different modeling transformation
matrices, however, can change the appearance of the object.


Translation

Suppose you have in your program an array of POINTL structures that
defines a 100-unit square with the bottom-left corner at point (0,0). You
want to draw this square using these points, but you want the bottom-left
corner to be positioned at point (100,200). This requires a form of
transform known as translation.

Nonzero values of the lM31 and lM32 elements in the transformation
matrix cause an image to be shifted or translated from one place to
another without any distortion of the image:

  │ 1.0   0.0   0 │
  │               │
  │ 0.0   1.0   0 │
  │               │
  │ lM31  lM32  1 │

The transformation formulas are:

  x' = x + lM31
  y' = y + lM32

The matrix translates the point (0,0) to the point (lM31,lM32). If the
matrix represents the default viewing transform, then the point (0,0)
in model coordinate space is the same as the point (lM31,lM32) in page
coordinate space.

TRANSFRM.C uses translation in order to set the model space origin to
the center of the window. During the processing of the WM_SIZE message in
the client window procedure, TRANSFRM sets the default viewing
transformation matrix so that the lM31 field is half the width of the
client window in page units and lM32 is half the height. Because the
WM_SIZE message reports the size of a window in pixels, TRANSFRM must use
the GpiConvert function to convert the center position from device space
to page space.

Changing the position of the figure relative to the ruler can be done by
entering different values of lM31 and lM32 in the TRANSFRM dialog box.
Figure 13☼ shows the result when lM31 equals 150 and lM32 equals ──50.

The figure is shifted right by 150 units (1.5 inches) and down by 50 units
(0.5 inch). As you can see, translation will always transform a square to
another square. The object is simply moved to a different location on the
output device.


Scaling

The fxM11 and fxM22 fields of the MATRIXLF structure are the "scaling"
fields. If fxM11 is greater than 1.0, then the resultant image is
expanded along the x axis. Values less than 1.0 cause the width of the
image to be compressed. Similarly, fxM22 affects the height of the image.

If all other fields of the matrix are set to their default values, the
transformation matrix for scaling is

  │ fxM11  0.0    0 │
  │                 │
  │ 0.0    fxM22  0 │
  │                 │
  │ 0      0      1 │

and the transformation formulas are:

  x' = (fxM11) x
  y' = (fxM22) y

In general, scaling causes a square to be transformed into a rectangle.
Figure 14☼ shows TRANSFRM with fxM11 set to 3.0 and fxM22 set to 0.5.\


Reflection

Reflection is a form of scaling that uses negative scaling factors.
Negative values of the fxM11 and fxM12 fields cause the image to be
flipped horizontally or vertically.

For example, if you use this transformation matrix

  │ -1.0  0.0  0 │
  │              │
  │  0.0  1.0  0 │
  │              │
  │  0    0    1 │

then the transformation formulas are:

  x' = -x
  y' = y

This reflection flips the image around the vertical axis (see Figure 15☼).

Similarly, this matrix

  │ 1.0   0.0  0 │
  │              │
  │ 0.0  -1.0  0 │
  │              │
  │ 0     0    1 │

causes a reflection around the horizontal axis and turns the object upside
down (Figure 16☼).


Shear

The shear transform effectively tilts one of the coordinate axes in such
a way that it is not at right angles to the other axis. The resultant
drawing is distorted: a square is mapped to a parallelogram.

A nonzero value of fxM21 causes x-shear:

  │ 1.0    0.0  0 │
  │               │
  │ fxM21  1.0  0 │
  │               │
  │ 0      0    1 │

The transformation formulas are:

  x' = x + (fxM21) y
  y' = y

This shifts a point left or right by an amount dependent upon the distance
between the point and the horizontal axis. Figure 17☼ shows the TRANSFRM
screen when fxM21 is set to 1.0.

Similarly, the fxM12 field causes y-shear. The transformation matrix
is:

  │ 1.0  fxM12  0 │
  │               │
  │ 0.0  1.0    0 │
  │               │
  │ 0    0      1 │

Figure 18☼ shows y-shear when fxM12 is set to 1.0.

Shear sometimes suggests a three-dimensional view of an object. However,
because a square is always mapped to a parallelogram, there is no real
depth perspective.

Now try this matrix, which combines y-shear and negative x-shear:

  │  1.0  1.0  0 │
  │              │
  │ -1.0  1.0  0 │
  │              │
  │  0    0    1 │

The result appears in Figure 19☼, in which the image is rotated 45 degrees
counterclockwise and enlarged somewhat. This suggests that a combination
of scaling and shear can produce rotation, which is exactly what happens.


Rotation

From basic trigonometry we know that to rotate an image a degrees around
the origin in a counterclockwise rotation requires the following
formulas:

  x' = x * cos(a) ── y * sin(a)
  y' = x * sin(a) +

These formulas fit very nicely into the transformation matrix:

  │  cos(a)   sin(a)   0 │
  │                      │
  │ ─sin(a)   cos(a)   0 │
  │                      │
  │  0        0        1 │

For example, to rotate an image 45 degrees counterclockwise around the
origin, use:

  │  0.707  0.707  0 │
  │                  │
  │ ─0.707  0.707  0 │
  │                  │
  │  0      0      1 │

To rotate an image 90 degrees counterclockwise, use:

  │  0.0  1.0  0 │
  │              │
  │ -1.0  0.0  0 │
  │              │
  │  0    0    1 │

You can experiment with rotation angles in TRANSFRM by selecting the
"Angle..." button to invoke a second dialog box from the first. This is
shown in Figure 20☼.

A rotation of 112.5 degrees and a scaling factor of 2.5 is seen in
Figure 21☼.


Working Backwards

The TRANSFRM program also allows you to derive a transformation matrix
by selecting three corners of the box that encloses the stick figure. You
select the lower-left corner by clicking with the mouse, the lower-right
corner by clicking with the mouse while holding down the Shift key, and
the upper-left corner by clicking with the mouse while holding down the
Ctrl key.


Matrix Multiplication

One reason why matrices are used to represent graphics transforms is that
the cumulative effect of two transforms is equivalent to the product of
the two matrices.

For example, suppose you want to first double the height of an object and
then rotate the object 45 degrees counterclockwise. You would multiply
the two matrices that describe these two transforms, as shown in
Figure 22.

In the resultant matrix, the element in the ith row and jth column is the
summation of element-by-element products of the ith row of the first
matrix and the jth column of the second matrix.

Matrix multiplication is associative but not commutative. If you want to
first rotate an object 45 degrees and then double the height, the
composite transformation matrix would be calculated as shown in
Figure 23.

A combination of matrices is often used with rotation to choose the point
around which the object is rotated. For example, suppose you have stored
a series of points that describes an object. You want to rotate this
object 90 degrees around the point (100,100). This is equivalent to
translating the object by ──100 units on the x axis and ──100 units on the y
axis, rotating the image around the origin, then translating it back 100
units on the x and y axes. The three matrices that describe these
transforms are multiplied together in sequence as shown in Figure 24.

The result is:

  │  0.0  1.0  0 │
  │              │
  │ ─1.0  0.0  0 │
  │              │
  │  200  0    1 │

This is the same as rotating the image 90 degrees and then translating
it 200 units along the x axis.


Combining Transforms

You do not have to do the matrix multiplication in your program. GPI can
do the multiplications for you by using the TRANSFORM_ADD and
TRANSFORM_PREEMPT parameters to such functions as
SetModelTransformMatrix and SetDefaultViewMatrix.

The use of GPI matrix multiplication to combine three transforms is
demonstrated in the REVOLVE program shown in Figure 25.

This program does some rudimentary animation using the GPI transforms.
It shows the stick figure drawn the same way that it was in the TRANSFRM
program, but subjected to three transforms in the REVOLVE function
SetModelTransform. The transforms are varied on each WM_TIMER
message, so the figure moves and changes in shape. The three transforms
are effectively multiplied together through the use of the parameter
TRANSFORM_ADD when GpiSetModelTransformMatrix is called for the
second and third matrices.

The first transform scales the height of the figure by a factor ranging
between ─1 and 1. (The factor is obtained from a sine function.) This
gives the figure the appearance of falling head-over-heels. The second
transform rotates the figure. The third transform scales the figure by
the same factor on both the x and y axes. First, this scaling factor is
decreased, which makes the figure look like it's falling away from you;
then it is increased, which makes the figure look like it's falling toward
you.


Conclusion

The key to creating sophisticated graphics applications is based on the
ability to understand the interrelationships between coordinate spaces
and the transforms required to move from one to another. The transforms
themselves are controlled by transformation formulas and represented by
transformation matrices. GPI provides a rich set of functions for handling
transformations, allowing simple programs such as TRANSFRM and REVOLVE
to create animated sequences. Although GPI was not designed for animation,
it's nice to discover that it's features can be exploited for such
purposes.


Figure 1:  The Cartesian coordinate system that forms the basis of our
           intuitive notion of the GPI coordinate system. But it's not that
           simple.

                                      
                                      ∙
                                     +y
                                      ∙
                                      ∙
                                      ∙
                      ◄ ∙-x ∙ ∙ ∙ ∙ ∙ ∙ ∙ ∙ ∙ ∙ ∙ +x∙ ►
                                      ∙
                                      ∙
                                      ∙
                                     -y
                                      ∙
                                      ▼


Figure 2:  A rotated coordinate system.

                                                 
                            ∙                   ∙
                             +y              +x
                                ∙           ∙
                                  ∙       ∙
                                    ∙   ∙
                                      ∙
                                    ∙   ∙
                                  ∙       ∙
                                ∙           ∙
                             -x              -y
                            ∙                   ∙
                          ▼                       ▼


Figure 3:  A coordinate system in which the two axes are not at right angles.

                                                  
                                                ∙
                                             +y
                                            ∙
                                          ∙
                                        ∙
                      ◄ ∙-x ∙ ∙ ∙ ∙ ∙ ∙ ∙ ∙ ∙ ∙ ∙ +x∙ ►
                                    ∙
                                  ∙
                                ∙
                             -y
                            ∙
                          ▼


Figure 4:  The five coordinate spaces in the micro-PS subset of GPI.
           Transforms map from one coordinate space to the next.

╓┌────────────┌──────┌────────────┌───────────────┌──────────────────────────╖
 Coordinate  Units  Transform    Types of        GPI Function to
 Space                           Transformation  Set the Transform
┌─────────────────────┐
│World       Arbitrary│
└───────────────┬─────┘
                │  ┌───────────────────────────────────────────────────────┐
                └─►│Modeling     Translation                               │
                   │             Scaling         GpiSetModelTransformMatrix│
                   │             Rotation                                  │
                   │             Shear                                     │
                   └─────┬─────────────────────────────────────────────────┘
┌─────────────────────┐  │
│Model       Arbitrary│◄─┘
└───────────────┬─────┘
                │  ┌───────────────────────────────────────────────────────┐
                └─►│Default      Translation                               │
                   │Viewing      Scaling         GpiSetModelTransformMatrix│
                   │             Rotation                                  │
                   │             Shear                                     │
 Coordinate  Units  Transform    Types of        GPI Function to
 Space                           Transformation  Set the Transform
                   │             Shear                                     │
                   └─────┬─────────────────────────────────────────────────┘
┌─────────────────────┐  │
│Page        Metrics, │◄─┘
│           Pixels, or│
│            arbitrary│
└───────────────┬─────┘
                │  ┌──────────────────────────────────────────────────────┐
                └─►│Device       Translation     GpiCreatePS and          │
                   │             Scaling         GpiSetPageViewport       │
                   └─────┬────────────────────────────────────────────────┘
┌─────────────────────┐  │
│ Device     Pixels   │◄─┘
│(window)             │
└───────────────┬─────┘
                │  ┌──────────────────────────────────────────────────────┐
                └─►│Windowing    Translation     (None)                   │
                   │System                                                │
                   └─────┬────────────────────────────────────────────────┘
 Coordinate  Units  Transform    Types of        GPI Function to
 Space                           Transformation  Set the Transform
                   └─────┬────────────────────────────────────────────────┘
┌─────────────────────┐  │
│ Media      Pixels   │◄─┘
│(screen)             │
└─────────────────────┘


Figure 5:  Initial values of the device transform rectangle after creating a
           presentation space.

Device Transform
RECTL Field         Initial Value

rcl.xLeft           0
rcl.yBottom         0
rcl.xRight          Page width in pixels  ──1
rcl.yTop            Page height in pixels ──1


Figure 6:  Device Transformation Formulas

                           rcl.xRight - rcl.xLeft + 1
     x         = x      * ────────────────────────────  + rcl.xLeft
      device      page             sizl.cx

                           rcl.yTop - rcl.yBottom + 1
     y         = y      * ────────────────────────────  + rcl.yBottom
      device      page             sizl.cy


Figure 7:  Reduced Device Transformation Formulas

                           Page width in pixels
     x        = x      * ────────────────────────
      device     page    Page width in page units

                          Page height in pixels
     y        = y      * ─────────────────────────
      device     page    Page height in page units


Figure 8:  Examples of FIXED Values

FIXED Value    Interpretation

       0x1     1/65536
    0x2000         1/8
   0x10000           1
   0xAC000      10 3/4
0xFFF8F000     ─7 1/16


Figure 9:  GPI Uses Matrix Multiplication to Convert Points

                   │ fxM11  fxM12  0 │
                   │                 │
                   │                 │
   │ x  y  1 │  *  │ fxM21  fxM22  0 │  =  │ x'  y'  1 │
                   │                 │
                   │                 │
                   │  lM31   lM32  1 │


Figure 10:  Source Code for the TRANSFRM Program

TRANSFRM Make File

#--------------------
# TRANSFRM make file
#--------------------

transfrm.obj : transfrm.c transfrm.h
     cl -c -G2sw -W3 -Zp transfrm.c

transfrm.res : transfrm.rc transfrm.h
     rc -r transfrm

transfrm.exe : transfrm.obj transfrm.def transfrm.res
     link transfrm, /align:16, NUL, os2, transfrm
     rc transfrm.res

TRANSFRM.C Source Code

/*-------------------------------------------
   TRANSFRM.C -- Demonstrates GPI Transforms
  -------------------------------------------*/

#define INCL_WIN
#define INCL_GPI

#include <os2.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include "transfrm.h"

MRESULT EXPENTRY ClientWndProc (HWND, USHORT, MPARAM, MPARAM) ;

HAB  hab ;

int main (void)
     {
     static CHAR szClientClass [] = "Transfrm" ;
     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,
                    WS_VISIBLE │ FS_SIZEBORDER │ FS_TITLEBAR
                               │ FS_SYSMENU    │ FS_MINMAX   │ FS_MENU,
                    szClientClass, "GPI Transform Demo",
                    0L, NULL, ID_RESOURCE, &hwndClient) ;

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

     WinDestroyWindow (hwndFrame) ;
     WinDestroyMsgQueue (hmq) ;
     WinTerminate (hab) ;

     return 0 ;
     }
          /*-------------------------------------
             Functions for managing dialog boxes
            -------------------------------------*/

VOID SetFixedItem (HWND hwnd, USHORT idItem, FIXED fxValue)
     {
     CHAR szBuffer [20] ;

     sprintf (szBuffer, "%.3f", fxValue / 65536.0) ;
     WinSetDlgItemText (hwnd, idItem, szBuffer) ;
     }

FIXED GetFixedItem (HWND hwnd, USHORT idItem)
     {
     CHAR szBuffer [20] ;

     WinQueryDlgItemText (hwnd, idItem, sizeof szBuffer, szBuffer) ;
     return (FIXED) (atof (szBuffer) * 65536) ;
     }

VOID SetLongItem (HWND hwnd, USHORT idItem, LONG lValue)
     {
     CHAR szBuffer [20] ;

     ltoa (lValue, szBuffer, 10) ;
     WinSetDlgItemText (hwnd, idItem, szBuffer) ;
     }

LONG GetLongItem (HWND hwnd, USHORT idItem)
     {
     CHAR   szBuffer [20] ;

     WinQueryDlgItemText (hwnd, idItem, sizeof szBuffer, szBuffer) ;
     return atol (szBuffer) ;
     }
          /*------------------------------
             Dialog box window procedures
            ------------------------------*/

MRESULT EXPENTRY AngleDlgProc (HWND hwnd, USHORT msg, MPARAM mp1, MPARAM
mp2)
     {
     static PMATRIXLF pmatlf ;
     FIXED            fxAngle, fxScale ;

     switch (msg)
          {
          case WM_INITDLG:
               pmatlf = (PMATRIXLF) mp2 ;
               break ;

          case WM_COMMAND:
               switch (COMMANDMSG(&msg)->cmd)
                    {
                    case DID_OK:
                         fxAngle = GetFixedItem (hwnd, IDD_ANGLE) ;
                         fxScale = GetFixedItem (hwnd, IDD_SCALE) ;

                         pmatlf->fxM11 = (FIXED) (fxScale * cos (6.283 *
                                            fxAngle / 65536 / 360)) ;

                         pmatlf->fxM22 = pmatlf->fxM11 ;
                         pmatlf->fxM12 = (FIXED) (fxScale * sin (6.283 *
                                             fxAngle / 65536 / 360)) ;

                         pmatlf->fxM21 = - pmatlf->fxM12 ;

                         WinDismissDlg (hwnd, TRUE) ;
                         break ;

                    case DID_CANCEL:
                         WinDismissDlg (hwnd, FALSE) ;
                         break ;
                    }
               break ;

          default:
               return WinDefDlgProc (hwnd, msg, mp1, mp2) ;
          }
     return FALSE ;
     }

MRESULT EXPENTRY MatrixDlgProc (HWND hwnd, USHORT msg, MPARAM mp1, MPARAM
mp2)
     {
     static PMATRIXLF pmatlf ;

     switch (msg)
          {
          case WM_INITDLG:
               pmatlf = (PMATRIXLF) mp2 ;

               SetFixedItem (hwnd, IDD_FXM11, pmatlf->fxM11) ;
               SetFixedItem (hwnd, IDD_FXM12, pmatlf->fxM12) ;
               SetFixedItem (hwnd, IDD_FXM21, pmatlf->fxM21) ;
               SetFixedItem (hwnd, IDD_FXM22, pmatlf->fxM22) ;

               SetLongItem (hwnd, IDD_LM31, pmatlf->lM31) ;
               SetLongItem (hwnd, IDD_LM32, pmatlf->lM32) ;
               break ;

          case WM_COMMAND:
               switch (COMMANDMSG(&msg)->cmd)
                    {
                    case IDD_ANGLEPOP:
                         if (!WinDlgBox (HWND_DESKTOP, hwnd,
                                        AngleDlgProc, NULL,
                                        IDD_ANGLEBOX, pmatlf))
                              break ;

                         SetFixedItem (hwnd, IDD_FXM11, pmatlf->fxM11) ;
                         SetFixedItem (hwnd, IDD_FXM12, pmatlf->fxM12) ;
                         SetFixedItem (hwnd, IDD_FXM21, pmatlf->fxM21) ;
                         SetFixedItem (hwnd, IDD_FXM22, pmatlf->fxM22) ;
                         break ;

                    case DID_OK:
                         pmatlf->fxM11 = GetFixedItem (hwnd, IDD_FXM11);
                         pmatlf->fxM12 = GetFixedItem (hwnd, IDD_FXM12);
                         pmatlf->fxM21 = GetFixedItem (hwnd, IDD_FXM21);
                         pmatlf->fxM22 = GetFixedItem (hwnd, IDD_FXM22);

                         pmatlf->lM31 = GetLongItem (hwnd, IDD_LM31) ;
                         pmatlf->lM32 = GetLongItem (hwnd, IDD_LM32) ;

                         WinDismissDlg (hwnd, TRUE) ;
                         break ;

                    case DID_CANCEL:
                         WinDismissDlg (hwnd, FALSE) ;
                         break ;
                    }
               break ;

          default:
               return WinDefDlgProc (hwnd, msg, mp1, mp2) ;
          }
     return FALSE ;
     }

MRESULT EXPENTRY AboutDlgProc (HWND hwnd, USHORT msg, MPARAM mp1,
                                                      MPARAM mp2)
     {
     switch (msg)
          {
          case WM_COMMAND:
               switch (COMMANDMSG(&msg)->cmd)
                    {
                    case DID_OK:
                    case DID_CANCEL:
                         WinDismissDlg (hwnd, TRUE) ;
                         break ;

                    default:
                         break ;
                    }
               break ;

          default:
               return WinDefDlgProc (hwnd, msg, mp1, mp2) ;
          }
     return FALSE ;
     }
          /*-------------------
             Drawing functions
            -------------------*/

VOID DrawLine (HPS hps, SHORT x1, SHORT y1, SHORT x2, SHORT y2)
     {
     POINTL ptl ;

     ptl.x = x1 ; ptl.y = y1 ; GpiMove (hps, &ptl) ;
     ptl.x = x2 ; ptl.y = y2 ; GpiLine (hps, &ptl) ;
     }

VOID DrawRuler (HPS hps)
     {
     SHORT sIndex ;

     DrawLine (hps, -350,    0, 350,   0) ;
     DrawLine (hps,    0, -350,   0, 350) ;

     for (sIndex = -300 ; sIndex <= 300 ; sIndex += 100)
          {
          DrawLine (hps, sIndex,     -5, sIndex,      5) ;
          DrawLine (hps,     -5, sIndex,      5, sIndex) ;
          }
     }

VOID DrawFigure (HPS hps)
     {
     static POINTL aptlFigure [] =
               {
                -20,  100,  20, 100,  20,   90,  10,   90,   10,   85,
                 20,   85,  20,  75,  35,   75,  35,   70,   20,   70,
                 20,   65,   5,  65,   5,   60,  20,   60,   10,   50,
                 10,   40,  25,  40,  85,  100, 100,  100,  100,   85,
                 25,   10,  25, -25,  50,  -85,  75,  -85,   75, -100,
                 35, -100,   0, -15, -35, -100, -75, -100,  -75,  -85,
                -50,  -85, -25, -25, -25,   10, -85,  -50, -100,  -50,
               -100,  -35, -25,  40, -10,   40, -10,   50,  -20,   60,
                -20,  100
               } ;
     static POINTL aptlBox [] =
               {
               -100, -100, 100, -100, 100, 100, -100, 100
               } ;

     GpiMove (hps, aptlFigure) ;
     GpiPolyLine (hps, sizeof aptlFigure / sizeof aptlFigure [0] - 1L,
                  aptlFigure + 1) ;

     GpiSetLineType (hps, LINETYPE_DOT) ;

     GpiMove (hps, aptlBox + 3) ;
     GpiPolyLine (hps, 4L, aptlBox) ;

     GpiSetLineType (hps, LINETYPE_DEFAULT) ;
     }
          /*-------------------------
             Client window procedure
            -------------------------*/

MRESULT EXPENTRY ClientWndProc (HWND hwnd, USHORT msg, MPARAM mp1,
                                                       MPARAM mp2)
     {
     static HPS      hps ;
     static MATRIXLF matlfModel ;
     static POINTL   aptl [3] = { -100, -100, 100, -100, -100,  100 } ;
     HDC             hdc ;
     MATRIXLF        matlf ;
     POINTL          ptl ;
     SIZEL           sizl ;
     enum            { LowerLeft, LowerRight, UpperLeft } ;

     switch (msg)
          {
          case WM_CREATE:
               hdc = WinOpenWindowDC (hwnd) ;

               sizl.cx = sizl.cy = 0 ;

               hps = GpiCreatePS (hab, hdc, &sizl, PU_LOENGLISH │
                                    GPIF_DEFAULT │ GPIT_MICRO   │
                                    GPIM_NORMAL  │ GPIA_ASSOC) ;

               GpiQueryModelTransformMatrix (hps, 9L, &matlfModel) ;
               break ;

          case WM_SIZE:
               ptl.x = SHORT1FROMMP (mp2) / 2 ; /* Half window width  */
               ptl.y = SHORT2FROMMP (mp2) / 2 ; /* Half window height */

               GpiConvert (hps, CVTC_DEVICE, CVTC_PAGE, 1L, &ptl) ;

               GpiQueryDefaultViewMatrix (hps, 9L, &matlf) ;

               matlf.lM31 = ptl.x ;
               matlf.lM32 = ptl.y ;

               GpiSetDefaultViewMatrix (hps, 9L, &matlf,
                                        TRANSFORM_REPLACE) ;
               break ;

          case WM_BUTTON1DOWN:
               ptl.x = MOUSEMSG(&msg)->x ;
               ptl.y = MOUSEMSG(&msg)->y ;

               GpiConvert (hps, CVTC_DEVICE, CVTC_MODEL, 1L, &ptl) ;

               if (WinQueryKeyState (HWND_DESKTOP, VK_CONTROL) < 0)
                    aptl [UpperLeft] = ptl ;

               else if (WinQueryKeyState (HWND_DESKTOP, VK_SHIFT) < 0)
                    aptl [LowerRight] = ptl ;

               else /* neither shift nor control */
                    aptl [LowerLeft] = ptl ;

               matlfModel.fxM11 = 65536 * (aptl [LowerRight].x -
                                           aptl [LowerLeft].x) / 200 ;
               matlfModel.fxM12 = 65536 * (aptl [LowerRight].y -
                                           aptl [LowerLeft].y) / 200 ;
               matlfModel.fxM21 = 65536 * (aptl [UpperLeft].x -
                                           aptl [LowerLeft].x) / 200 ;
               matlfModel.fxM22 = 65536 * (aptl [UpperLeft].y -
                                           aptl [LowerLeft].y) / 200 ;
               matlfModel.lM31 =  (aptl [LowerRight].x +
                                   aptl [UpperLeft].x) / 2 ;
               matlfModel.lM32 =  (aptl [LowerRight].y +
                                   aptl [UpperLeft].y) / 2 ;

               WinInvalidateRect (hwnd, NULL, FALSE) ;
               return TRUE ;

          case WM_COMMAND:
               switch (COMMANDMSG(&msg)->cmd)
                    {
                    case IDM_SELECT:
                         if (WinDlgBox (HWND_DESKTOP, hwnd,
                                   MatrixDlgProc, NULL,
                                   IDD_MATRIXBOX, &matlfModel))
                              {
                              GpiSetModelTransformMatrix (hps, 9L,
                                   &matlfModel, TRANSFORM_REPLACE) ;

                              aptl [LowerLeft].x  = -100 ;
                              aptl [LowerLeft].y  = -100 ;
                              aptl [LowerRight].x =  100 ;
                              aptl [LowerRight].y = -100 ;
                              aptl [UpperLeft].x  = -100 ;
                              aptl [UpperLeft].y  =  100 ;

                              GpiConvert (hps, CVTC_WORLD, CVTC_MODEL,
                                             3L, aptl) ;

                              WinInvalidateRect (hwnd, NULL, FALSE) ;
                              }
                         break ;

                    case IDM_ABOUT:
                         WinDlgBox (HWND_DESKTOP, hwnd, AboutDlgProc,
                                    NULL, IDD_ABOUTBOX, NULL) ;
                         break ;

                    default:
                         break ;
                    }
               break ;

          case WM_PAINT:
               /* WinBeginPaint (hwnd, hps, NULL) ; */

                                  /* temporary fix for graphics field */
               {
               static RECTL rcl ;

               GpiQueryPS (hps, (PSIZEL) &rcl.xRight) ;
               GpiSetGraphicsField (hps, &rcl) ;
               }
                                  /* end of temporary fix */
               GpiErase (hps) ;

               GpiSetModelTransformMatrix (hps, 0L, NULL,
                                             TRANSFORM_REPLACE) ;
               DrawRuler (hps) ;

               GpiSetModelTransformMatrix (hps, 9L, &matlfModel,
                                             TRANSFORM_REPLACE) ;
               DrawFigure (hps) ;

               /* WinEndPaint (hps) ; */

               WinValidateRect (hwnd, NULL, FALSE) ; /* temporary fix */
               break ;

          case WM_DESTROY:
               GpiDestroyPS (hps) ;
               break ;

          default:
               return WinDefWindowProc (hwnd, msg, mp1, mp2) ;
          }
     return FALSE ;
     }

TRANSFRM.H Header File

/*------------------------
   TRANSFRM.H header file
  ------------------------*/

#define ID_RESOURCE      1

#define IDM_SELECT       10
#define IDM_ABOUT        11

#define IDD_ABOUTBOX     1
#define IDD_MATRIXBOX    2
#define IDD_ANGLEBOX     3

#define IDD_FXM11        10
#define IDD_FXM12        11
#define IDD_FXM21        12
#define IDD_FXM22        13
#define IDD_LM31         14
#define IDD_LM32         15
#define IDD_ANGLEPOP     16

#define IDD_ANGLE        20
#define IDD_SCALE        21

TRANSFRM.RC  Resource Script File

/*----------------------------------
   TRANSFRM.RC resource script file
  ----------------------------------*/

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

MENU ID_RESOURCE
     {
     SUBMENU "~Transform",                   -1
          {
          MENUITEM "~Select...",             IDM_SELECT
          MENUITEM SEPARATOR
          MENUITEM "E~xit",                  SC_CLOSE, MIS_SYSCOMMAND
          MENUITEM "A~bout Transfrm...",     IDM_ABOUT
          }
     }

DLGTEMPLATE IDD_ABOUTBOX
  {
  DIALOG "", 0, 10, 20, 160, 100, WS_SAVEBITS │ FS_DLGBORDER
    {
    CTEXT "Transfrm"                            -1,  10, 72, 140, 10
    CTEXT "GPI Transformation Demo"             -1,  10, 46, 140, 10
    CTEXT "Copyright(C) Charles Petzold, 1988" -1,  10, 30, 140, 10
    DEFPUSHBUTTON "OK"            DID_OK,  64,  8,  32, 14, WS_GROUP
    }
 }

#define XFORMULA "x' = (fxM11) x + (fxM21) y + (lM31)"
#define YFORMULA "y' = (fxM12) x + (fxM22) y + (lM32)"

DLGTEMPLATE IDD_MATRIXBOX
  {
  DIALOG "", 0, 8, 72, 176, 140, WS_SAVEBITS │ FS_DLGBORDER
    {
    LTEXT    "fxM11 (x scale):"        -1,  32, 122,  80, 10,
    EDITTEXT ""                 IDD_FXM11, 116, 122,  32,  8, ES_MARGIN
    LTEXT    "fxM12 (y shear):"        -1,  32, 108,  80, 10,
    EDITTEXT ""                 IDD_FXM12, 116, 108,  32,  8, ES_MARGIN
    LTEXT    "fxM21 (x shear):"        -1,  32,  94,  80, 10,
    EDITTEXT ""                 IDD_FXM21, 116,  94,  32,  8, ES_MARGIN
    LTEXT    "fxM22 (y scale):"        -1,  32,  80,  80, 10,
    EDITTEXT ""                 IDD_FXM22, 116,  80,  32,  8, ES_MARGIN
    LTEXT    "lM31 (x translate):"     -1,  32,  66,  80, 10,
    EDITTEXT ""                  IDD_LM31, 116,  66,  32,  8, ES_MARGIN
    LTEXT    "lM32 (y translate):"     -1,  32,  52,  80, 10,
    EDITTEXT ""                  IDD_LM32, 116,  52,  32,  8, ES_MARGIN
    CTEXT    XFORMULA                  -1,   8,  36, 160, 10
    CTEXT    YFORMULA                  -1,   8,  26, 160, 10
    PUSHBUTTON    "Angle..." IDD_ANGLEPOP,   8,   8,  48, 14, WS_GROUP
    DEFPUSHBUTTON "Ok"             DID_OK,  64,   8,  48, 14, WS_GROUP
    PUSHBUTTON    "Esc=Cancel" DID_CANCEL, 120,   8,  48, 14, WS_GROUP
    }
  }

DLGTEMPLATE IDD_ANGLEBOX
  {
  DIALOG "", 0, 0, -64, 176, 64, WS_SAVEBITS │ FS_DLGBORDER
    {
    LTEXT    "Angle in degrees:"       -1,  32,  46,  80, 10,
    EDITTEXT "0.00"             IDD_ANGLE, 116,  46,  32,  8, ES_MARGIN
    LTEXT    "Scaling factor:"         -1,  32,  32,  80, 10,
    EDITTEXT "1.00"             IDD_SCALE, 116,  32,  32,  8, ES_MARGIN
    DEFPUSHBUTTON "Ok"             DID_OK,  24,   8,  48, 14, WS_GROUP
    PUSHBUTTON    "Esc=Cancel" DID_CANCEL, 104,   8,  48, 14, WS_GROUP
    }
  }

TRANSFRM.DEF  Module Definition File

;-------------------------------------
; TRANSFRM.DEF module definition file
;-------------------------------------

NAME           TRANSFRM
DESCRIPTION    'Demonstrates GPI Transforms(C) Charles Petzold, 1988'
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        ClientWndProc
               MatrixDlgProc
               AngleDlgProc
               AboutDlgProc


Figure 22:  The cumulative effect of two transforms is equivalent to the
            product of the two matrices. This example first doubles the
            height of an object and then rotates it 45° counterclockwise.

│ 1.0  0.0  0 │   │  0.707  0.707  0 │   │  0.707  0.707  0 │
│             │   │                  │   │                  │
│             │   │                  │   │                  │
│ 0.0  2.0  0 │ * │ ─0.707  0.707  0 │ = │ ─1.414  1.414  0 │
│             │   │                  │   │                  │
│             │   │                  │   │                  │
│ 0    0    1 │   │  0      0      1 │   │  0      0      1 │


Figure 23:  Matrix operation necessary to first rotate an object 45°,
            then double the height. The result is due to the fact that
            matrix multiplication is associative, but not commutative.

│  0.707  0.707  0 │   │ 1.0  0.0  0 │   │  0.707  1.414  0 │
│                  │   │             │   │                  │
│                  │   │             │   │                  │
│ ─0.707  0.707  0 │ * │ 0.0  2.0  0 │ = │ ─0.707  1.414  0 │
│                  │   │             │   │                  │
│                  │   │             │   │                  │
│  0      0      1 │   │ 0    0    1 │   │  0      0      1 │


Figure 24:  A combination of matrices such as those shown here are used with
            rotation to choose the point around which the object is rotated.

│  1.0   0.0  0 │   │  0.0  1.0  0 │   │ 1.0  0.0  0 │
│               │   │              │   │             │
│               │   │              │   │             │
│  0.0   1.0  0 │ * │ ─1.0  0.0  0 │ * │ 0.0  1.0  0 │
│               │   │              │   │             │
│               │   │              │   │             │
│ ─100  ─100  1 │   │  0    0    1 │   │ 100  100  1 │


Figure 25:  Source Code for the Revolve Program

REVOLVE Make File

#-------------------
# REVOLVE make file
#-------------------

revolve.obj : revolve.c
     cl -c -G2sw -W3 -Zp revolve.c

revolve.exe : revolve.obj revolve.def
     link revolve, /align:16, NUL, os2, revolve

REVOLVE.C Source Code

/*------------------------------------------
   REVOLVE.C -- Demonstrates GPI Transforms
  ------------------------------------------*/

#define INCL_WIN
#define INCL_GPI

#include <os2.h>
#include <math.h>
#include <stddef.h>

MRESULT EXPENTRY ClientWndProc (HWND, USHORT, MPARAM, MPARAM) ;

HAB  hab ;

int main (void)
     {
     static CHAR szClientClass [] = "Revolve" ;
     HMQ    hmq ;
     HWND   hwndFrame, hwndClient ;
     QMSG   qmsg ;

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

     WinRegisterClass (hab, szClientClass, ClientWndProc, CS_SIZEREDRAW │
                                                          CS_SYNCPAINT, 0)
;

     hwndFrame = WinCreateStdWindow (HWND_DESKTOP,
                    WS_VISIBLE │ FS_SIZEBORDER │ FS_TITLEBAR
                               │ FS_SYSMENU    │ FS_MINMAX,
                    szClientClass, "GPI Transform Demo",
                    0L, NULL, 0, &hwndClient) ;

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

     WinDestroyWindow (hwndFrame) ;
     WinDestroyMsgQueue (hmq) ;
     WinTerminate (hab) ;

     return 0 ;
     }

VOID SetModelTransform (HPS hps, FIXED fxYScale, FIXED fxAngle,
                                                 FIXED fxScale)
     {
     MATRIXLF matlf ;

     GpiSetModelTransformMatrix (hps, 0L, NULL, TRANSFORM_REPLACE) ;
     GpiQueryModelTransformMatrix (hps, 9L, &matlf) ;

     matlf.fxM22 = fxYScale ;

     GpiSetModelTransformMatrix (hps, 9L, &matlf, TRANSFORM_REPLACE) ;

     matlf.fxM11 = (FIXED) (cos (6.283*fxAngle/65536/360) * 65536) ;
     matlf.fxM22 = matlf.fxM11 ;
     matlf.fxM12 = (FIXED) (sin (6.283*fxAngle/65536/360) * 65536) ;
     matlf.fxM21 = -matlf.fxM12 ;

     GpiSetModelTransformMatrix (hps, 9L, &matlf, TRANSFORM_ADD) ;

     matlf.fxM11 = matlf.fxM22 = fxScale ;
     matlf.fxM12 = matlf.fxM21 = 0 ;

     GpiSetModelTransformMatrix (hps, 9L, &matlf, TRANSFORM_ADD) ;
     }

VOID DrawFigure (HPS hps)
     {
     static POINTL aptlFigure [] =
               {
                -20,  100,  20, 100,  20,   90,  10,   90,   10,   85,
                 20,   85,  20,  75,  35,   75,  35,   70,   20,   70,
                 20,   65,   5,  65,   5,   60,  20,   60,   10,   50,
                 10,   40,  25,  40,  85,  100, 100,  100,  100,   85,
                 25,   10,  25, -25,  50,  -85,  75,  -85,   75, -100,
                 35, -100,   0, -15, -35, -100, -75, -100,  -75,  -85,
                -50,  -85, -25, -25, -25,   10, -85,  -50, -100,  -50,
               -100,  -35, -25,  40, -10,   40, -10,   50,  -20,   60,
                -20,  100
               } ;

     GpiMove (hps, aptlFigure) ;

     GpiPolyLine (hps, sizeof aptlFigure / sizeof aptlFigure [0] - 1L,
                  aptlFigure + 1) ;
     }

MRESULT EXPENTRY ClientWndProc (HWND hwnd, USHORT msg, MPARAM mp1,
                                                       MPARAM mp2)
     {
     static double dScaler = 1.1, dYScaleAngle = 0.0,
                   dYScaleAngleInc = 0.25 ;
     static FIXED  fxAngle,  fxAngleInc = 0xA0000L,
                   fxYScale, fxScale = 0x2000L ;
     static HPS    hps ;
     HDC           hdc ;
     MATRIXLF      matlf ;
     POINTL        ptl ;
     SIZEL         sizl ;

     switch (msg)
          {
          case WM_CREATE:
               hdc = WinOpenWindowDC (hwnd) ;

               sizl.cx = sizl.cy = 0 ;

               hps = GpiCreatePS (hab, hdc, &sizl, PU_LOENGLISH │
                                    GPIF_DEFAULT │ GPIT_MICRO   │
                                    GPIM_NORMAL  │ GPIA_ASSOC) ;

               GpiSetMix (hps, FM_INVERT) ;

               if (!WinStartTimer (hab, hwnd, 1, 100L))
                    WinAlarm (HWND_DESKTOP, WA_ERROR) ;

               break ;

          case WM_SIZE:
               ptl.x = LOUSHORT (mp2) / 2 ;  /* Half of window width  */
               ptl.y = HIUSHORT (mp2) / 2 ;  /* Half of window height */

               GpiConvert (hps, CVTC_DEVICE, CVTC_PAGE, 1L, &ptl) ;

               GpiQueryDefaultViewMatrix (hps, 9L, &matlf) ;

               matlf.lM31 = ptl.x ;
               matlf.lM32 = ptl.y ;

               GpiSetDefaultViewMatrix (hps, 9L, &matlf,
                                             TRANSFORM_REPLACE) ;
               break ;

          case WM_TIMER:
                                   /* temporary fix for graphics field */
               {
               static RECTL rcl ;

               GpiQueryPS (hps, (PSIZEL) &rcl.xRight) ;
               GpiSetGraphicsField (hps, &rcl) ;
               }
                                   /* end of temporary fix */

               SetModelTransform (hps, fxYScale, fxAngle, fxScale) ;
               DrawFigure (hps) ;

               fxYScale = (FIXED) (65535 * cos (dYScaleAngle)) ;

               if ((dYScaleAngle += dYScaleAngleInc) > 6.283)
                    dYScaleAngle -= 6.283 ;

               if ((fxAngle += fxAngleInc) > (FIXED) 0x1680000L)
                    fxAngle -= (FIXED) 0x1680000L ;

               if ((FIXED) (fxScale *= dScaler) >= (FIXED) 0x30000L ││
                         fxScale <= (FIXED) 0x2000L)
                    dScaler = 1 / dScaler ;

               SetModelTransform (hps, fxYScale, fxAngle, fxScale) ;
               DrawFigure (hps) ;
               break ;

          case WM_PAINT:
               /* WinBeginPaint (hwnd, hps, NULL) ; */

                                 /* temporary fix for graphics field */
               {
               static RECTL rcl ;

               GpiQueryPS (hps, (PSIZEL) &rcl.xRight) ;
               GpiSetGraphicsField (hps, &rcl) ;
               }
                                 /* end of temporary fix */
               GpiErase (hps) ;

               SetModelTransform (hps, fxYScale, fxAngle, fxScale) ;
               DrawFigure (hps) ;

               /* WinEndPaint (hps) ; */

               WinValidateRect (hwnd, NULL, FALSE) ;     /* temp fix */
               break ;

          case WM_DESTROY:
               WinStopTimer (hab, hwnd, 1) ;
               GpiDestroyPS (hps) ;
               break ;

          default:
               return WinDefWindowProc (hwnd, msg, mp1, mp2) ;
          }
     return NULL ;
     }

REVOLVE.DEF Module Definition File

;------------------------------------
; REVOLVE.DEF module definition file
;------------------------------------

NAME           REVOLVE
DESCRIPTION    'Demonstrates GPI Transforms(C) Charles Petzold, 1988'
PROTMODE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        ClientWndProc

████████████████████████████████████████████████████████████████████████████

Microsoft Macro Assembler Version 5.1 Simplifies Macros and Interfacing

Ross M. Greenberg☼

Interrupt service routines. High-speed async routines. Direct screen
memory writes. Direct low-level disk routines. Precise control. The C
language. Which item seems out of place?

Until recently, you probably would have said C. I know I would have.
Whenever it was necessary to create a routine to do something out of the
ordinary quickly and efficiently, the idea of using a high-level language
like C never crossed my mind. All that overhead-PUSH BPs when not doing
stack frame referencing, volatile variables changed by a well-meaning
optimization routine, lengthy compile times-contributed to my looking to
Microsoft(R) Macro Assembler (MASM) for true efficiency.

Of course, that meant going through the headache of creating MASM
preambles and postambles to each MASM routine, not to mention the extra
work required if my code exceeded 64Kb and I needed to go to the next
memory model. I admit it. I always felt it was too much work to create
MASM code as a model-independent piece of work-until I started using
macros.

Macros are probably the most underutilized assembly language programmer's
tool. A macro is a simple shorthand for using the same concepts again and
again; PUSH_ALL and POP_ALL are certainly concepts all programmers use,
so why not create a shorthand that will later be expanded into the
appropriate instructions?

With the introduction of MASM 5.0, writing macros was made easier by a set
of well-written macros that came with the package. My productivity picked
up substantially, my code became clearer to read, I didn't spend hours
trying to avoid a memory model change, and I had plenty of time to think
up excuses for not documenting my work.

With the release of MASM 5.1, many of my macros have become obsolete
because their functionality is now part of the assembler itself. MASM 5.1
includes the changes I would have made to the assembler if I had written
this version.

Some substantial rewrites of your code may therefore be in order. This
article will examine the changes needed to take advantage of the new
features and enhancements in MASM 5.1.


Limitations of C

The overhead in certain routines in C can be a problem when trying to
write good, clean, fast code. Think about the movmem routine, for example.
Now consider overlapping source and destination pointers. You can't
really write dual-purpose code that is as clean and fast as single-purpose
code.

Even calling DOS interrupts directly can be inefficient. Although you know
that only one or two registers must be set before the interrupt routine is
called, the int86 routine doesn't know that. It saves all the registers,
sets them from your REGS structure, calls the interrupt, stuffs all the
registers on return into the REGS (or possibly another) structure, then
pops all the registers back. Think of all the overhead necessary for doing
something as simple as setting the AH register where the return value
isn't even important. Aside from making your code bigger than it has to
be, all those extra instructions add up to more work for the processor,
and that translates into processor overhead──exactly what you're trying to
avoid by calling the interrupt routines directly.

Certain specific operations also cry out for MASM efficiency:
intersegment memory moves, taking over a hardware interrupt (such as the
timer tick, keyboard, or serial comm), or something esoteric like an
overlay loader.


Interfacing MASM and C

There are some problems involved with getting the most from your MASM
routines. Having a MASM routine call a C routine from an interrupt can
certainly cause confusion. You have to remember to provide your own stack
and to restore the data segment register from someplace, and you must
assume that even calling one C routine will cause every register to
change.

Pulling variables off the stack when a MASM routine is called from C is
also an example of heuristic programming. I almost never remember the
offsets into the stack (which depend on the memory model and the number of
registers I save before the address), and I used to end up with monsters
that did work but somehow embarrassed me when I showed someone my code
(see Figure 1).

With the new MASM 5.1, the same code can simply be rewritten as

  routine  proc uses ax, var1:int
   mov ax, var1
     <other code>
     ret
  routine endp

which is quite a bit cleaner, even though the code generated is virtually
identical.

Another bad habit is forgetting to pop registers in the same order that
you push them. This results in bugs that usually show up pretty quickly.
MASM 5.1 comes to the rescue with the utilization of the "uses" clause.
The code produced is not necessarily more efficient, but at least one
problem is no longer in the check-this-out-in-the-debugger phase of
development.


Introducing MASM 5.1

The new features of MASM 5.1 represent both a formalization of what one
does with macros as well as enhancements to the macro capability itself.
If you're moving from 5.0 to 5.1, you won't really have to modify much of
your source code. If you're moving up from 4.0, the upgrade will astonish
you with its power and ease of use.


Macros

Macros essentially enable you to define new "operands" within MASM; in
order to create one you assign a symbolic name (associated with one or
more variables or arguments, or none at all) with a block of source
statements. When the assembler sees a macro reference, it expands it to
the predefined sequence of instructions. Macros can have some
intelligence built into them, letting you generate a different set of
instructions depending upon such conditions as the type and number of
arguments and the memory model. However, the macro parser only looks at
the conditions at compilation time. You cannot use the value of a register
at run time, for example, to define macro expansion. As you build your own
set of macros, you'll typically keep them in an include file.

MASM 5.1 includes the ability to have "text macros," such as using the
right-hand value of an EQU statement as a macro and allowing for its
expansion. In 5.0, Figure 2 would have required a call to a "regular"
macro in order to be properly evaluated. A macro might have been:

  msg_print macro
     do_print %errprint
  endm
  do_print macro   msg_list
     irp msg, <msg_list>
       push ax
       push dx
       mov ah, 9
       mov dx, offset msg
       int 21h
       pop dx
       pop ax
     endm
  endm

In 5.1, however, the middle macro isn't required, and a macro such as this
one is perfectly valid:

  msg_print macro
  %  irp msg, <msg_list>
        push ax
        push dx
        mov ah, 9
        mov dx, offset msg
        int 21h
        pop dx
        pop ax
     endm
  endm

The percent (%) sign at the beginning of the IRP line forces text macro
evaluation to take place. There is no saving in code size, but the
structure is certainly clearer to read.

Another nice feature is the ability to use the ampersand (&) operator for
string concatenation outside a macro just as you did inside the macro
body:

  label equ <test_stuff>
  %table_entry_&label db
  "tester"

In MASM 5.0 this was illegal and the table_entry line without the
ampersand was seen by the compiler as table_entry_label and not
evaluated.

You can now also do substring searches and culls, string concatenation,
and determine the argument size of the text macro argument(s). Using the
SUBSTR operator, the line in Figure 3 will push the appropriate register,
and the following will push ax and bx on the stack:

  reg_push 1
  reg_push 2

The second argument is the starting position within the string, starting
at position 1, and the third argument is the length of the string.

CATSTR allows you to concatenate a number of text strings into one:

  big_string CATSTR <string1>,<string2>

which causes big_string to be set to string1string2.

SIZESTR enables you to do a strlen on the text and would cause

  len_big_string SIZESTR big_string

to evaluate to 14.

Finally, you can determine the offset within a text string of the
substring you're searching for by using the INSTR directive. For example,
to determine if an argument contains a segment as well as an offset, you
could search for the colon in the third position:

  print_msg macro   addr colon INSTR <addr>,<:>
  if  colon gt 0
  msg_seg SUBSTR <addr>,1,2
  len SIZESTR <addr>
  lab SUBSTR <addr>,4,len -3
      push ax
      push dx
      push ds
      mov dx, msg_seg
      mov ds, dx
      mov dx, offset lab
      mov ah, 9h
      int 21h
      pop ds
      pop dx
      pop ax
  else
      push ax
      push dx
      mov dx, offset addr
      mov ah, 9h
      int 21h
      pop dx
      pop ax
  endif
  endm
  start:
     print_msg    cs:msg1
     print_msg    msg1
     int 20h
     ret
  msg1  db   'msg1$'
  end start


Predefined Macros

MASM 5.1 includes some additional macros that are predefined by the
assembler itself. @WordSize will return the size of the segment word size
in bytes, returning a 4 for 32-bit word sizes and a 2 for 16-bit word
sizes. This is very useful when you have the .386 directive turned on and
need to create a 386-specific macro.

@Cpu returns a weird bit pattern that, when interpreted, can tell you
what processor you're running on, what mode the 286 or 386 is in
(protected or real), and what type of math coprocessor is installed, if
any. Remember that this is a compile-time macro and that the actual run-
time machine may have a different configuration.

The @Version macro returns a three-character identifier in order to let
you know which version of MASM you're running: the newest release returns
"510". It is undefined in earlier versions, so you can test for it with
code such as:

  IFNDEF @Version


ELSE Extensions

Finally, at long last, no more IF kludges that act like brain-damaged case
statements! You can now test with a host of ELSEIF directives that include
those listed in Figure 4.

Some of the additions to conditional assembly directives can  make your
job a lot easier, too. Consider having a piece of code such as:

  zb proc uses bx es, p1:ptr
 ; zero out the first byte here, regardless of memory model
  if @datasize
     les  bx, p1 ; if large model
  else
     mov  bx, p1 ; if small model
  endif
  mov  byte ptr [bx], 0
  ret zb endp


Local Labels

I hate thinking up names for variables. I'm a strong believer in self-
documenting code; variable and label names ought to be mnemonic and make
some sense when reading code. This certainly helps during the documentation
phase of a project. However, thinking up unique names is tough, and always
trying to use position-relative code can be cumbersome. But with MASM 5.1
you can now use the double at-sign (@@) convention to define jumps in both
the forward and backward directions. The jumps are evaluated at compilation
time (see Figure 5).


TYPE Directives

Based on the type of an argument or variable, you can choose which
operations should take place on that argument or variable. The TYPE
directive has been expanded considerably to include the ability to
ascertain whether an object is program-related (code), data-related, a
constant, addressing mode, whether it is a register or not, whether it is
defined or not, and whether it is a local or public symbol.

The complete definition of the bit positions of the TYPE directive are
listed in the MASM manual. As an example of how handy the TYPE directive
can be, consider how easily it lets you do something like:

  print_me macro   arg
  if ((.TYPE arg) AND REGISTER)
     push arg
     push dx
     mov dx, arg
     call do_print
     pop dx
     pop arg
  elseif
     push dx
     mov dx, offset arg
     call do_print
     pop dx
  endm


CodeView(R) Enhancements

When you are using CodeView to help debug your code, it helps if the
object type itself is known to CodeView. You can enable this by using the
enhanced and extended PTR data definition word shown in Figure 6.


High-level Support

First and foremost, I am a C programmer. I sometimes find myself entering
C syntax when forced to use assembler and have been hesitant to delve into
assembler, always thinking beforehand how much trouble it is to do so.
Setting up the segments properly, remembering where and what the data
segment is, doing mixed-model programming, getting variables off the
stack, problems with underscores: it just wasn't worth the hassle.

In MASM 5.1, almost all of my complaints are in the past; it is now
significantly easier to interface with C. Though I focus on C, it is
important to note that interfacing to other high-level languages,
including BASIC, has become much easier.

As discussed previously, MASM 5.1 lets you employ the "uses" word as part
of your PROC directive to let the assembler know what registers your
routine is going to use. Using

  my_proc  proc uses ax dx
     mov dx, offset cs:my_msg
     mov ah, 9h
     int 21h
     ret
  my_proc  endp

actually generates the push ax/pop ax and push dx/pop dx pairs for you
automatically and in the right order. The basic syntax is to separate each
of the registers by white space.

When I was converting my code to MASM 5.1, that caused some trouble. I was
using a routine to save the register values and to swap the AX and BX
registers. Obviously, if you are playing any games with the values you pop
off the stack, you'll have to use the older method. Or you can go back and
figure out what the kludge was that had you doing any strange things and
either document it or change it to a more reasonable approach.

One item shocked me a bit and does not appear to be documented
anywhere: you must use the .MODEL directive with both of its arguments, or
the uses clause will not be processed properly.

The first argument that the .MODEL directive takes indicates the memory
model you wish to use, and the second one indicates which language you
wish to "configure" for. You can choose the small, medium, compact,
large, or huge memory models. Using the language specification as in

  .MODEL small,C

will set up the calling and returning conventions for you, make all
symbols publicly accessible, and append to the beginning of the ubiquitous
underline character so the linker doesn't get confused. You need to
specify BASIC, C, FORTRAN, or Pascal, as each one has its own
requirements regarding parameter and naming conventions.

The update to MASM can really be helpful in developing transportable
code-simply change the .MODEL language specifier from, say, 'C' to 'BASIC'
and MASM automatically changes the calling and parameter conventions
with no additional work. This is useful for people who produce commercial
libraries. A library for compiled BASIC is now as easy to produce as one
for C-simply change one specifier and recompile. Underscores will
magically appear and disappear as the appropriate language compiler
requires for the link process.


Stack Frame Addressing

As already mentioned, you can use the uses clause to specify which
registers to save and restore on the stack on procedure entry and exit.
But, when interfacing with a high-level language, you'll often want to
pass parameters to the assembly routine. In most languages, and in C in
particular, this parameter passing is done on the stack: each argument to
a function is pushed on the stack in either left-to-right or right-to-left
order (depending on whether or not you have specified the routine as a
Pascal routine). The enhancements to MASM allow you very easy access to
these parameters, regardless of the memory model chosen. This will save
you hours in debugging and source code rewrites, if you change memory
models frequently.

Let's assume that you're not using the Pascal calling convention, which
forces the parameters to be placed on the stack in left-to-right order.
If you've specified the C language with the .MODEL directive, then you can
use a line such as the one in Figure 7, which not only specifies that ax
should be saved on program entry, but also allows you to access three
items on the stack directly, as in:

  mov ax, arg1

It can be done without regard to the memory model used and without having
to stick the usual preamble for saving and restoring BP; that's done
automagically for you.

You can select the following "types" for your argument list: word, dword,
fword, qword, tbyte, or even the name of a STRUC. The default is either a
word (for 8086/88/286/386) or a dword if you've turned the .386 directive
on.

You can also qualify the type with the far, near, or ptr convention, as
shown by

  my_proc proc uses ax, arg1:far ptr dword

which specifies a variable that is 4 bytes long on the stack and indicates
what you'll be using it as (in this case, a far ptr). That permits you to
specify later in your code something like:

  lds dx, arg1

The pointer information isn't really used by MASM for much, except that
additional information is generated for CodeView debugging if the ptr
keyword is seen at assembly time.

The routine in Figure 8, for example, resulted in the debug output in
Figure 9 (to which I added comments). The same kind of code you'd generate
by hand, right? Obviously, I used the small model for this example.


Local Variables

In a high-level language like C, when you need a local variable you
declare it at the top of the routine, or inside a block, and space is
automatically allocated from the stack for you to play with. This space
is returned to the stack when you return. You just use the variable name,
and the compiler does the rest.

MASM has always had this capability. A macro such as

  tmp_buf equ <byte ptr [bp  - size1]>

allows you to include the tmp_buf name as if it were a true variable.
However, such variables have global scope, and I always ended up using
names like "tmp_buf_serial_routine" to differentiate it from other
tmp_buf's. Also, it never actually saved any space on the stack, and an
inadvertent push/pop sequence could change the stack in strange and less-
than-wonderful ways.

With MASM 5.1, you can now allocate local variables with only a local
scope. The names don't exist outside of the procedure in which they are
defined. For example, consider Figure 10, which causes MASM to set up a
text macro almost magically behind the scenes. Here's a simple example:

  HEADER_SIZE   equ   3
  DATA_SIZE     equ   128
  CRC_SIZE      equ   2

The source code generated by MASM 5.1 is listed in Figure 11 (the casting
of tmp_buf is because tmp_buf is being defined as a byte array). The dump,
with the calls to the other routines fabricated for the purposes of this
example, is shown in Figure 12.


Conversion Guidelines

Conversion of your nonmacro MASM code is very simple:

  ■  First convert all of your push/pop sequences into their appropriate
     uses statements. MASM isn't too smart in one regard; each return will
     generate the appropriate pop sequence, so you should convert all of
     the routines to use only one return, with jumps directed toward that
     return. This is, in any case, good programming practice.

  ■  Next, convert all of your argument processing conventions into the
     new "proc line" sequence. Also, remember to drop the "PUSH BP/MOV
     BP, SP/.../POP BP" sequences you've generated; MASM will do all
     that automatically.

  ■  Look for the areas where you've allocated space off the stack for
     local variables (try a global search for "SUB SP,"), and convert them
     over to the new local calling definition convention.

  ■  Now the hard task: converting all parts of your code into the
     appropriate text macro sequences to take advantage of the new
     features and enhancements. You have to decide for yourself if it's
     worth converting over to the new sequences. Personally, I just looked
     for the word ";kludge" in my macro files and fixed it up.

  ■  Remember to include the .MODEL directive or MASM 5.1 will basically
     revert back to its old style and give you a lot of error lines about
     "extra characters" and syntax errors galore.

MASM 5.1 is basically just an upgrade──an evolutionary not a
revolutionary step. A variety of other enhancements have been made to the
entire package as well as the assembler itself. These include the new
CodeView, the ability to link easily with OS/2 systems dynamic-link
libraries, and updates to the BIND, IMPLIB, MAKE, ILINK, and EXEHDR
utilities. Also, the Microsoft Editor is now part of the MASM
distribution package.

I was able to reduce my MASM source code line count by about 30 percent,
and I'm less hesitant to use MASM routines now when I program in C. Also,
I spend a lot less time debugging my MASM routines. As for using all that
extra time the upgrade gives me to document my code, well, as soon as I
finish this one routine, all right?


Figure 1:  Under earlier versions of MASM, the programmer needed to create
           user stacks and was responsible for the pushing/popping of
           appropriate registers.

routine proc  <whatever>
   push bp
   mov bp, sp
   push ax
   mov ax, ss:[bp + MEMORY_MODEL + OFS_VAR1]
     <other code>
     pop ax
   pop bp
   ret
routine  endp


Figure 2:  Example of a Text Macro

errprint equ <err_msg1, err_msg2, err_msg3>


Figure 3:  Sample Code Using SUBSTR

reg_push macro   start
reg substr <axbxcsdxsidib>,(start - 1) * 2 + 1, 2
   push reg
endm


Figure 4:  New ELSEIF Options

ELSEIF <test>                 if <test> evaluates to true
ELSEIF <test>                 if <test> evaluates to false
ELSEIF1                       if pass1
ELSEIF2                       if pass2
ELSEIFB <arg>                 if blank
ELSEIFNB <arg>                if not blank
ELSEIFDEF <arg>               if defined
ELSEIFNDEF <arg>              if not defined
ELSEIFDIF <arg1>, <arg2>      if different
ELSEIFDIFI <arg1>, <arg2>     if different, regardless of case
ELSEIFIDN <arg1>, <arg2>      if identical arguments
ELSEIFIDNI <arg1>, <arg2>     if identical, regardless of case


Figure 5:  Sample Code Using the New @@ Convention

\print_me macro   args
   irp print_arg, <args>
     mov dx, print_arg
     call print_it
     jnz @F       ; error, leave "loop"
     call print_it_part_two
     <some_other_convoluted_macro_calls>
   endm
@@:
endm


Figure 6:  Defining Object Types for CodeView

print_msg db  "This is a test message"
near_ptr dw  byte ptr print_msg   ; a near pointer
far_ptr  dd  byte ptr print_msg   ; a far pointer


Figure 7:  This sample code specifies that ax must be saved and lets you
           directly access three items on the stack. Note that you can
           specify a type for each item on the list.

my_proc  proc uses ax, arg1:word, arg2:dword, arg3:ptr dword


Figure 8:  The "ptr" keyword causes information to be generated that can be
           used by CodeView. See the debug output in Figure 9 below.

my_proc proc uses ax cx dx ds es,arg1:word, arg2:dword, arg3: far ptr
dword
   mov ax, arg1
   lds dx, arg2
   les cx, arg3
   ret
my_proc endp


Figure 9:  Code fragment produced by CodeViewfrom the code shown in
           Figure 8 above.

PUSH  BP         ; standard preamble for stack frame
MOV   BP,SP
PUSH  AX         ; save the "uses" registers
PUSH  CX
PUSH  DX
PUSH  DS
PUSH  ES
MOV   AX,[BP+04] ; arg1
LDS   DX,[BP+06] ; arg2
LES   CX,[BP+0A] ; arg3
POP   ES         ; restore the "uses" registers
POP   DS
POP   DX
POP   CX
POP   AX
POP   BP         ; and the stack frame register
RET              ; the calling routine removes vars
                 ; from the stack in C, so we don't


Figure 10:  Sample code defining local variables with local scope.

LOCAL tmp_buf[size1]:byte, tmp_buf2[size2]:word, var1:dword


Figure 11:  Sample Source Utilizing Local Variables.

make_xmodem_block proc uses cx si di ds es,source:far ptr dword,
data_len:word
local  tmp_buf[HEADER_SIZE + DATA_SIZE + CRC_SIZE]:byte
   lds si, source
   push es
   push ss
   pop es
   mov di, word ptr (tmp_buf + HEADER_SIZE)
   mov cx, data_len
   rep movsb
   call do_rest
   call do_send
   ret
make_xmodem_block  endp


Figure 12:  Actual MASM listing generated from the code shown in Figure 11.

PUSH  BP           ; standard preamble
MOV   BP,SP
SUB   SP,0086      ; reserve some space for loc var tmp_buf
PUSH  CX           ; save registers
PUSH  SI
PUSH  DI
PUSH  ES
PUSH  DS
LDS   SI,[BP+04]   ; pointer to the source
PUSH  ES           ; cause ES to address stack
PUSH  SS
POP   ES
MOV   DI,[BP+FF7D] ; pointer to the local var
MOV   CX,[BP+08]   ; get the length off the stack
REPZ
MOVSB              ; move the bytes in
CALL  DO_REST
CALL  DO_SEND
POP   DS           ; restore the registers
POP   ES
POP   DI
POP   SI
POP   CX
MOV   SP,BP        ; restore the stack
POP   BP           ; and finally BP
RET
████████████████████████████████████████████████████████████████████████████

Color Mixing Principles and How Color Works in the Raster Video Model

Kevin P. Welch☼

In the last few years a dramatic revolution in computer graphics has
occurred. When I first started working with computers, I was content to
use a small 80-character by 25-line monochrome terminal with no graphics
capabilities. Now I feel confined if the display has a resolution of less
than 640 by 480 pixels. Today displays with resolutions of 1,280 by 960
pixels are common, and some are even higher. We used to be satisfied with
a monochrome display; however, now we expect at least eight or 16 colors
from even the simplest display subsystem.

This emergence of color graphics on personal computers has had a profound
effect on the software currently being written. Desktop publishing
systems, presentation graphics aids, drawing applications, and image
processing tools are partly made possible by this technology. Yet despite
these advances, we still take for granted the way that computers use
color. An analysis of the situation reveals that even the best display
systems available today fall far short of the real world.


Color Modeling

Color is very subjective and depends entirely on our perceptions. The
colors we see depend on the physical properties of the object we are
looking at, the light sources that illuminate that object, and the
physiological and perceptual characteristics of the human eye.

The process of perception is very complex, and no single physical
interpretation has yet been able to explain adequately even the simplest
observations. For example, from a physical perspective we are able to
perceive subtle differences in the intensity of any particular color but
our ability to detect small changes in hue, or subtle changes from one
color to another, varies greatly. In fact, many different energy
distributions of light appear, for all practical purposes, to be
identical.

For centuries man has attempted to characterize these physical
observations and harmonize them with the colors we perceive. Two
general characterizations have emerged from this investigation.

The first approach is that used by artists: tints, shades, and tones (see
Figure 1☼). This subtractive model results from the way in which an
artist might paint an image on white paper. Progressive layers of pigment
are added until the desired effect is achieved. White is achieved simply
by the absence of pigment, and black by the combination of all
pigments. A tint results from reducing the saturation of a pure
pigment, or in effect mixing it with white. A shade comes from mixing a
pure pigment with black, effectively reducing its brightness. Finally, a
tone results from the addition of both black and white to a pure pigment.
These steps combined produce different colors of the same hue but with
varying degrees of brightness and saturation.

The next approach involves hue, saturation, and brightness. From a
descriptive point of view this adds a different dimension to the
traditional tint, shade, and tone characterization used by artists. A
hue refers to a particular color, such as red, green, or blue.
Saturation is a little more complicated in that it refers to purity of
color, that is, how strong it appears. For example, if you mix pure red
light with white light you get pink. This pink color is like red, only
less saturated. Finally, brightness refers to the physical intensity or
luminance of the color and is independent of the actual hue and saturation
involved.

From a mathematical perspective, both of these models are difficult to
handle when it comes to describing several different colors. Often the
most practical means to measure color involves comparing an unknown with a
known set of samples, just like the color cards you get when you purchase
a can of paint.

A more objective approach to comparing colors is to classify the
perception (usually visible light) in terms of dominant wavelength,
purity, and luminance. The resulting spectral energy distribution then
determines a "color." The term dominant wavelength is derived from the
fact that our eye tends to respond to one particular wavelength when
viewing light, which determines the color we see. Purity refers to the
mathematical combination of colored and white light. A completely pure
color contains no white light and is often called 100 percent saturated.
Luminance refers to the overall energy of a particular color. This
involves both the purity and dominant wavelength of the light we see──the
more energy, the more visible the particular color.


Color Mixing Models

From a physiological perspective, it has been theorized that the retina
of the human eye contains three kinds of cones, each sensitive to red,
green, and blue light, respectively. This hypothesis, despite other recent
vision theories, has been extended to the field of computer graphics.
Most color monitors use red, green, and blue phosphor in an attempt to
simulate the human eye.

The extension of the red-green-blue hypothesis to color monitors and other
hardware-oriented systems has sparked considerable interest in the
development of models that facilitate the convenient specification of
color. The resulting models shape our view of how these systems utilize
color. Although one model may do particularly well in one situation and
poorly in another, in the end all the color mixing models are
theoretically equivalent in that they are capable of producing the same
results, albeit with various degrees of ease.


RGB Mixing Model

The red-green-blue (RGB) mixing model (see Figure 2☼) is hardware-oriented
and is a simple extension of the physical properties of color monitors. In
this model the three primary colors are mixed together in a Cartesian
coordinate system. The final result of the mixture can be considerably
different from the primaries involved. For example, the lack of color is
black (defined as the RGB triple 0-0-0), and the presence of all color is
white (1-1-1). The combination of two primary colors produces a
secondary color mixture. This can be demonstrated by the creation of
yellow (1-1-0) from pure red (1-0-0) and pure green (0-1-0).

Note that in the theoretical RGB mixing model each primary color is
specified by a continuous value in the range {0,1}. Most physical
implementations of this scheme use some sort of discrete mapping from
this range. Microsoft Windows, for example, via the Graphics Device
Interface (GDI), only supports colors on the integral range {0,255}.

Palette, a Windows color-control program detailed in the accompanying
article ("Creating User-Defined Controls for Your Own Windows
Applications,"), for example, supports the RGB mixing model but
converts the selected color to a CRT equivalent supported by GDI. This
conversion is performed via the following simple formula:

  crtValue = rgbValue * 255
  rgbValue = crtValue / 255

Using Palette you can easily experiment with varying amounts of primary
color. In Figure 3☼ you can see that by combining pure red and green you
can obtain pure yellow, assuming, of course, that you have a color
display. If you search you may be able to find some other pure colors that
do not occur at the corners of the RGB color cube. If you try this same
experiment with different display subsystems you will probably get
different results.

If you are using a monochrome system the colors will hopefully appear as
a varying mixture of black and white pixels. On a color system the number
of pure colors supported depends on the underlying hardware
characteristics and the implementation dependencies imposed by the GDI
display driver you are using. Some display drivers are very good at
approximating colors that fall between primary ones; others simply use the
closest possible primary color.

As you experiment you'll see that the red, green, and blue spectra
displayed are themselves samples from the color cube. These samples were
created with a simple algorithm that traverses an edge of the RGB color
cube:

  for ( i=0; i<16; i++ )
  {
   rgbValue = i / 15.00;
   crtValue = rgbValue * 255;
  }

You could create your own mixing table by using a different algorithm and
substitute it in place of the one displayed.


CMY Mixing Model

The cyan-magenta-yellow (CMY) mixing model is a variant of the RGB model
that uses subtractive primaries instead of additive ones; in other words,
its effect is to subtract color from white light. The presence of all
color results in black and the absence in white.

It is important to understand the CMY model since it is used in many hard-
copy output devices. The notion of adding color or pigment to white paper
is employed in plotters, ink jet, and laser printers.

Mathematically, the CMY mixing model can also be described using a
Cartesian color cube (see Figure 4☼). The resulting subspace is identical
to the RGB cube except that white is at the origin instead of black. While
examining the CMY color cube you should notice the diagonal line between
the black and white corners. Color triples on this line result in various
mixtures of gray. Using Palette you can produce a gray in both the RGB and
CMY models by picking the same intensity value for each of the primary
colors.

With Palette you will notice something else about the CMY and RGB mixing
models: a gray value component is always produced whenever a color
mixture involves all of the primaries. For example, suppose you have an
equal mixture of cyan and magenta that results in a tint of blue. If you
progressively add yellow (up to the level of the original colors), you can
fade the tint into gray. Unfortunately this is a little more difficult
when you start with a tint of unequal amounts of cyan and magenta.

Fortunately the CMY and RGB models are simply related:

  Cyan    = 1 - Red
  Magenta = 1 - Green
  Yellow  = 1 - Blue

Using this relationship you can easily create a transformation that
converts any CMY value to an RGB value and vice versa.


YIQ Mixing Model

The YIQ mixing model, like the CMY and RGB models, can also be described
by using a Cartesian coordinate cube. This model is of particular interest
as it represents a recoding of the RGB model, optimized for transmission
efficiency and downward compatibility with monochrome display
subsystems, for example, a black-and-white television.

The Y component of this model represents a primary whose spectral energy
distribution matches the luminosity response curve, implying that the Y
component is a gray-scale equivalent for the defined color. This resolves
an important television broadcasting problem in which the same signal is
received by both color and black-and-white television sets. If the RGB
mixing model is used, two distinguishable shades on a color television can
result in the same luminances on a black-and-white television. The YIQ
mixing model resolves this problem by mapping different colors to
different intensity levels.

Although Palette does not implement the YIQ mixing model (you can easily
do it if you are so inclined), you can calculate the YIQ equivalent of
an RGB color value with the empirical formula shown in Figure 5.


HSV Mixing Model

The HSV mixing model, unlike the three previous hardware-oriented models,
is a user model based upon the artist's concept of hue, saturation, and
value. This model has several distinct advantages over the other models.

From a topological point of view, the HSV model can be described as a
hexcone, or a six-sided cone (see Figure 6☼). The bottom of the hexcone
corresponds to V = 0 and represents black. The top of the hexcone
represents white with V = 1. Each vertex of the hexcone corresponds to
one of the primary or secondary colors and is referenced by H in
degrees. For example, red corresponds to a hue of 0 degrees, yellow to 60
degrees, and so on around the cone. The remaining parameter, saturation,
corresponds to a vector that extends horizontally from the value axis. The
defined color is always a gray, ranging from black to white, when S = 0.
When S is greater than 0, a particular tone is defined. This tone
becomes increasingly more saturated until S = 1. Any color obtained when
S = 1 and V = 1 corresponds to a pure pigment that an artist would use.

By maintaining a constant saturation, say equal to 1, you can easily
traverse all the primary and secondary tints by cycling the hue from 0 to
360 degrees. Once a given tint is produced, it can easily be faded to
white by gradually scaling the saturation back to zero. Alternatively,
any tint can be faded to black by reducing V until black is reached.

If you experiment with Palette using the HSV mixing model, you can easily
see how intuitive this approach is. In fact, some of the more trivial
color transitions obtained with this model are difficult to achieve when
using one of the other models.

The default spectrum provided for you when using this mixing mode was
selected by traversing various paths through the hexcone. The default hue
spectrum was calculated by holding S = 1 and V = 1 and cycling H from 0
to 360 degrees. Note that the values 0 and 360 are equivalent-once you've
traversed a full 360 degrees on a circle you return to your starting
point. With Palette, the highest value you can obtain on a linear scale
is 340. The next step is back to 0 (or 360). The saturation spectrum
was calculated at H = 240 degrees (blue) and V = 1. In this case,
various S values were used, ranging from 0 to 1. Finally, the default
value spectrum was determined by slowly increasing V from 0 to 1 with S
= 0 and H = 0.

One interesting aspect of the HSV approach is its relationship to the RGB
mixing model. If you view the RGB color cube along its principal diagonal,
it becomes like a constant V slice through the hexcone. Each plan of
constant V corresponds to a subcube in the RGB cube (see Figure 7☼).


Windows Palette

In the accompanying article, you will note that at the end of the
PALETTE.C module there are a number of color conversion routines that
Palette uses when working with a particular mixing model. Whenever you
define a new color the program automatically converts it to an equivalent
representation in one of the other mixing models. Also, when you select a
new color mixing model (using one of the radio buttons), Palette searches
for the best possible match to the current color using the new model.

This mathematical interpolation-and-search process is somewhat
inexact; however, it gives you a good idea of how to create a similar
color using a different model. Although in most cases the match is exact,
the quantized nature of the spectrum control may result in some
inconsistencies.

The routines in PALETTE.C that are used to perform this color conversion
are shown in Figure 8. In the previous discussion we presented all of
the algorithms except for the HSV model. This algorithm is a little more
complicated (mathematically it is a form of topological convolution)
and is best left for another discussion. If you are interested, several
texts on computer graphics are available that discuss this model and
contain the RGB-to-HSV conversion algorithm.

Besides being an interesting program that shows the use of customized
dialog box controls, Palette also provides insight into color mixing
models and their device-specific dependencies.

This discussion has, we hope, given you a better understanding of these
fundamentals and removed some of the uncertainties surrounding Windows
GDI. Furthermore, the color conversion routines presented might be of
good use when you build your next Windows application.

Many good texts contain a thorough presentation of color, color mixing
models, and interactive computer graphics in general. One of the more
helpful books is Fundamentals of Interactive Computer Graphics, by J.D.
Foley and A. Van Dam, which is published by Addison-Wesley.


Figure 5:  This formula calculates YIQ equivalents of RGB values.

Y = (0.30*Red) + ( 0.59*Green) + ( 0.11*Blue)
I = (0.60*Red) + (-0.28*Green) + (-0.32*Blue)
Q = (0.21*Red) + (-0.52*Green) + ( 0.31*Blue)


Figure 8:  The routines shown are used by Palette to convert color values
           from one mixing model to another.

RGBtoCRT()     convert from RGB to CRT (GDI RGB) model
RGBtoCMY()     convert from RGB to CMY model
RGBtoHSV()     convert from RGB to HSV model

CRTtoRGB()     convert from CRT to RGB model
CMYtoRGB()     convert from CMY to RGB model
HSVtoRGB()     convert from HSV to RGB model

████████████████████████████████████████████████████████████████████████████

Creating User-Defined Controls for Your Own Windows Applications

Kevin P. Welch☼

Microsoft(R) Windows, like other graphical environments, appears to suffer
from a limitation that programmers frequently complain about──they are
restricted to a predefined set of tools that virtualize the host machine.
However, buried deep within the Windows Software Development Kit (SDK) are
clues to suggest that this may not be the case. The cooperative nature of
Windows offers a number of mechanisms that can be employed to create new
and dramatically different software objects.

One of the most exciting of these mechanisms is the ability to define
your own dialog box controls. This lets you functionally extend Windows,
making your application more appealing visually and enhancing the user
interface. For example, you can define controls to represent switches,
gauges, rulers, and other useful tools. We will explore the workings of a
particular control called Spectrum and investigate its use in a color
mixing program called Palette. In the preceding article, "Color Mixing
Principles and How Color Works in the Raster Video Model," we explored
various theoretical color mixing models. Palette can be used to
demonstrate three of those representations.


Control Fundamentals

Almost everything of importance in the Windows environment is some kind
of window. Standard dialog box controls, as you would expect, are
predefined windows that can be used throughout your application.

These controls are similar to the very windows registered and created by
your own programs. However, unlike most of the windows you create,
controls are by necessity object-oriented in nature and consist of
completely reentrant code. This implies that a control should not access
static data nor be dependent on the fact that only one thread of
execution may be present. Also, in order for the control to be
correctly handled by the dialog box manager, it must process all keyboard
or mouse input received in a consistent manner.

Although the details involved in implementing a control can get rather
complicated, the basic steps are simple.

Come up with a class name. Each control must be referenced by a unique
name. This same class name is also used in your RC file whenever you wish
to use the control in a dialog box. Be careful not to use one of the
predefined class names provided by Windows; the results can be quite
spectacular if you accidentally duplicate one of these names.

Define all instance variables. Instance variables are private static data
associated with each instance of your control. This data must be
allocated dynamically and be readily accessible to each thread of
execution. Although you could develop your own scheme for instance
variables, you should probably consider using local or global atoms,
property lists, or perhaps defining extra bytes to be associated with
every instance of the window class data structure.

Define your control window function. Each control must have an associated
window function responsible for processing all related messages. Since
your control will usually be managed by the dialog box manager, you should
consider implementing both a keyboard and a mouse user interface. At the
very least, your window function should handle the following messages:

  ■  WM_CREATE. When you receive this message you should initialize any
     instance variables and perform all other necessary initialization
     steps.

  ■  WM_PAINT. After receiving this message, you should perform any
     painting desired in the client region of your window. You should be
     careful to tune the results of your paint operation for a variety of
     display devices, especially those of radically different pixel-aspect
     ratios. This might be the most crucial and time-consuming part of
     the entire control development process.

  ■  WM_DESTROY. This message indicates that you should release any
     memory associated with the control instance and perform any other
     cleanup needed before the window is destroyed.

If your control must be able to receive input (both mouse and keyboard),
you should consider handling one or more of the following messages:

  ■  WM_GETDLGCODE. Your response to this message determines how the
     dialog box manager handles your control. By responding to this
     message with one of the return codes shown in Figure 1, you can
     manage a particular type of input and process the data yourself.

  ■  WM_SETFOCUS. When you receive this message you should create and
     display some sort of caret to indicate to the user that you have
     possession of the input focus.

  ■  WM_KILLFOCUS. This message is received whenever some other window
     receives the input focus. If you have a caret defined you should
     destroy it at this time.

  ■  WM_KEYDOWN. This message is received whenever a key is pressed or
     held down. By processing the virtual key codes you can perform any
     necessary actions. Be sure to notify the parent window whenever an
     action is taken that may result in a new system state, such as a
     button being pressed or an item selected from a list box.

  ■  WM_LBUTTONDOWN. This message is received whenever the user presses
     the left mouse button inside the control window. In most cases you
     will want to capture the system focus, dis-play your caret, and
     capture mouse movements. If the mouse click resulted in a new
     selection, you may also want to notify the parent window of the
     change.

  ■  WM_MOUSEMOVE. Although in most situations you will want to ignore
     this message, it may sometimes be desirable to perform an action if
     it is received in the context of a mouse-drag  operation.

  ■  WM_LBUTTONUP. This message is received whenever the user releases
     the left mouse button. If you are capturing mouse movements, you
     should be careful to release the capture at this time. Note that it
     is entirely possible to receive extra WM_LBUTTONDOWN and WM_LBUTTONUP
     messages, so you should make sure to exclude those that are out of
     context.

Register the control class. Before using the control in an application,
you have to register the control window class. Any attempt to use the
control before it has been registered will result in a serious system
error and may even prevent your application from running. In practice it
is a good idea to define a specific function as part of the entire control
package that performs this task.

Export the control window function. As is the case with any other window
function in your application, you must export it before use. Failure to do
so is a very common problem encountered by even the most experienced
Windows programmers.

Define related utility functions. The creation of even a simple control
often requires the definition of a number of associated utility
functions that enable the host application to define or retrieve various
control-related parameters. Although a carefully defined message scheme
will sometimes suffice, in more complicated situations it is better to
create a specific function that performs each desired action. This
provides an even greater degree of encapsulation for the control and
promotes the development of self-contained software objects.

Include the control in desired RC files. Generally, you want to use the
dialog box editor (part of the Windows SDK) to create the templates for
inclusion in your RC file; unfortunately, the dialog box editor does not
directly support user-defined controls. I have found it helpful to define
the size and location of the control using a static frame. When completed
I edit the resulting DLG file contents and replace the frame with the
appropriate control. Be sure to note that the dialog box editor is not
capable of handling RES files that contain references to such user-defined
controls.


Spectrum Control

The best way to explain the definition and implementation of user-defined
controls is to look at a particular example. The Spectrum control is a
particularly interesting one to study as it has a well-developed user
interface that utilizes both local and global instance data.

The Spectrum control was a result of my frustration with the lack of a
system-defined mechanism for selecting colors. At the time, many of the
applications I was developing required the user to be able to choose
one or more colors from a predefined palette. After several attempts
using list boxes and scroll bars I decided that it would be advantageous
to design a series of general-purpose controls to solve this and several
other related user-interface problems. The result of this development
was the creation of a library of controls ranging from gauges, switches,
and buttons to sliders and other selection tools. The Spectrum control
is a simplification of a tool developed for this library.

With the Spectrum control you can define a sequence of colors from which
the user can select. Each color maintained by the control is associated
with a numerical value that is returned to the parent window each time a
new selection is made. This numerical value can be a character, integer,
long, or float. In certain situations this numerical value may even be the
same red-green-blue (RGB) color value that is displayed.

The currently selected color is indicated by a small rectangle that
surrounds the color in question. When the Spectrum control has the input
focus, the system caret is also displayed inside this rectangle. If the
caret is visible, the selector can be moved with the cursor keys or by
dragging it to the left or right with the mouse.

Before using the Spectrum control in a dialog box it must be registered,
which is done by calling the RegisterSpectrum function. See Figure 2 for
a listing of several of the functions referred to for Spectrum.
(Complete source code listings and executables for Spectrum and Palette
may be downloaded from any one of our bulletin boards-Ed.) The only
unusual aspect of this function is the use of the cbWndExtra field in the
WndClass data structure. The value specified defines the number of extra
bytes to be allocated with each instance of the Spectrum window. These
extra bytes can then be accessed with the functions SetWindowWord and
GetWindowWord. Note the various definitions at the beginning of
SPECTRUM.C that provide access to these extra bytes of data. If you
compare the offsets used to those predefined for GetWindowWord, you will
discover that those defined in WINDOWS.H are negative offsets. This
means that those bytes allocated under the cbWndExtra specification are
always positive and start at offset zero.

The SetSpectrum function is a small utility function that permits you to
define the selected color patch from the palette currently available.
Note how the selector index is saved as an instance variable using a
predefined window offset. Like SetSpectrum, GetSpectrum is an analog
function that lets you retrieve an index to the currently selected color
patch. Although this function is extremely simple, it is useful because it
allows the host application to remain unaware of the internal
implementation details of the Spectrum control.

As I mentioned previously, when an instance of the Spectrum window is
created, a default set of colors is automatically generated. The
SetSpectrumColors function allows you to replace this default set of
colors and associated numerical values with ones that you define. In
doing so, you reset the selected color to the first one in the range and
the parent is notified of the change. Note that the array of colors
offered consists of a series of RGB colors followed by a series of
associated numerical values that are returned when the color is
selected.

The GetSpectrumColors function permits you to retrieve the current color
and value table maintained by the Spectrum control. No check is made to
verify that sufficient space is available for the list of values. This can
easily be added but the host application is usually well aware of the
range of values it might be expected to support.

The SpectrumWndFn function is possibly the most complicated part of the
code associated with the Spectrum control. Although simple in structure,
it is carefully designed to operate in conjunction with the dialog box
manager. This is illustrated by the way in which it responds to the
WM_GETDLGCODE message. In this case, DLGC_WANTARROWS, the value returned,
informs the dialog box manager that the control wants to process all
cursor-related keystrokes. This enables the control to provide a
keyboard user interface for color selection.

Note how the WM_SIZE command is processed. Under most normal
circumstances controls are not resized; however, the handling of this
message supports this possibility.

Throughout the function SpectrumWndFn you will see several SendMessage
function calls, which notify the parent whenever the current selection is
changed. As with most other controls, information is returned to the
parent window through a WM_COMMAND message. The wParam portion of this
message is equal to the child window ID (this value is retrieved by using
one of those negative window offsets), and the lParam portion is equal to
the actual numerical value associated with the selected color.

DrawSelector is the last function in SPECTRUM.C. This internal utility
function is responsible for drawing the selector that highlights the
selected color patch. Note how a TRANSPARENT drawing mode is used with
the R2_NOT operator. This permits subsequent calls to the function to
effectively restore the display to its original condition, eliminating
the need to paint part of Spectrum between movements of the selector.


Windows Palette

Now that you have a reasonable understanding of how a typical user-
defined control operates, we will investigate its use in the context of a
small application. The Windows Palette is a logical extension of the
COLORSCR program presented by Charles Petzold (see "A Simple Windows
Application for Custom Color Mixing," MSJ, Vol. 2, No. 2). Like
COLORSCR, Palette enables you to mix RGB colors to create a custom color
mix. However, unlike COLORSCR, Palette uses the Spectrum control and
allows you to mix colors with two additional color mixing models (see
Figure 3☼). Furthermore, when you switch mixing models, Palette will
attempt to match your currently defined color with a mixture from the new
model (see Figure 4☼).

If you examine PALETTE.H you will see the data structures used by Palette
when it operates using the four different color mixing models it
supports:

  CRT  GDI red-green-blue
  RGB  red-green-blue
  CMY  cyan-magenta-yellow
  HSV  hue-saturation-value

The CRT model is directly analagous to the RGB one supported by the
Graphics Device Interface (GDI); note that the GDI RGB model is different
from the theoretical RGB model supported by Palette. In the process of
mixing colors the RGB, CMY, and HSV models are used to define an
equivalent GDI RGB (hereafter referred to as CRT) value, which is
displayed in the upper right corner of the Palette window. For more on the
color mixing models, refer to "Color Mixing Principles and How Color Works
in the Raster Video Model,".

The window used by the application is defined in PALETTE.RC as the
Palette dialog box. Although this window was originally designed using
the dialog box editor (part of the Windows SDK), it was manually changed
in a few important areas.

The first of these changes was adding WS_MINIMIZEBOX to the list of dialog
box-style attributes. In combination with a minor change to the window
class data structure, this enables the dialog box to become iconic.

The second change was made to the ID_SAMPLE button. The button style was
modified so that it would be initially disabled and the parent window
would be responsible for painting the button contents. This was
accomplished by changing the button style to BS_USERBUTTON| WS_DISABLED|
WS_CHILD.

The third change was to add three instances of the Spectrum control to the
dialog box definition. When using the dialog box editor, the size and
location of the Spectrum control were initially determined by using a
static frame. Each frame was subsequently removed, and the Spectrum
control inserted as a replacement using the original dimensions.

At the start of PALETTE.C. nine small arrays that contain the default
color tables for the Spectrum control windows are defined. The first three
tables define the default values for the red, green, and blue spectra.
Note that each table consists of 16 CRT values followed by 16 long
representations of their floating-point equivalents. (In the
theoretical RGB model each color value is in the continuous range {0,1},
whereas the CRT model utilizes discrete values in {0,255}.) The second and
third triplets of tables define, respectively, the default values for the
CMY and HSV mixing models.

Although the Spectrum control does not put an upper bound on the size of
the color table, each color patch must be sufficiently large in order to
be easily visible. When considered along with the upper size limits
suggested for dialog boxes in the Windows Style Guide, this means that a
practical upper bound is about 30 color values.

The WinMain function in PALETTE.C (see Figure 5 for listings of various
functions) is different from those you have experienced in the past.
Unlike a conventional WinMain, this one does not have a message
processing loop──the actual retrieval and dispatching of messages is left
to the dialog box manager. Apart from creating the Palette dialog box, the
only other purpose of this routine is to register the Spectrum control by
using the current instance handle as a parameter. Note that if a previous
instance of the application exists, the Spectrum control is not
reregistered.

The PaletteDlgFn function, like most other dialog functions, is
responsible for processing all messages relating to the Palette dialog
box. Most of this function consists of relatively standard Windows code
with exceptions in a few critical areas.

The first exception is the way the WM_INITDIALOG message is handled. In
this section the dialog box icon (initially NULL) is defined with a
SetClassWord function call. This subtle change, when combined with the
addition of WS_MINIMIZEBOX to the window style, allows the dialog box to
be made iconic.

The second exception is the handling of the Spectrum child window IDs in
the processing of the WM_COMMAND message. As noted in the discussion of
the SpectrumWndFn function, a WM_COMMAND message (with wParam equal to
the child window ID and the lParam equal to the associated numerical
value) is generated whenever the user selects a new color from the
Spectrum. When this message is received by the dialog function, the
appropriate color value is defined (using a long-to-float conversion), the
corresponding color values calculated, and the numerical value displayed
in the text field to the right of the Spectrum control.

The third critical area of this function is the handling of the ID_SAMPLE
ID when processing the WM_COMMAND message. The Palette dialog box
contains a user-defined button; whenever this button needs repainting,
HIWORD(lParam) is equal to BN_PAINT and LOWORD(lParam) to the button
window handle. By retrieving the display context for this button we can
create a sample of the currently selected color mixture.

The SelectMixingModel function is responsible for switching the Palette
dialog box to a different color mixing model whenever a new one is
selected. In doing so it updates the related text fields and displayed
color spectrum to reflect the new model. It also searches for the closest
possible match, using the new model, to the currently defined color.
Although this matching process will usually result in a nearly
identical color, there are situations when the integral nature of the
Spectrum control hinders this process, resulting in a slight shift in
color.

The MatchColor function is responsible for determining the best possible
match by using the new color spectrum table. This matching process,
although only involving local optima or extreme points, is relatively
robust and produces a reasonable solution in most situations.

The DlgPrintf function responds like sprintf except that the output is
directed to a dialog box text control. For those unfamiliar with the
constructs used in this routine, the vsprintf function is a standard
Microsoft C Version 5.0 library call that performs an sprintf using a
variable number of arguments.

The remainder of the Palette module consists of six color conversion
procedures that are responsible for converting one color value into
another. Each of these routines is described in detail in the preceding
article.


Building Palette

To build PALETTE.EXE, you need the Microsoft C 5.0 Compiler (or a later
version) and the Windows Version 2.03 Software Development Kit. Before
building the application you may need to make a few changes to the LNK
and DEF files in order to account for the specific organization of your
hard disk.

Once you have made these changes, you can create PALETTE.EXE by entering
the following command:

  MAKE PALETTE

After compiling and linking the program you can experiment with the
Palette on your own computer. As you can see, designing your own dialog
box controls is a simple task that can greatly enhance any user
interface. With a little effort you can easily rework Spectrum into
several other interesting controls. Before long you'll be well on your
way to developing your own toolbox of software objects for that next
exciting project.


Figure 1:  Return Codes for WM_GETDLGCODE

DLGC_WANTALLKEYS  intercept all keyboard input
DLGC_WANTARROWS   intercept only cursor/direction keys
DLGC_WANTCHARS    intercept only WM_CHAR messages
DLGC_WANTMESSAGE  intercept all messages
DLGC_WANTTAB      intercept tab key


Figure 2:  Code Fragments from SPECTRUM.C

/*
 * WINDOWS SPECTRUM CONTROL
 *
 * LANGUAGE          : Microsoft C 5.0
 * TOOLKIT           : Windows 2.03 SDK
 * MODEL : Small or Medium
 * STATUS            : Operational
 *
 * 03/20/88 - Kevin P. Welch - initial creation.
 *
 */

#include <windows.h>
#include "spectrum.h"
                                    ∙
                                    ∙
                                    ∙
/*
 * RegisterSpectrum( hAppInstance ) : BOOL
 *
 *       hAppInstance    application instance handle
 *
 * This function is responsible for the definition and registration
 * of the spectrum window class.  Note that this function should
 * only be called ONCE (typically during the initialization phase
 * of the host application) within a program.
 *
 * A value of TRUE is returned if the registration operation was
 * performed sucessfully.
 *
 */

BOOL FAR PASCAL RegisterSpectrum( hAppInstance )
  HANDLE hAppInstance;
{
  /* local variables */
  WNDCLASS           WndClass;     /* window class data structure */

  /* Define spectrum window class. Note that no check is made to
   * see if it has been previously registered! It is up to the host
   * application to prevent multiple registrations.
   */

  memset( &WndClass, 0, sizeof(WNDCLASS) );

  WndClass.lpszClassName =    (LPSTR)"Spectrum";
  WndClass.hCursor =          LoadCursor( NULL, IDC_ARROW );
  WndClass.lpszMenuName =     (LPSTR)NULL;
  WndClass.style =            CS_HREDRAW|CS_VREDRAW;
  WndClass.lpfnWndProc =      SpectrumWndFn;
  WndClass.hInstance =        hAppInstance;
  WndClass.hIcon =            NULL;
  WndClass.cbWndExtra =       SPECTRUM_EXTRA;
  WndClass.hbrBackground =    (HBRUSH)(COLOR_WINDOW + 1 );

  /* register spectrum window class & return */
  return( RegisterClass( (LPWNDCLASS)&WndClass ) );

}
                                    ∙
                                    ∙
                                    ∙
/*
 * SetSpectrumColors( hWnd, pwRange, prgbList ) : BOOL
 *
 *       hWnd        handle to spectrum control in dialog box
 *       pwRange     number of color entries in rgb list
 *       prgbList    list of rgb colors & values for spectrum
 *
 * This function enables the caller to define a new spectrum
 * with associated lookup values for the control.  It is left
 * up to the calling routine to determine the appropriate rgb
 * colors and lookup values.  Note that the selected color is
 * always reset to the first available entry whenever a new
 * range is defined.
 *
 * A value of TRUE is returned if a new rgb color list was
 * sucessfully defined.
 *
 */

BOOL FAR PASCAL SetSpectrumColors( hWnd, pwRange, prgbList )
  HWND   hWnd;
  WORD * pwRange;
  LONG * prgbList;
{
  /* local variables */
  WORD   wEntry;         /* temporary color entry */
  HANDLE hrgbList;       /* handle to rgb list */
  LONG FAR *lprgbEntry;  /* pointer to rgb list */
  RECT   rectClient;     /* client area rectangle */

  /* release previous table from memory */
  GlobalFree( TABLE );

  hrgbList = GlobalAlloc( GMEM_MOVEABLE,
                           sizeof(LONG)*(*pwRange)*2L);
  if ( hrgbList ) {

     /* define initial rgb colors & values */
     lprgbEntry = (LONG FAR *)GlobalLock( hrgbList );
     for ( wEntry=0; wEntry < *pwRange; wEntry++ ) {
        lprgbEntry[wEntry] = prgbList[wEntry];
        lprgbEntry[(*pwRange)+wEntry] =
        prgbList[(*pwRange)+wEntry];
     }
     GlobalUnlock( hrgbList );

     /* retrieve current window dimensions */
     GetClientRect( hWnd, &rectClient );

     /* re-define instance variables */
     SET_RANGE( *pwRange );
     SET_TABLE( hrgbList );
     SET_WIDTH( (rectClient.right-rectClient.left)/(*pwRange) );
     SET_HEIGHT( rectClient.bottom-rectClient.top );
     SET_CHOICE( 0 );

     /* update window & notify parent of new selection */
     InvalidateRect( hWnd, NULL, TRUE );
     SendMessage(PARENT, WM_COMMAND,ID, lprgbEntry[RANGE+CHOICE]);

     /* normal return */
     return( TRUE );

  } else
     return( FALSE );

}
                                    ∙
                                    ∙
                                    ∙
/*
 * SpectrumWndFn( hWnd, wMsg, wParam, lParam ) : LONG
 *
 *      hWnd             handle to spectrum window
 *      wMsg             message number
 *      wParam       single word parameter
 *      lParam       double word parameter
 *
 * This function is responsible for processing all the messages
 * which relate to the spectrum control window.  Note how the
 * code is written to avoid potential problems when re-entrancy
 * happens - this involves the use of extra bytes associated
 * with the window data structure.
 *
 * The LONG value returned by this function is the conventional
 * result of the default window procedure or the internal
 * handling of a message.
 *
 */

LONG FAR PASCAL SpectrumWndFn( hWnd, wMsg, wParam, lParam )
  HWND          hWnd;
  WORD          wMsg;
  WORD          wParam;
  LONG          lParam;
{
  /* local variables */
  LONG    lResult;       /* temporary result variable */

  /* initialization */
  lResult = TRUE;

  /* process message */
  switch( wMsg )
     {
  case WM_GETDLGCODE : /* capture all key strokes */
     lParam = DLGC_WANTARROWS;
     break;
  case WM_CREATE : /* create pallette window */

     {
        /* temporary variables */
        HANDLE  hrgbList;          /* handle to rgb list */
        LONG FAR *   lprgbEntry;   /* pointer to rgb list */

        /* allocate space for rgb color list */
        hrgbList=GlobalAlloc( GMEM_MOVEABLE, sizeof(LONG)*16L );
        if ( hrgbList ) {

          /* define initial rgb color & value list - note that
           * eight default colors are selected with the values
           * matching each of the colors.
           */

          lprgbEntry = (LONG FAR *)GlobalLock( hrgbList );
          lprgbEntry[0]  = RGB( 0x00, 0x00, 0x00 );
          lprgbEntry[1]  = RGB( 0x00, 0x00, 0xFF );
          lprgbEntry[2]  = RGB( 0x00, 0xFF, 0x00 );
          lprgbEntry[3]  = RGB( 0xFF, 0x00, 0x00 );
          lprgbEntry[4]  = RGB( 0x00, 0xFF, 0xFF );
          lprgbEntry[5]  = RGB( 0xFF, 0xFF, 0x00 );
          lprgbEntry[6]  = RGB( 0xFF, 0x00, 0xFF );
          lprgbEntry[7]  = RGB( 0xFF, 0xFF, 0xFF );
          lprgbEntry[8]  = RGB( 0x00, 0x00, 0x00 );
          lprgbEntry[9]  = RGB( 0x00, 0x00, 0xFF );
          lprgbEntry[10] = RGB( 0x00, 0xFF, 0x00 );
          lprgbEntry[11] = RGB( 0xFF, 0x00, 0x00 );
          lprgbEntry[12] = RGB( 0x00, 0xFF, 0xFF );
          lprgbEntry[13] = RGB( 0xFF, 0xFF, 0x00 );
          lprgbEntry[14] = RGB( 0xFF, 0x00, 0xFF );
          lprgbEntry[15] = RGB( 0xFF, 0xFF, 0xFF );
          GlobalUnlock( hrgbList );

          /* define instance variables */
          SET_RANGE( 8 );
          SET_TABLE( hrgbList );
          SET_WIDTH( ((LPCREATESTRUCT)lParam)->cx / 8 );
          SET_HEIGHT( ((LPCREATESTRUCT)lParam)->cy );
          SET_CHOICE( 0 );
          SET_CAPTURE( FALSE );

        } else
          DestroyWindow( hWnd );

     }

     break;
  case WM_SIZE : /* window being resized */

     /* redefine width & height instance variables */
     SET_WIDTH( LOWORD(lParam) / 8 );
     SET_HEIGHT( HIWORD(lParam) );

     break;
  case WM_PAINT : /* paint control window */

     {
     PAINTSTRUCT     Ps;           /* paint structure */
     WORD            wEntry;       /* current color entry */
     HANDLE          hBrush;       /* handle to new brush */
     HANDLE          hOldBrush;    /* handle to old brush */
     LONG FAR * lprgbEntry;        /* pointer to rgb list */

        /* start paint operation */
        BeginPaint( hWnd, (LPPAINTSTRUCT)&Ps );

        /* iteratively paint each color patch */
        lprgbEntry = (LONG FAR *)GlobalLock( TABLE );
        for ( wEntry=0; wEntry<RANGE; wEntry++ ) {

          /* create solid brush for patch & select */
          hBrush = CreateSolidBrush( lprgbEntry[wEntry] );
          hOldBrush = SelectObject( Ps.hdc, hBrush );

          /* draw rectangle with brush fill */
          Rectangle(
                Ps.hdc,
                wEntry*WIDTH,
                0,
                (wEntry*WIDTH)+WIDTH,
                HEIGHT
          );

          /* unselect brush and delete */
          SelectObject( Ps.hdc, hOldBrush );
          DeleteObject( hBrush );

        }
        GlobalUnlock( TABLE );

        /* define current selection & end paint operation */
        DrawSelector( hWnd, Ps.hdc );
        EndPaint( hWnd, (LPPAINTSTRUCT)&Ps );

     }

     break;
  case WM_KEYDOWN : /* key being pressed */

     {
        /* local variables */
               HDC  hDC;                   /* display context handle */
               LONG FAR *     lprgbEntry;  /* pointer to rgb list */

               /* retrieve display context & unmark current selection*/
               hDC = GetDC( hWnd );
               DrawSelector( hWnd, hDC );

               /* process virtual key codes */
               switch( wParam )
                    {
               case VK_HOME :  /* home key */
                    SET_CHOICE( 0 );
                    break;
               case VK_LEFT :  /* left cursor key */
                    SET_CHOICE( (CHOICE > 0) ? CHOICE-1 : RANGE-1 );
                    break;
               case VK_RIGHT : /* right cursor key */
               case VK_SPACE : /* space bar - move right */
                    SET_CHOICE( (CHOICE < RANGE-1) ? CHOICE+1 : 0 );
                    break;
               case VK_END :   /* end key */
                    SET_CHOICE( RANGE-1 );
                    break;
               default :       /* some other key */
                    lResult = FALSE;
                    break;
               }

               /* mark new selection & release display context */
               DrawSelector( hWnd, hDC );
               ReleaseDC( hWnd, hDC );

               /* move caret to new position */
               SetCaretPos( CARET_XPOS, CARET_YPOS );

               /* notify parent of new selection */
               lprgbEntry = (LONG FAR *)GlobalLock( TABLE );
               SendMessage(PARENT,WM_COMMAND,ID,lprgbEntry
               [RANGE+CHOICE]);
               GlobalUnlock( TABLE );

          }

          break;
     case WM_SETFOCUS : /* get focus - display caret */

          /* create caret & display */
          CreateCaret( hWnd, NULL, CARET_WIDTH, CARET_HEIGHT );
          SetCaretPos( CARET_XPOS, CARET_YPOS );
          ShowCaret( hWnd );

          break;
     case WM_LBUTTONDOWN : /* left button depressed - fall through */

          {
               /* local variables */
               HDC  hDC;                     /* display context handle */
               LONG FAR *     lprgbEntry;    /* pointer to rgb list */

               /* retrieve display context */
               hDC = GetDC ( hWnd );

               /* unmark old selection & mark new one */
               DrawSelector( hWnd, hDC );
               SET_CHOICE( LOWORD(lParam)/WIDTH );
               DrawSelector( hWnd, hDC );

               /* release display context & move caret */
               ReleaseDC( hWnd, hDC );

               /* capture focus & move caret */
               if ( hWnd == SetFocus(hWnd) )
                    SetCaretPos( CARET_XPOS, CARET_YPOS );

               /* notify parent of new selection */
               lprgbEntry = (LONG FAR *)GlobalLock( TABLE );
               SendMessage(PARENT,WM_COMMAND,ID,lprgbEntry
               [RANGE+CHOICE]);
               GlobalUnlock( TABLE );

               /* activate capture */
               SetCapture( hWnd );
               SET_CAPTURE( TRUE );

          }

          break;
     case WM_MOUSEMOVE : /* mouse being moved */

          /* track mouse only if capture on */
          if ( CAPTURE ) {

               /* local variables */
               HDC            hDC     /* display context handle */
               WORD wNewChoice;       /* new mouse selection */
               LONG FAR * lprgbEntry; /* pointer to rgb list */

               /* calculate new selection */
               wNewChoice = ( *((int*)&lParam) <= 0 ) ?
                    0 :
                    ( LOWORD(lParam)/WIDTH >= RANGE ) ?
                         RANGE - 1 :
                         LOWORD(lParam) / WIDTH;

               /* update display if different */
               if ( wNewChoice != CHOICE ) {

                    /* retrieve display context */
                    hDC = GetDC ( hWnd );

                    /* unmark old selection & mark new one */
                    DrawSelector( hWnd, hDC );
                    SET_CHOICE( wNewChoice );
                    DrawSelector( hWnd, hDC );

                    /* release display context & move caret */
                    ReleaseDC( hWnd, hDC );
                    SetCaretPos( CARET_XPOS, CARET_YPOS );

                    /* notify parent of new selection */
                    lprgbEntry = (LONG FAR *)GlobalLock( TABLE );
                    SendMessage(PARENT,WM_COMMAND,ID,
                    lprgbEntry[RANGE+CHOICE]);
                    GlobalUnlock( TABLE );

               }

          }

          break;
     case WM_LBUTTONUP : /* left button released */

          /* release capture if active */
          if ( CAPTURE ) {
               SET_CAPTURE( FALSE );
               ReleaseCapture();
          }

          break;
     case WM_KILLFOCUS : /* kill focus - hide caret */
          DestroyCaret();
          break;
     case WM_DESTROY : /* window being destroyed */
          GlobalFree( TABLE );
          break;
     default : /* default window message processing */
          lResult = DefWindowProc( hWnd, wMsg, wParam, lParam );
          break;
     }

     /* return final result */
     return( lResult );

}
                                    ∙
                                    ∙
                                    ∙


Figure 5:  Code Fragments from PALETTE.C

/*
 * WINDOWS COLOR PALETTE UTILITY - SOURCE
 * LANGUAGE : Microsoft C 5.0
 * TOOLKIT  : Windows 2.03 SDK
 * MODEL    : Small
 * STATUS   : Operational
 * 03/20/88 - Kevin P. Welch - initial creation.
 */

#include <windows.h>
#include <math.h>
#include "spectrum.h"
#include "palette.h"
                                    ∙
                                    ∙
                                    ∙
/*
 * WinMain( hInst, hPrevInst, lpszCmdLine, wCmdShow ) : int
 *
 *   hInst                   handle of current instance
 *   hPrevInst               handle to previous instance (if any)
 *   lpszCmdLine             pointer to command line arguments
 *   wCmdShow                initial ShowWindow command
 *
 * This function is the system entry point for the application and is
 * responsible for defining the appropriate window classes and for
 * processing all the messages. Note how the dialog box manager is
 * responsible for the operation of the palette window.
 */

int PASCAL WinMain( hInst, hPrevInst, lpszCmdLine, wCmdShow )
     HANDLE         hInst;
     HANDLE         hPrevInst;
     LPSTR          lpszCmdLine;
     WORD           wCmdShow;
{
     /* local variables */
     FARPROC        lpProc;   /* temporary function */

     /* register window if first instance */
     if ( hPrevInst || RegisterSpectrum(hInst) ) {

/* display palette dialog box */
       lpProc = MakeProcInstance( (FARPROC)PaletteDlgFn, hInst );
       DialogBox( hInst, "Palette", NULL, lpProc );
       FreeProcInstance( lpProc );

     }

           /* end program */
           return( FALSE );

}

/*
 * PaletteDlgFn( hWnd, wMsg, wParam, lParam ) : BOOL
 *   hWnd                handle to palette window
 *   wMsg                message number
 *   wParam single word parameter
 *   lParam double word parameter
 *
 * This function is responsible for processing all the messages
 * which relate to the color palette dialog box. This mainly
 * involves the definition and retrieval of the various colors
 * selected by the user.
 *
 */

BOOL FAR PASCAL PaletteDlgFn( hDlg, wMsg, wParam, lParam )
     HWND   hDlg;
     WORD   wMsg;
     WORD   wParam;
     LONG   lParam;
{
     /* local variables */
     BOOL                bResult;  /* result of function */
     /* initialization */
     bResult = TRUE;

     /* process messages */
     switch( wMsg )
     {
case WM_INITDIALOG : /* initialize dialog box */

     /* select crt mixing model */
     wModel = ID_RGB;
     SelectMixingModel( hDlg, wModel );

     /* define icon for dialog box */
     SetClassWord(
        hDlg,
        GCW_HICON,
        LoadIcon( INSTANCE, (LPSTR)"PaletteIcon" )
     );

     break;
                                    ∙
                                    ∙
                                    ∙
case WM_COMMAND : /* window command */

     /* process sub-message */
     switch( wParam )
     {
     case ID_RGB : /* RGB mixing model */
     case ID_CMY : /* CMY mixing model */
     case ID_HSV : /* HSV mixing model */
          SelectMixingModel( hDlg, wParam );
          break;
     case ID_SPECTRUM1 : /* 1st spectrum */

     switch( wModel )
          {
     case ID_RGB :
           rgbColor.fRed = *((float*)&lParam);
           RGBtoCMY( &rgbColor, &cmyColor );
           RGBtoHSV( &rgbColor, &hsvColor );
           DlgPrintf( hDlg, ID_VALUE1, "%.3f", rgbColor.fRed );
                 break;
     case ID_CMY :
            cmyColor.fCyan = *((float*)&lParam);
            CMYtoRGB( &cmyColor, &rgbColor );
            RGBtoHSV( &rgbColor, &hsvColor );
                 DlgPrintf( hDlg, ID_VALUE1, "%.3f", cmyColor.fCyan );
                 break;
     case ID_HSV :
             hsvColor.fHue = *((float*)&lParam);
             HSVtoRGB( &hsvColor, &rgbColor );
             RGBtoCMY( &rgbColor, &cmyColor );
                 DlgPrintf( hDlg, ID_VALUE1, "%.0f", hsvColor.fHue );
                 break;
        }
     InvalidateRect( GetDlgItem(hDlg,ID_SAMPLE), NULL, NULL );

     break;
case ID_SPECTRUM2 : /* 2nd spectrum */

     switch( wModel )
        {
     case ID_RGB :
             rgbColor.fGreen = *((float*)&lParam);
             RGBtoCMY( &rgbColor, &cmyColor );
             RGBtoHSV( &rgbColor, &hsvColor );
             DlgPrintf( hDlg, ID_VALUE2, "%.3f", rgbColor.fGreen );
                break;
     case ID_CMY :
             cmyColor.fMagenta = *((float*)&lParam);
             CMYtoRGB( &cmyColor, &rgbColor );
             RGBtoHSV( &rgbColor, &hsvColor );
             DlgPrintf( hDlg, ID_VALUE2, "%.3f", cmyColor.fMagenta );
                break;
     case ID_HSV :
             hsvColor.fSaturation = *((float*)&lParam);
             HSVtoRGB( &hsvColor, &rgbColor );
             RGBtoCMY( &rgbColor, &cmyColor );
                DlgPrintf( hDlg, ID_VALUE2, "%.3f",
                hsvColor.fSaturation );
                break;
        }
     InvalidateRect( GetDlgItem(hDlg,ID_SAMPLE), NULL, NULL );

     break;
case ID_SPECTRUM3 : /* 3rd spectrum */

     switch( wModel )
        {
     case ID_RGB :
             rgbColor.fBlue = *((float*)&lParam);
             RGBtoCMY( &rgbColor, &cmyColor );
             RGBtoHSV( &rgbColor, &hsvColor );
                DlgPrintf( hDlg, ID_VALUE3, "%.3f", rgbColor.fBlue );
                break;
     case ID_CMY :
             cmyColor.fYellow = *((float*)&lParam);
             CMYtoRGB( &cmyColor, &rgbColor );
             RGBtoHSV( &rgbColor, &hsvColor );
                DlgPrintf( hDlg, ID_VALUE3, "%.3f", cmyColor.fYellow );
                break;
     case ID_HSV :
             hsvColor.fValue = *((float*)&lParam);
             HSVtoRGB( &hsvColor, &rgbColor );
             RGBtoCMY( &rgbColor, &cmyColor );
                DlgPrintf( hDlg, ID_VALUE3, "%.3f", hsvColor.fValue );
                break;
     }
     InvalidateRect( GetDlgItem(hDlg,ID_SAMPLE), NULL, NULL );

        break;
case ID_SAMPLE : /* sample color patch */

        /* update sample only if necessary */
        if ( HIWORD(lParam) == BN_PAINT ) {

        /* local variables */
                HDC     hDC;
                CRT     crtValue;
                HANDLE  hOldBrush;
                RECT    rectClient;

                /* display crt equivalent numerical values */
                    RGBtoCRT( &rgbColor, &crtValue );
                    DlgPrintf( hDlg, ID_RED, "RED  %u", crtValue.cRed );
                    DlgPrintf( hDlg, ID_GREEN, "GREEN %u", crtValue.cGreen);
                    DlgPrintf( hDlg, ID_BLUE, "BLUE  %u", crtValue.cBlue );

                    /* paint color sample */
                    hDC = GetDC( LOWORD(lParam) );
                    GetClientRect( LOWORD(lParam), &rectClient);

                     /* setup display context */
                     hOldBrush = SelectObject(hDC,
                     CreateSolidBrush( *((DWORD*)&crtValue) )
                     );

                     /* draw color sample */
                                 Rectangle(
                                 hDC,
                                    rectClient.left,
                                    rectClient.top,
                                    rectClient.right,
                                    rectClient.bottom
                                 );

            /* cleanup & release display context */
            DeleteObject( SelectObject(hDC,hOldBrush) );
            ReleaseDC( LOWORD(lParam), hDC );

        }

        break;
     default : /* something else */
        break;
     }

     break;
case WM_NCLBUTTONDBLCLK : /* non-client double click */
     EndDialog( hDlg, TRUE );
     break;
default :
     bResult = FALSE;
     break;
     }

/* return result */
return( bResult );

}

/*
* SelectMixingModel( hDlg, wNewModel )
*    hDlg                handle to palette dialog box
*    wNewModel           id of new mixing model
*
* This function is responsible for changing the displayed
* color mixing model to a new one.  In doing so the color
* spectra are changed and an attempt is made to find a
* match for the currently defined color under the new
* mixing model.
*/

void SelectMixingModel( hDlg, wNewModel )
     HANDLE hDlg;
     WORD                wNewModel;
{
     /* local variables */
     WORD                wRange;
     WORD                wEntry;

     /* define new range & check button */
     wRange = 16;
     wModel = wNewModel;
     CheckRadioButton( hDlg, ID_RGB, ID_HSV, wModel );

     /* perform initialization depending on model */
     switch( wNewModel )
        {
     case ID_RGB :

        SetDlgItemText( hDlg, ID_TITLE1, "Red.......:" );
        SetDlgItemText( hDlg, ID_TITLE2, "Green.....:" );
        SetDlgItemText( hDlg, ID_TITLE3, "Blue......:" );

        wEntry = MatchColor(rgbRed,rgbColor.fRed);
        SetSpectrumColors( SPECTRUM1, &wRange, rgbRed );
        SetSpectrum( SPECTRUM1, &wEntry  );

        wEntry = MatchColor(rgbGreen,rgbColor.fGreen);
        SetSpectrumColors( SPECTRUM2, &wRange, rgbGreen );
        SetSpectrum( SPECTRUM2, &wEntry );

        wEntry = MatchColor(rgbBlue,rgbColor.fBlue);
        SetSpectrumColors( SPECTRUM3, &wRange, rgbBlue );
        SetSpectrum( SPECTRUM3, &wEntry  );

        break;
     case ID_CMY :

        SetDlgItemText( hDlg, ID_TITLE1, "Cyan......:" );
        SetDlgItemText( hDlg, ID_TITLE2, "Magenta...:" );
        SetDlgItemText( hDlg, ID_TITLE3, "Yellow....:" );

        wEntry = MatchColor(cmyCyan,cmyColor.fCyan);
        SetSpectrumColors( SPECTRUM1, &wRange, cmyCyan );
        SetSpectrum( SPECTRUM1, &wEntry );

        wEntry = MatchColor(cmyMagenta,cmyColor.fMagenta);
        SetSpectrumColors( SPECTRUM2, &wRange, cmyMagenta );
        SetSpectrum( SPECTRUM2, &wEntry );

        wEntry = MatchColor(cmyYellow,cmyColor.fYellow);
        SetSpectrumColors( SPECTRUM3, &wRange, cmyYellow );
        SetSpectrum( SPECTRUM3, &wEntry );

        break;
     case ID_HSV :

        SetDlgItemText( hDlg, ID_TITLE1, "Hue.......:" );
        SetDlgItemText( hDlg, ID_TITLE2, "Saturation:" );
        SetDlgItemText( hDlg, ID_TITLE3, "Value.....:" );

        wEntry = MatchColor(hsvHue,hsvColor.fHue);
        SetSpectrumColors( SPECTRUM1, &wRange, hsvHue );
        SetSpectrum( SPECTRUM1, &wEntry );

        wEntry = MatchColor(hsvSaturation,hsvColor.fSaturation);
        SetSpectrumColors( SPECTRUM2, &wRange, hsvSaturation );
        SetSpectrum( SPECTRUM2, &wEntry );

        wEntry = MatchColor(hsvValue,hsvColor.fValue);
        SetSpectrumColors( SPECTRUM3, &wRange, hsvValue );
        SetSpectrum( SPECTRUM3, &wEntry );

        break;
     }

}

/*
* MatchColor( plTable, fValue ) : iEntry
*    plTable   table of color values
*    fValue    value to search for
* This utility function searches a spectrum table for
* a particular entry using the key value provided.  An
* index to the closest match is returned for subsequent
* use when switching color mixing modes.
*
* Note that an exact match may not be present (due to the
* discrete nature of the spectrum table).  As a result
* some variations in the resulting color may by visible.
*/

static int MatchColor( plTable, fValue )
     LONG *    plTable;
     float     fValue;
{
     int       iEntry;
     int       iMinimum;
     float     fMinimum;

     /* initialization */
     iMinimum = 0;
     fMinimum = ABS(*((float*)&plTable[16])-fValue);

     /* search for best fit */
     for ( iEntry=16; iEntry<32; iEntry++ ) {
          if (ABS(*((float*)&plTable[iEntry])-fValue) < fMinimum ) {
          iMinimum = iEntry - 16;
          fMinimum = ABS(*((float*)&plTable[iEntry])-fValue);
          }
     }

     /* return best entry */
     return( iMinimum );

}
                                    ∙
                                    ∙
                                    ∙

████████████████████████████████████████████████████████████████████████████

SQL Server Brings Distributed DBMS Technology to OS/2 Via LAN Manager

───────────────────────────────────────────────────────────────────────────
Also see the related article:
  SQL: A Short Primer
───────────────────────────────────────────────────────────────────────────

Marc Adler☼

The OS/2 systems open up many possibilities for building distributed
applications. Separate processes can perform different tasks, and, by
using the kind of interprocess communication (IPC) offered by OS/2, a
highly coordinated system can be constructed from different parts. A
sophisticated front end that can communicate with a back-end process to
form a single application, for example, can be developed by using the
OS/2 Presentation Manager. With the addition of the Microsoft(R) LAN
Manager, OS/2 can also be used to develop distributed applications. Front-
end and back-end processes will run on totally separate machines hooked
together on a network and will be able to easily pass information back and
forth.

This approach is frequently called client-server computing. The client-
server approach allows large numbers of PC-based user workstations (called
clients) to access one or many server machines across a local area
network. Client machines provide an intelligent and flexible user
interface. Servers bring minicomputer- and mainframe-style shared
services to the PC world and, because they are accessed through
intelligent clients, are not limited to the "dumb" terminal-based
applications characteristic of minis and mainframes. Among the first of
these distributed, client-server applications is SQL Server.


SQL Server

SQL Server is a high-performance, multiuser DBMS designed specifically
for MS(R) OS/2-based local area networks, especially those that utilize the
Microsoft OS/2 LAN Manager software. It is based on technology originally
developed by Sybase for minicomputers and optimized by Microsoft and
Sybase for OS/2 LANs. It incorporates such features as stored
procedures, triggers, and an advanced transaction-oriented SQL kernel.
These features are essential for guaranteeing data integrity in the
multiuser LAN environment. The advances, along with SQL Server's high-
volume performance, mean that SQL Server will enable LANs to become a
viable platform for business-critical applications, which until now were
limited to the mainframe and minicomputer environment.

SQL Server is a server-based implementation of the SQL language
(Structured Query Language, usually pronounced "sequel"), the language by
which applications communicate with relational databases. It is the
underlying "technology" that will work in conjunction with the OS/2 LAN
Manager and end-user software created by third-party developers to
transparently:

  ■  package SQL requests from any user workstation
  ■  send those requests to a server across a LAN
  ■  perform whatever requests were made via the SQL statements
  ■  package the results in a suitable manner
  ■  return the results from the server to the user workstation

SQL Server is an "open platform" that will give developers the ability to
create SQL Server-compatible front-end applications. Users can always
have direct access to the SQL language; however, most will access some
front-end program that hides the underlying SQL. This gives developers
the opportunity to create new and exciting applications and user
interfaces that offer the power of SQL without requiring users to know the
language.

SQL Server is the first SQL-based database management system designed from
the ground up that supports high-volume transaction processing, as well
as the less demanding decision support applications that other SQL
databases have targeted──it has the ability to support the entire
application spectrum, from simple to complex. SQL Server runs on any
OS/2-based server machine, and multiple instances of SQL Server can be on
any number of servers on a given network. Users can concurrently and
transparently access shared databases through SQL Server from any OS/2-
based, as well as any DOS-based, workstation on a network (see Figures 1
and 2).

SQL Server is a joint development effort of Microsoft and Sybase and
includes technological contributions from Ashton-Tate as well. Ashton-
Tate will make its dBASE(R) IV product compatible with SQL Server so that
existing dBASE III(R) and new dBASE IV applications can run unchanged with
SQL Server. SQL Server will be sold by Ashton-Tate through its retail
channels under the name Ashton-Tate/Microsoft SQL Server. Microsoft will
license SQL Server to hardware OEMs, who will resell the product through
their standard channels.


Industry Needs

The corporate world has come to depend on data for every decision that
needs to be made; data is now the most important resource of a company
after its people. As the base of corporate data grows, so does the
complexity of the data, and as corporate users become more hungry for
data, they become more demanding as well. More information from more
sources and substantial decreases in access time are demands high on the
list of every user. At the top of the list is the ability to conduct
reliable, high-speed transactions against a given database.

To handle this data demand, centralized corporate MIS departments turned
to the technology of relational database systems such as IBM's DB2 and
used mainframe and minicomputer systems as their hardware backbones.
With DB2 and other relational systems came the emergence of SQL, a high-
level database language that programmers could embed within their COBOL
and PL/1 programs to access data and perform other powerful database
functions.

Meanwhile at the departmental level, PCs, networked workstations, and
micro-to-mainframe links were becoming commonplace. The need for more
local distributed processing of data and for a powerful SQL engine that
could run as a back-end server on a network became obvious.


A Typical Scenario

Wall Street is an excellent example of an industry in great need of PC-
based distributed processing. Financial institutions are constantly
engaged in putting together more powerful workstations for their traders,
since any increase in a trader's productivity can translate into
substantial revenue increases for a firm.

Recent workstation and PC developments have provided considerably improved
windowing environments and analytical tools. However, these advances do
not address the Wall Street trader's most urgent need: quick access to
large amounts of real-time information, as well as the ability to process
transactions safely.

The financial industry has long needed a database system that would run on
networked PCs and workstations and yet be powerful enough to handle a
large number of transactions. Such a system would also include the ability
to have several users perform actions against this data.

The emergence of networked workstations, relational database systems,
SQL, and the high-speed transaction processing needs of the corporate
world indicate that SQL Server's design goals──to provide high transaction
volume, guaranteed data integrity, and support for SQL-were correct. SQL
Server, together with the emergence of OS/2 and the OS/2 LAN Manager,
finally brings distributed, SQL-based database power to the PC community
(see Figure 3).


The SQL Standard

SQL, the language through which actual database queries are made, grew out
of IBM's pioneering research into database models. The relational
database model was developed by E.F. Codd, a research scientist with
IBM, in the early 1970s. SQL was intended to be the standard query
language for the relational model. It was first used in System R, a
research database system created at IBM's San Jose research facility. SQL
has since been commercialized in IBM's two mainframe database
products, DB2 and SQL/DS.

IBM has announced that SQL will be part of its Systems Application
Architecture (SAA), one of the reasons why SQL has essentially become the
standard for relational-based query languages. As many large
corporations are IBM shops, vendors will have to support SQL to sell
to these corporations.

There is now an ANSI standard for SQL, which can be considered further
proof of the language's acceptance. To compete in the marketplace, any
dialect of SQL should comply with this standard, and SQL Server fully
supports the ANSI standard. SQL Server's dialect of SQL is called
Transact-SQL, and is, in fact, a superset of the ANSI SQL standard,
support-ing all of the SQL commands and offering important additions for
developers.


The Client-Server Model

When the phrase "front end" is used, you think of user interfaces, and
when "back end" is mentioned, you think of a black box that invisibly
stores and retrieves data. The front and back ends are considered two
distinct parts of the same program; the front end gathers data from the
user and prepares it for the back end to process. The back end manipulates
the data and sends it back to the front end when finished. The front end
then takes the processed data and presents it as a completed transaction
to the user. The client-server model represents the concept of a front
end──back end system (see Figure 4).

A client is a requester of services, and a server performs these services
at the behest of a client. A server sits and waits for a request to be
delivered to it by a client. When the request comes, the server performs
some actions and may deliver some data back to the client. In a
multiprocessing or networked environment, the client and the server are
completely separate processes and may even run on different machines.

Under the OS/2 LAN Manager, any networked machine can be either a
workstation or a server, depending on how the station is configured. Any
server station can also simultaneously function as a workstation, that
is, the server is not dedicated. SQL Server can reside on any machine
configured as a server. Client applications reside on any machines
configured as workstations.

When a client, or front-end, application requests a service from SQL
Server, the underlying SQL Server software transparently handles the
request (performing its role as the back end or server), provides the
service, and returns information or data if any was requested. The
client and server may both reside on the same machine, or more commonly,
the server will be on some machine unknown to the user. The user only
needs to understand how the client program works; SQL Server and the
network software take care of everything else.

Multiple front-end (client) processes can access a single SQL Server
simultaneously. In the SQL Server environment, there can be a many-to-many
relationship between clients and servers. On the client side, there might
be a spreadsheet, a forms manager, and a report writer, each requiring
access to records from a database. On the server side, you can have
multiple SQL Servers running over a network, with each server providing
access to a different set of databases (see Figure 5). SQL Server
guarantees referential integrity, ensuring that multiple processes
accessing the same data will not corrupt the integrity of the data.

A major concern of database users is the ability of the database to
withstand crashes. Ideally, one user mishandling the system should not
affect other users. The client-server model gives the system a certain
amount of fault tolerance. By having the front and back ends in separate
processes, the entire system does not have to die if one process crashes.
For instance, if a spreadsheet client goes down, the database server can
still service the other clients.

By having a functionally complete server like SQL Server, software
vendors can concentrate on developing powerful front-end tools for the
server. This is especially true with SQL Server; database engines are
difficult to build, consuming substantial manpower for any company.
With a powerful database engine already in place, corporations can
instead invest their R&D dollars in new and better client/front-end (or
user) interface technology such as expert systems and natural language
processing.


Features of SQL Server

SQL Server provides users with a complete SQL-database environment while
offering an unprecedented level of security and integrity checking. Rules,
defaults, triggers, stored procedures, and views, the necessary
components of a complete database system, all contribute to SQL Server's
efforts to make your data error-free.

Transact-SQL, SQL Server's superset of the ANSI standard, gives the user
increased expressiveness by offering flow-of-control constructs among its
numerous extensions to the standard SQL language. Let's look at the most
important features of SQL Server and outline the ways in which the user
can access these enhancements through SQL (see Figure 6).


Referential Integrity

One of the most pressing problems in database technology is the
enforcement of referential integrity, which concerns keeping data
consistent across the various tables in a database. Let's say that you
had two tables in your company's database: one table contains the names,
addresses, and identification numbers of all your customers, and the
other table contains the current orders for each customer. If you delete
a record from the customer table, you must first ensure that there are no
outstanding orders for that customer. If the customer had an order pending
and you mistakenly removed the customer's vital statistics from the
customer table, then the pending order would refer to a nonexistent
customer ID.

In database terminology, you must ensure that when you delete a primary
key from a table (such as a customer record), the corresponding foreign
keys in the dependent table (such as the customer id field in the orders
table) are also removed.

A trigger is a stored procedure that is fired when a specific
modification action is applied to a table in a database. The three
modification actions that you can trigger on are INSERT, DELETE, and
UPDATE. Using triggers, it is possible to "trap" a modification
before the database has committed to the new data. If an error condition
is discovered, a user-defined action can be performed to enforce
integrity, including a rollback of the database.


Stored Procedures

Before an SQL statement is executed, it must go through several steps:

  ■  the command must be parsed

  ■  all the names must be validated through the data dictionary

  ■  protections are checked through the data dictionary

  ■  an execution plan is formulated, which involves determining the
     proper index to use to access the data, among other optimization
     measures

  ■  the query is compiled into a form that is easier for SQL Server to
     execute

Stored procedures let you save compiled SQL statements for later recall,
thus avoiding the overhead described above when the procedures are
subsequently executed. Stored procedures also help in enforcing database
integrity, since you can incorporate your "business rules" into a stored
procedure, for example, if this is a customer's first order, then set the
CreditGiven field to FALSE. Candidates for stored procedures are
statements that you execute frequently and complicated statements that
you will use in the future but do not want to retype. In fact, any series
of SQL statements that can be executed through Transact-SQL can be
automated through a stored procedure. Stored procedures have optional
parameters that will be "bound" when you call the procedure.


Rules and Defaults

To further enforce data integrity, you can specify rules that apply to
columns. For instance, if you run a furniture store, you might want to
prevent someone from entering "rocketship" in the ItemOrdered field of
the orders table. In SQL Server, as data is added to a column by an INSERT
or UPDATE action, all rules associated with the column are checked. If the
data does not conform to the constraints that have been set by the rules,
then the transaction will be rolled back. You use the CREATE RULE
statement to establish a new rule.

To further enforce data integrity, SQL Server supports default values for
columns, which are entered into the column whenever you do not enter an
explicit data value for the column. A default value can be associated with
one or more columns, or with all the columns in the database that have a
user-defined type. A default value is created by the CREATE DEFAULT
statement.


Flow-of-Control

SQL Server provides conventional flow-of-control constructs within
Transact-SQL to facilitate writing more complex commands. By using these
constructs along with stored procedures and triggers, the SQL Server
programmer will rely less on coding external programs in languages such
as C. SQL Server supports IF/THEN/ELSE statements, WHILE loops with
CONTINUE and BREAK jumps, GOTOs and labels, and the declaration of local
variables. A simple PRINT statement enables the output of messages and
local variables.

You can assign values to local variables with a special form of the SELECT
statement. The SELECT statement should produce a single value, which will
be assigned to the local variable. If the SELECT statement produces
multiple values, then the last value returned will be assigned to the
local variable.


Transaction Processing

A transaction is defined as both a unit of work and a unit of recovery.
Substantial efforts have been made to ensure that SQL Server has a
complete safety mechanism. Users can define their own transactions and
specify the method of recovery that will take place should the
transaction fail. If planned correctly, recovery from crashes can take
minutes as opposed to hours for other DBMSs. System overhead is also
reduced because the system is accessed only at the completion of the
transaction instead of at the end of every statement.

To create a user-defined transaction, the statements comprising the
transaction are surrounded by the BEGIN TRANsaction and COMMIT TRANsaction
statements. The statements can contain any SQL statements, including
stored procedures that you define.

You can cancel the entire transaction or just a part of it. SQL Server
lets you put place markers, called "savepoints," anywhere inside a
transaction. To restore a database image to a previous state, you must
"rollback" the database with the ROLLBACK command.


Views

Suppose you have a database of employee information on your system. This
database might contain some sensitive data such as salary and social
security number that you wouldn't want all the users of the database to
see. It would be helpful if you could define a "virtual" table consisting
only of the nonsensitive fields of the database. SQL Server lets you do
this by using "views."

A view is a customized way to look at the records of a database. You are
able to specify a subset of the rows and columns of a table for the view,
choose items selected from multiple tables, and even group data in ways
that differ from the data's physical storage. A view is treated just like
a separate table as far as the user is concerned; the SELECT statement can
always reference a view. INSERT and UPDATE can only reference a view that
reflects a single table.

The result from any SELECT statement is employed to create a view. The
SELECT statement serves as the "extraction" part of the CREATE VIEW
statement in order to define the columns and records used in the view.

For a more detailed look at the common SQL commands, as well as their
structure and syntax, see the sidebar, "SQL: A Short Primer."


Developer Support

SQL Server gives software developers a database engine that they won't
soon outgrow. Since the engine is complete and implemented as a server,
developers can concentrate on creating advanced front-end tools to
access data more quickly and more easily. A software development kit,
function libraries, a rich and open set of APIs, and published specs will
be available to software developers.

Programmers will be able to write programs in C to access and manipulate
an SQL Server database by utilizing the APIs that will be supplied in DB-
Lib, part of the SDK (see Figure 7 for a simple example). Unlike
"embedded-SQL" interfaces, the developer builds SQL statements and sends
them directly to SQL Server──the developer does not have to precompile a
program with a special SQL compiler.


A Platform for the Future

SQL Server's triggers, rules, defaults, and rollbacks of transactions
relieve much of the worry of database integrity. Corporate users will
especially appreciate SQL Server's ability to handle the large volume of
their on-line transactions. The next several years should be interesting
for database users as more and more vendors adopt the SQL Server platform
and begin developing innovative and exciting distributed applications.
Together with the open architecture of OS/2, the graphical user interface
of Presentation Manager, and LAN Manager/SQL Server networking support,
developers now have the platform necessary to build new and exciting
networked applications.


Figure 1:  SQL Server will be compatible with any OS/2 or DOS-based
           workstation.

  ╔═════════════╤═════════════╗       ╔═════════════╤═════════════╗
  ║▒▒▒▒▒▒▒▒▒▒▒▒▒│▒Application▒║       ║▒▒▒▒▒▒▒▒▒▒▒▒▒│▒Application▒║
  ║▒Application▒├─────────────╢       ║▒Application▒├─────────────╢
  ║▒▒▒▒▒▒▒▒▒▒▒▒▒│░░Windows░░░░║       ║▒▒▒▒▒▒▒▒▒▒▒▒▒│Presentation░║
  ║▒▒▒▒▒▒▒▒▒▒▒▒▒│░░░░░░░░░░░░░║       ║▒▒▒▒▒▒▒▒▒▒▒▒▒│░░Manager░░░░║
  ╟─────────────┴─────────────╢       ╟─────────────┴─────────────╢
  ║            DOS            ║       ║           OS/2            ║
  ╚═════════════╦═════════════╝       ╚═════════════╦═════════════╝
                ║                                   ║
  ══════════════╩═══════════════╦═══════════════════╩═════════════════════
                                ║
             ╔══════════════════╩══════════════════╗
             ║                OS/2                 ║
             ╟─────────────────────────────────────╢
             ║             LAN Manager             ║
             ╟─────────────────────────────────────╢
             ║▒▒▒▒▒▒▒▒▒▒▒▒▒▒SQL Server▒▒▒▒▒▒▒▒▒▒▒▒▒║─────┐ ┌─────────────┐
             ╚═════════════════════════════════════╝     │ │   Shared    │
                          Data Integrity                 └─┤  Database   │
                         High Performance                  └─────────────┘


Figure 2:  Independent software vendors (ISVs) will be able to build
           front-end applications and user interfaces that will utilize SQL
           Server.

                                           DOS or OS/2 front ends:
                                             PC Databases
                                             Spreadsheets
   ╔════════════════════════════════╗        Languages
   ║░░░░░░Front-end Application░░░░░║        Vertical Markets
   ╟────────────────────────────────╢        Corporate/MIS Applications
   ║▒▒▒▒▒▒▒▒▒▒▒DB-Library▒▒▒▒▒▒▒▒▒▒▒║              ∙
   ╚═══════════════╦════════════════╝              ∙
                   ║                               ∙
                   ║
  ═════════════════╩═══════════════╦════════════════════════════════════
                                   ║
 True Database Server              ║
  Data Integrity    ╔══════════════╩═══════════════╗
  Security          ║             OS/2             ║     ╔══════════╗
  High Performance  ╟──────────────────────────────╢     ║          ║
                    ║          LAN Manager         ║     ║ Database ║
                    ╟──────────────────────────────╢  ┌──╢          ║
                    ║▒▒▒▒▒▒▒▒▒▒SQL Server▒▒▒▒▒▒▒▒▒▒╟──┘  ╚══════════╝
                    ╚══════════════════════════════╝


Figure 3:  New Applications specifically designed for network environments
            and mission-critical industry applications that formerly required
            mainframe and minicomputer systems can now be run on PCs
            utilizing the LAN Manager and SQL Server Software.

 ╔══════════════════════╤══════════════════════╤══════════════════════╗
 ║    Traditional PC    │ New Network-oriented │   Mission-critical   ║
 ║     Applications     │     Applications     │ Industry Applications║
 ╟──────────────────────┼──────────────────────┼──────────────────────╢
 ║ ▒▒▒▒PC Databases▒▒▒▒▒│▒▒Document Libraries▒▒│▒▒▒▒▒▒Accounting▒▒▒▒▒▒║
 ║ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒│▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒│▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒║
 ║ ▒▒▒▒Spreadsheets▒▒▒▒▒│▒▒▒▒▒▒▒CAD/CAM▒▒▒▒▒▒▒▒│▒▒▒▒▒Manufacturing▒▒▒▒║
 ║ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒│▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒│▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒║
 ║ ▒▒▒▒▒Languages▒▒▒▒▒▒▒│▒▒Desktop Publishing▒▒│▒▒▒▒▒▒▒▒Banking▒▒▒▒▒▒▒║
 ║ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒│▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒│▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒║
 ║ ▒▒▒Report Writers▒▒▒▒│▒▒▒▒Electronic Mail▒▒▒│Transaction Processing║
 ║ ▒▒▒▒▒▒▒▒▒∙▒▒▒▒▒▒▒▒▒▒▒│▒▒▒▒▒▒▒▒▒▒∙▒▒▒▒▒▒▒▒▒▒▒│▒▒▒▒▒▒▒▒▒▒∙▒▒▒▒▒▒▒▒▒▒▒║
 ║ ▒▒▒▒▒▒▒▒▒∙▒▒▒▒▒▒▒▒▒▒▒│▒▒▒▒▒▒▒▒▒▒∙▒▒▒▒▒▒▒▒▒▒▒│▒▒▒▒▒▒▒▒▒▒∙▒▒▒▒▒▒▒▒▒▒▒║
 ║ ▒▒▒▒▒▒▒▒▒∙▒▒▒▒▒▒▒▒▒▒▒│▒▒▒▒▒▒▒▒▒▒∙▒▒▒▒▒▒▒▒▒▒▒│▒▒▒▒▒▒▒▒▒▒∙▒▒▒▒▒▒▒▒▒▒▒║
 ╟──────────────────────┼──────────────────────┼──────────────────────╢
 ║░Enhanced by powerful░│░Designed especially░░│░░░Now possible on░░░░║
 ║░░░network database░░░│░░░░for networks░░░░░░│░░░░░PC networks░░░░░░║
 ║░░░░░░░systems░░░░░░░░│░░░░░░░░░░░░░░░░░░░░░░│░░░░░░░░░░░░░░░░░░░░░░║
 ╚══════════════════════╧══════════════════════╧══════════════════════╝


Figure 4:  Client-Server Model

                     ╔═════════════════════════════════════════╗
                     ║                   Users                 ║
                     ╟─────────────────────────────────────────╢
      Intelligent    ║   Front-end Applications──DOS and OS/2  ║
     User Interface  ╟───────────╥───╥───────────╥──╥──────────╢
                     ║▒▒▒▒▒▒▒▒▒▒▒║   ║▒▒▒▒▒▒▒▒▒▒▒║  ║Accounting║
                     ║PC Database║   ║Spreadsheet║  ║▒▒System▒▒║ •  •  •
                     ║▒▒▒▒▒▒▒▒▒▒▒║   ║▒▒▒▒▒▒▒▒▒▒▒║  ║▒▒▒▒▒▒▒▒▒▒║
                     ╚═════╦═════╝   ╚═════╦═════╝  ╚═════╦════╝
                           ║               ║              ║
 ═LOCAL AREA NETWORK═══╦═══╩════════════╦══╩════════════╦═╩═════════════════
                       ║                ║               ║
                       ║                ║               ║
                       ║                ║               ║
   Powerful       ╔════╩═════╗    ╔═════╩════╗     ╔════╩═════╗
   Mulituser      ║File/Print║    ║▒Database▒║     ║▒▒▒Mail▒▒▒║
   Services       ║▒▒Server▒▒║    ║▒▒Server▒▒║     ║▒▒Server▒▒║ •  •  •
                  ╚══════════╝    ╚══════════╝     ╚══════════╝
                              Back-end OB/2 Servers


Figure 5:  Typical Client-Server Applications

                                                          Database
                                                          Communications
                                                            gateways
                     ╔══════════════╗   ╔══════════════╗  Mail store/forward
                     ║▒▒▒▒▒▒▒▒▒▒▒▒▒▒║   ║▒▒▒▒▒▒▒▒▒▒▒▒▒▒║  Name/directory
                     ║▒▒▒Server 1▒▒▒║∙∙∙║▒▒▒Server N▒▒▒║    database
                     ║▒▒▒▒▒▒▒▒▒▒▒▒▒▒║   ║▒▒▒▒▒▒▒▒▒▒▒▒▒▒║  Document Library
                     ╚═══════╦══════╝   ╚═══════╦══════╝       ∙
                             ║                  ║              ∙
                             ║                  ║              ∙
════════╦═════════════════╦══╩══════════════════╩══════════════════════
        ║                 ║           Spreadsheet
        ║                 ║           Word Processor
╔═══════╩══════╗   ╔══════╩═══════╗   Mail front end
║▒▒▒▒▒▒▒▒▒▒▒▒▒▒║   ║▒▒▒▒▒▒▒▒▒▒▒▒▒▒║   Database query/
║▒▒▒Client 1▒▒▒║∙∙∙║▒▒▒Client N▒▒▒║     report package
║▒▒▒▒▒▒▒▒▒▒▒▒▒▒║   ║▒▒▒▒▒▒▒▒▒▒▒▒▒▒║   Desktop Publishing
╚══════════════╝   ╚══════════════╝         ∙
                                            ∙
                                            ∙


Figure 6:  SQL Server and Server Intelligence

   ╔════════════════╗   ╔═════════════════╗   ╔═════════════════╗
   ║▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒║   ║▒▒▒C Language ▒▒▒║   ║▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒║
   ║Data Application║   ║▒▒▒Application▒▒▒║   ║▒▒▒Spreadsheet▒▒▒║ ∙ ∙ ∙
   ║▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒║   ║▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒║   ║▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒║
   ╚═══════╦════════╝   ╚════════╦════════╝   ╚═════════╦═══════╝
           ║                     ║                      ║
           ║                     ║                      ║
   ════════╩═════════════════════╩══════════╦═══════════╩═══════════════
                                            ║
                                            ║
Server Intelligence                ╔════════╩════════╗
  Programmable Server              ║▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒║
  Rules protect data               ║▒▒▒▒SQL Server▒▒▒║
  Stored Procedures,               ║▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒║
  Triggers, Views                  ╚═════════════╤═══╝  ╔════════╗
                                                 │      ║ Rules  ║
                                                 │      ║┌──────┐║
                                                 └──────╢│ Data │║
                                                        ║└──────┘║
                                                        ╚════════╝


Figure 7:  Code Sample Using the DB-Lib APIs

/*
**      DBLIBDEM.C - DB-Lib/C EXAMPLE
**
**      This is an example program which illustrates the use of
**      sending two queries to the DataServer, in a batch, and
**      then binds each of the statements and prints the rows.
**
**
*/

#include <stdio.h>
#include <sybfront.h>
#include <sybdb.h>

extern char             *dbgetmsg();
extern LOGINREC         *dblogin();
extern DBPROCESS        *dbopen();

#define DATELEN 25
#define TYPELEN 2

main(argc, argv)
int     argc;
char    *argv[];
{
    DBPROCESS       *dbproc;        /* Our connection with the DataServer */
    LOGINREC        *login;         /* Our login information */

    /* These are the variables used to store the returning data. */

    DBDATETIME      crdate;
    char            datebuf[DATELEN];
    int             datelen;
    DBINT           id;
    DBCHAR          name[MAXNAME+1];        /* MAXNAME is defined in
                                             * sybdb.h, as the maximum
                                             * length for names of database
                                             * objects, such as tables,
                                             * columns, and procedures.
                                             */
    RETCODE         result_code;
    char            type[TYPELEN+1];

    /*
    ** Get a LOGINREC structure, and fill it with the necessary
    ** login information.
    */

    login = dblogin();

    DBSETLAPP(login, "example1");

    /*
    ** Get a DBPROCESS structure for our communication with the DataServer.
    ** Using NULL for the servername will use the server specified by
    ** the DSQUERY environment variable.
    */

    if ((dbproc = dbopen(login, (char *)NULL)) = = NULL)
    {
       dbperror(dberrno());
       printf("Couldn't connect with server.\n");
       exit(0);
    }

    printmsgs(dbproc);

    /*
    ** We are going to retrieve some information from a table
    ** named 'sysobjects', regarding names of system tables and
    ** stored procedures.
    ** We will submit two queries.  The first finds all rows
    ** which describe system tables.  The second finds all the rows
    ** which describe stored procedures.  The program will only look
    ** at the first 10 rows which describe stored procedures.
    */

    /* First put the commands into the command buffer. */

    dbcmd(dbproc, "select name, type, id, crdate from sysobjects");
    dbcmd(dbproc, " where type = 'S' ");
    dbcmd(dbproc, "select name, type, id, crdate from sysobjects");
    dbcmd(dbproc, " where type = 'P' ");

    /*
    ** The DataServer processes the command batch in the following
    ** order:
    ** 1) It will check for syntax errors (i.e. 'use database pubs' is
    **    syntactically incorrect, it should be 'use pubs').
    ** 2) The second check is a semantic check (i.e. 'select * from titles'
    **    will be incorrect because the spelling should be 'titles.')
    ** 3) The third check is the actual execution phase. This will check
    **    for things like permissions or memory problems.
    **
    ** Bn the execution phase, dbsqlexec and dbresults can return the
    ** value 'SUCCEED', which means there are more commands in the batch
    ** to process and that that command was successful. A value of 'FAIL'
    ** means that the query failed, but there may be more commands in the
    ** batch to process. A value of 'NO_MORE_RESULTS' means that there are
    ** no more commands in the batch to process.
    ** Therefore, the programmer must check for the return values
    ** after dbsqlexec and dbresults as illustrated below.
    */

    /* Now send the commands to the DataServer & start execution. */
    if (dbsqlexec(dbproc) == FAIL)
            printf("dbsqlexec FAILed.\n");

    printmsgs(dbproc);

    /* Initialize the last character of our "type" array -
     * dbbind() does not append a terminating NULL to indicate
     * the end of character arrays, when the type is CHARBIND.
     */

    type[TYPELEN] = '\0';

    /* Process each command until there are no more. */

    while ((result_code = dbresults(dbproc)) != NO_MORE_RESULTS)
    {
            printmsgs(dbproc);

            if (result_code = = SUCCEED)
            {
                    /* Bind program variables. */

                    dbbind(dbproc, 1, NTBSTRINGBIND, 0, name);
                    dbbind(dbproc, 2, CHARBIND, 0, type);
                    dbbind(dbproc, 3, INTBIND, 0, &id);
                    dbbind(dbproc, 4, DATETIMEBIND, 0, &crdate);

                    /* Print header according to which data is
                     * coming back.
                     */

                    printf("\n %s Objects: \n\n",
                    DBCURCMD(dbproc) = = 1 ? "System Table": "Procedure");

                    /* Now print the rows. */

                    while (dbnextrow(dbproc) != NO_MORE_ROWS)
                    {
                            /*
                            ** If this is the 2nd command and
                            ** 10th row, flush the rest of the
                            ** rows for that command.
                            */

                            if ((DBCURCMD(dbproc) = = 2)
                                && (DBCURROW(dbproc) > 10))
                                    continue;

                            /* Convert DataServer datetime datatype
                             * to ascii.
                             */

                            datelen = datetochar(&crdate, datebuf, DATELEN);
                            datebuf[datelen] = '\0';
                            printf
                             ("%s %s %ld %s\n", name, type, id, datebuf);
                    }

                    printmsgs(dbproc);
            }
            else
                    printf("dbresults FAILed.\n");

    }

/* Close our connection and exit the program. */

        dbexit();
        exit(0);
}

/*
** PRINTMSGS
**      Check the DBPROCESS for any error or informational messages
**      returned from the DataServer.
**
** Parameters:
**      dbproc - the DBPROCESS to check for messages.
**
** Returns:
**      none.
*/

printmsgs(dbproc)
DBPROCESS       *dbproc;
{
        char    *msg;

        while ((msg = dbgetmsg(dbproc)) != NULL)
        {
                printf("%s \n", msg);
        }
}


───────────────────────────────────────────────────────────────────────────
SQL: A Short Primer
───────────────────────────────────────────────────────────────────────────

SQL is a relatively straightforward and easy language to use──at least for
basic database access. Though it's not reasonable to expect to actually
learn SQL from the brief introduction provided here, you can get a good
feel for its simplicity as well as its power by going through a
demonstration of the major SQL commands and how they are used.

Suppose, for example, that you are the new proprietor of the Kaps & Mugs
Brewery and that you have decided to create a computerized employee
database. Further, suppose that as your first project you have decided to
put all of your employee information into a SQL Server system.

We will create two tables to hold the employee information. The first
table is called "employees" and consists of each employee's name, address,
social security number, and employee identification number. A second table
called "wages" contains the employee ID number, the employee's department
number, and the employee's current salary. In both cases, let's say that
you decide that the tables will be indexed by each employee's unique ID
number.

The CREATE verb is the basis for adding new objects to a SQL Server
database. CREATE is used to make new tables, rules, triggers, defaults,
stored procedures, and views.

To add a new table to your database, the CREATE TABLE statement is used.
The syntax is:

  CREATE TABLE [database.] table-name
  (column-name datatype [, column-name datatype]...)

The employees table is created by the statement:

  CREATE TABLE employees
  (name char(20), address char(50), socsec char(9), id integer)

The wages table is added by:

  CREATE TABLE wages
  (id integer, dept integer, salary money)

If you want to create an index on a field, you use the

  CREATE INDEX index-name ON column-name

statement. You can therefore create an index on the employees' unique id
field by using:

  CREATE INDEX id_index ON employees(id)

Indexing substantially speeds up certain operations. The field that is
indexed is the primary key and must contain unique values. In order to
further maximize the speed of operations, secondary indexes can be created
on other fields as needed.

SQL's basic data retrieval mechanism is the SELECT statement. The syntax
of this statement is:

  SELECT [DISTINCT] expr-list FROM table-list
   [WHERE conditional-expr]
   [GROUP BY columns]
   [HAVING expr]
   [ORDER BY columns]

The DISTINCT clause allows you to eliminate any duplicate records from the
selection process. The GROUP BY clause lets you group the selected records
together by columns; records whose column values are identical are printed
together in the same group. The HAVING clause can apply more filtering to
the data that is the subject of a GROUP BY.

Let's examine some SELECT statements to see how complex queries can be
built up. To select all the records in the employees table:

  SELECT * FROM employees

To select an employee's name and social security number:

  SELECT name, socsec FROM employees

To show the name and salary of each employee:

  SELECT name, salary FROM employees, wages
  WHERE employees.id = wages.id

The above example performs an implicit database operation called a "join,"
which merges two or more tables by joining the records at a common field
(in this case the ID field common to both tables). This is an illustration
of the most common type of join, an "equi-join."

You can print out the salaries and tax information for each employee and
group them together by department:

  SELECT name, salary, salary * .25 FROM employees, wages
  WHERE employees.id = wages.id
  GROUP BY wages.deptnum

Data is added to a table by using the INSERT statement. Its syntax is:

  INSERT table-name [(column-list)]
  VALUES (constant-expr [, constant-expr])

To add a record into the employees table:

  INSERT employees
  VALUES ('Marc Adler', 'New York City', '123-44-5667', 666)

Besides inserting data manually, you can use the result of a SELECT
statement to provide the necessary data:

  INSERT employees
  SELECT * FROM new_employees WHERE deptnum = 150

You can change the values in tables quite easily by using the UPDATE
statement, whose syntax is:

  UPDATE table-list
  SET column1=expr
    [column2=expr]...
  [FROM table-list ]
  [WHERE expression]

To give every employee in department 666 a 50 percent increase in salary:

  UPDATE wages
  SET salary = salary * 1.5
  WHERE deptnum = 666

Finally, you can remove certain rows from a table by using the DELETE
statement. The syntax for DELETE is:

  DELETE table-name
  [FROM table-list]
  [WHERE expression]

If you decided that everyone in department 666 earned too much, you
could:

  DELETE employees
  FROM employees, wages
  WHERE employees.id = wages.id AND wages.deptnum = 666

So far, the discussion has been limited to SQL statements that fall within
the ANSI standard. Now let's discuss some of the powerful extensions of
Transact:SQL that go beyond this standard.


A Sample Trigger

You make a trigger with the CREATE TRIGGER statement. The syntax of this
statement is:

  CREATE TRIGGER [owner.]trigger-name ON [owner.]table-name
  FOR {INSERT | UPDATE | DELETE} [, {INSERT | UPDATE | DELETE}]...
  AS <SQL statements>

There are several keywords that can be used inside the CREATE TRIGGER
statement, which greatly enhance its power. The keyword "deleted" refers
to a temporary table containing records that have just been DELETEd from
the trigger table. The keyword "inserted" refers to another temporary
table holding records that have just been INSERTed into the trigger table.
The keywords "IF UPDATE(column-name)" can test if data in the specified
column has been modified in any way.

For example, suppose you want to enforce uniqueness on the primary key
"id". When data is inserted into the "employees" table, you would like to
see if the id field has been duplicated, and if it has, print an error
message and rollback the entire transaction:

  CREATE TRIGGER id_trig ON employees.id FOR INSERT
  AS
  IF (SELECT count(*) FROM employees, inserted
  WHERE employees.id == inserted.id) !=
    (SELECT count(*) from inserted)
  BEGIN
  rollback transaction
  print "You inserted a duplicate employee-id value."
  print "The transaction has not been completed."
  END

You can remove a specific trigger from a table with the DROP TRIGGER
command. The syntax is:

  DROP TRIGGER [owner.]trigger-name


Creating Procedures

The CREATE PROCEDURE statement adds a new stored procedure to the
database. The syntax of the CREATE PROCEDURE statement is:

  CREATE PROCedure [owner.]proc-name [;number]
  [[(]@parameter-name datatype [= default]
  [, @parameter datatype [= default]]...[)]]
    [WITH RECOMPILE]
  AS SQL-statements

A procedure can contain one or more SQL commands, as in the following
example:

  CREATE PROC show_new_emp AS
  select id, name from employees where id >= 5000

Stored procedures are precompiled, and an "execution plan" is stored in a
system table. When a stored procedure is called, you are guaranteed that
the syntax is correct and little time will be spent on query
optimization.

To execute a stored procedure, you can either call the procedure by name
or use the EXECUTE command. The syntax for this command is

  [EXECute] proc-name[;number] [value [,value] ...]
  [WITH RECOMPILE]

or:

  [EXECute] proc-name[;number]
  [@parameter = value] [, @parameter = value] ...
    [WITH RECOMPILE]

As with traditional high-level programming languages, you are permitted to
pass arguments to a stored procedure. In the above example, if you wanted
to replace the number 5000 with an integer variable, you could code the
procedure as

  CREATE PROC show_new_emp @limit integer = 5000 AS
  select id, name from employees where
    id >= @limit

and execute the procedure with the call:

  show_new_emp @limit=2000

In the second example, the integer variable @limit is the lower bound for
the id comparison. The procedure call above will pass the value 2000 as
the argument to this procedure. If no argument is passed, then the
procedure will use the value 5000 as the default value for @limit.


Rules and Defaults

Rules are used to constrain the data values that a column(s) can contain.
The syntax of the CREATE RULE statement is:

  CREATE RULE [owner.]rule-name
  AS conditional-expression

As an example, suppose you want to create a rule that forces the
wages.dept field to accept only department numbers 100, 200, 300, 400,
500, or a department number that begins with 99 and contains two
additional digits. The rule that you would write in this case is:

  CREATE RULE deptrule
  AS @deptnum in ("100", "200", "300", "400", "500") or
    @deptnum like "99[0-9][0-9]"

As with stored procedures, a rule can accept an "argument"; in this case
the argument is @deptnum. By using run-time arguments, the same rule can
apply to more than one column simultaneously.

To bind a column name to a particular rule, you would use the system
procedure sp_bindrule. To bind the above rule to the wages.dept column,
invoke the command:

  sp_bindrule empidrule, "wages.dept"

You can unbind a rule by using the sp_unbindrule procedure, and you can
remove a rule from the database with the DROP RULE statement.

A DEFAULT is a value associated with a particular column or datatype. The
system will insert this default value into the target column if no value
is provided explicitly by the user.

The syntax for the DEFAULT statement is:

  CREATE DEFAULT [owner.]default-name AS constant-expression

Once a default is created, you associate it with a column by binding it to
that column with the "sp_bindefault" column. As an example, if you want
the default value of field "salary" in the table "wages" to be 15000.00,
you would enter the commands:

  CREATE DEFAULT sal_def AS 15000.00
  sp_bindefault sal_def, "wages.salary"

As with rules, you can unbind and drop defaults when you don't need them
anymore.


Flow-of-Control Example

Transact:SQL provides the common flow-of-control constructs that appear in
high-level languages like C. Included in the Transact:SQL extensions are
IF/ELSE condition testing, WHILE/BREAK/CONTINUE loop control, GOTOs and
labels, and local variable declaration. An example of these extensions is
shown below, in which we ask the system to alert us if the brewery grows
beyond 100 people:

  DECLARE @numemps int
  SELECT @numemps = count(*) from employees
  IF @numemps > 100
     PRINT "Our company has grown too large. Time to move!"

The WAITFOR statement is useful for scheduling commands to take place at a
certain time. The syntax is:

  WAITFOR {DELAY "time" | TIME "time" | ERROREXIT | PROCESSEXIT}

As an example, this code fragment waits until 2:20 in the afternoon,
inserts a new record in the employees table, and calls a procedure to
notify user Marc of the event:

  BEGIN
  WAITFOR TIME "17:00"
  IF select count(*)from employees >=1000
      EXEC sendmessage 'adler','Too many employees-commence firing!'
  END


Rollback

Rollbacks restore the database to a previous state. Until a transaction is
COMMITed, the database can be restored to the state it was in before the
transaction was started or to a savepoint that existed during the
transaction.

The syntax of the ROLLBACK statement is:

  ROLLBACK TRANsaction [transaction-name | savepoint-name]

If the transaction name or savepoint name is omitted, then the entire
transaction will be rolled back. A skeleton of a typical transaction might
look like this:

  BEGIN TRAN trans-name
    statement
    procedure
    SAVE TRAN savepoint-name
    statement
  IF <condition>
    ROLLBACK TRAN savepoint-name
    procedure
    statement
  IF <condition>
    ROLLBACK TRAN savepoint-name
    statement
    COMMIT TRAN

As an example of a transaction, suppose we want to double the salary of
all upper management (people in departments 99xx). We also want to
experiment to see what happens if we promote all VPs who earn more than
$50,000 to the highest salary grade. This experiment should not actually
change the main database, so we will save the state of the database before
we try the experiment and restore the state afterwards.

  BEGIN TRAN give_raise
   UPDATE wages
    SET salary = salary*2 WHERE dept >= 900 AND dept <= 9999
   SAVE TRAN chkpt1
   UPDATE wages
    SET dept = 9999 WHERE dept = 9998 AND salary >= 50000.00
   ROLLBACK TRAN chkpt1
   COMMIT TRAN

Note that the above is an unconditional rollback, which will always
restore the original state of the database.


Views

Views allow you to construct "virtual" tables whose fields and records
are selectively derived from the fields and records in the master
database. The syntax of the CREATE VIEW statement is:

  CREATE VIEW [[database.]owner]view-name
  [(column-name [, column-name]...)]
  AS select-statement

For example, let's create a view that permits a user to see only the names
and department numbers of all employees who earn under $50,000. To create
this view, you use the following statement:

  CREATE VIEW emps(name, empdept)
  AS SELECT name, dept FROM employees, wages
  WHERE wages.salary < 50000.00

Once a view is created, you can use the GRANT and REVOKE statements to
tell the system which users may manipulate this view (emps) and which
users may manipulate the view's underlying table (employees).

When a query is issued on a view, SQL Server combines the query with the
stored conditions in the view to form the complete query on the underlying
tables, a process called "view resolution." For example, if you have the
query

  SELECT * FROM emps WHERE empdept = 942

then, after the view is resolved, the query

  SELECT name, dept FROM employees, wages WHERE
  salary < 50000.00 AND dept = 942

would be issued to SQL Server.

The examples shown here are all fairly simple to read and understand, yet
they perform some very powerful database manipulations across entire
tables of information, either globally or selectively. In most cases, a
simple SQL command will be able to perform operations that in a language
such as dBASE III Plus(R) may require many lines of code, or even entire
programs, to execute.

As the proud new proprietor and database administrator of the Kaps & Mugs
Brewery, you have successfully set up your employee database by using the
standard SQL commands, and integrity-preserving constraints by using the
powerful Transact:SQL extensions provided by SQL Server. Have one on me!


Summary of SQL and Transact-SQL Commands and Syntax

╓┌─────────────────────┌─────────────────────────────────────────────────────╖
Command               Syntax

CREATE DEFAULT        [owner.]default-name AS constant-expression

CREATE INDEX          index-name ON column-name

CREATE PROCedure      [owner.]proc-name [;number]
                      [[(]@parameter-name datatype [= default]
                      [, @parameter datatype [= default]]...[)]]
                      [WITH RECOMPILE]
                      AS SQL-statements

CREATE RULE           [owner.]rule-name
Command               Syntax
CREATE RULE           [owner.]rule-name
                      AS conditional-expression

CREATE TABLE          [database.] table-name
                      (column-name datatype [, column-name datatype]...)

CREATE TRIGGER        [owner.]trigger-name ON [owner.]table-name
                      FOR {INSERT | UPDATE | DELETE} [, {INSERT |
                      UPDATE | DELETE}]...
                      AS <SQL statements>

CREATE VIEW           [[database.]owner]view-name
                      [(column-name [, column-name]...)]
                      AS select-statement

DELETE                table-name
                      [FROM table-list]
                      [WHERE expression]

DROP TRIGGER          [owner.]trigger-name
Command               Syntax
DROP TRIGGER          [owner.]trigger-name

[EXECute]             proc-name[;number] [value [,value] ...]
                      [WITH RECOMPILE]

[EXECute]             proc-name[;number]
                      [@parameter = value] [, @parameter = value] ...
                      [WITH RECOMPILE]

INSERT                table-name [(column-list)]

VALUES                (constant-expr [, constant-expr])

ROLLBACK              TRANsaction [transaction-name | savepoint-name]

SELECT                [DISTINCT] expr-list FROM table-list
                      [WHERE conditional-expr]
                      [GROUP BY columns]
                      [HAVING expr]
                      [ORDER BY columns]
Command               Syntax
                      [ORDER BY columns]

UPDATE                table-list
                      SET column1=expr
                      [column2=expr]...
                      [FROM table-list]
                      [WHERE expression]

WAITFOR               {DELAY "time" | TIME "time" | ERROREXIT | PROCESSEXIT}

GOTO and labels       Flow-of-control

IF/THEN/ELSE          Condition testing

Variables             Local declarations

WHILE/BREAK/CONTINUE  Loop control

████████████████████████████████████████████████████████████████████████████

Ask Dr. Bob!

DS and SS Segments

Dear. Dr. Bob,

I have been hearing a great deal about the DS and SS segment issue,
specifically that DS and SS cannot be the same for Windows and OS/2. Why is
"DS!=SS" so important and why is it necessary?──WFB

Dear WFB,

Anyone who has built a dynamic-link library (DLL) has run into the DS!=SS
issue. Most C code is compiled under the assumption that the Data Segment
(DS) and Stack Segment (SS) registers have the same value. They both point
to the application's main data segment, called DGROUP. In small and medium
models, this is almost a necessity because of the use of 16-bit NEAR
pointers. These pointers do not contain any segment information, only the
offset within a segment. The DS register provides the segment value needed
to address the actual data.

C allows you to take the address of any variable with the "&" operator,
but note that some variables (statics and externs) are located in your data
segment, while others (automatics, i.e., most local variables) are in the
stack segment. If DS and SS have the same value, there is no problem because
a 16-bit offset is enough to identify either kind of variable. However, if
DS and SS are different (DS!=SS), a 16bit NEAR pointer does not provide
enough information to address an automatic variable. For most MS-DOS(R),
Windows, and OS/2 applications, this is never a problem. It's customary for
DS and SS to contain the same value, which simply avoids the entire issue
Dynamic-link libraries such as the ones used in Windows and OS/2 are another
story.

Most DLLs have their own data segments. for example, USER.EXE, one of the
DLLs in Windows, stores the data structure that describes each window in its
own data segment. Every time a function in a DLL is called, the prolog code
saves the old DS register value and sets up DS to point to its own data
segment. The catch is that the SS register still points to the stack of the
calling program, which it must to be able to address the parameters that
were pushed onto the stack before the function call.

Dynamic-link libraries, therefore, generally run in the DS!=SS
environment, thus imposing several restrictions on the code. first, the C
code must be compiled with the -Aw switch. This tells the compiler not to
make its usual assumption that DS and SS are equal. Second, any time the
DLL uses the address of data on the stack that address must be a full 32-bit
FAR pointer, not a NEAR pointer. finally, some of the C run-time functions
are "off limits" because they assume that DS equals SS. The Microsoft(R)
Windows Software Development Kit contains a list of those functions that can
and cannot be used. for example, the scanf function (and its relatives)
can't be used when DS!=SS.

There are a few other situations where you will run into the DS!=SS
problem. One such problem occurs when using the Windows function
SetWindowsHook with one of the "system level" hooks, such as WH-
SYSMSGFILTER. Although the rest of your application runs in a normal DS==SS
environment, the hook function itself gets called from some other
application's stack, so that function (and anything it calls) is running
with DS!=SS.


Bells and Graphics

Dear Dr. Bob,

I have two questions for you that concern the Microsoft(R) C Compiler
shipped with the OS/2 Development Kit. First, can you tell me whether it
supports any of the graphics primitives now available with Version 5.0 of
the C Compiler? Second, how does one control the bell? I'd like to have a C
function that turns on the sound generator for a specified duration, pitch,
and volume.──RB

Dear RB,

The Microsoft C Compiler shipped with the OS/2 Development Kit does not yet
support the graphics primitives available in Version 5.0. These primitives
will be available for OS/2 in a future version of Microsoft C. With regard
to the function you are looking for to control the bell, please look at the
DosBeep() function, which accepts frequency and duration. The function is
one of the family API functions, or a dualmode OS/2 function──one that will
work under both MS-DOS 3.x and OS/2.


FAR and NEAR Memory Objects

Dear Dr. Bob,

A recent issue of MSJ described how Microsoft C 5.0 can address memory
objects, declared as FAR, but accessed as NEAR (with the ES register). My
question is, can I use small-model library functions (memset, memchr,
strcpy, etc.) with these objects, and should I preserve ES in my assembler
functions when I use this feature?──AF

Dear AF,

With the information provided by the same-seg pragma, Microsoft C 5.0 knows
that several FAR data objects share the same segment address. Consequently
this segment address is kept in ES and not reloaded for each new FAR
reference. Think of the compiler as enregistering a segment value when it
knows that several far data objects are in the same segment. Can you use the
small-model library functions with these objects? No, you cannot because
small-model library functions assume the segment address is kept in DS, not
in ES or passed on the stack. Should you preserve ES in your assembler
function when you use this feature? No, you don't need to. When the compiler
enregisters a value, it takes care to save and restore this value through
function calls.


Optimization Blues

Dear Dr. Bob,

I have a bad case of the Optimization Blues. Please consider the following
code fragment: int compare (n1, n2) char *n1, *n2;

  { char i; for (i = 0; i < 8; if (*n1++ != *n2++) return 0; return 1; }

Compiling this code under Microsoft C 4.0 resulted in 46 bytes using the

  /Ox/G1 switch.

With Microsoft C 5.0 it's 57 bytes using /Oails/Gs/G1, and 62 bytes
with/Ox/G1. That' s an increase in code size of 25 percent even if I specify
"favor code Size?" Overall, code size increases 10-15 percent on some
relatively small and not too complex programs even when turning off loop
optimization.

This might not seem such a big issue, but since I have to squeeze a large
application into 256Kb of EPROM space, an increase of 10 percent means I'd
have to use microsoft C 4.0.

Looking at the generated code, at least 9 bytes could be saved by a simple
reordering of statements, and another 9 if the compiler were smart enough to
recognize that it does not have to store the values of temporary variables
before returning from a function.

I can accept code size increases for option /Ox. If I say "favor speed,"
then it's OK with me if the compiler triples code to double speed. But code
increases in this magnitude when specifying "favor size" are not what I
would call "optimizing."

Help!──TW

Dear TW,

The problems with code size are interesting. Basically, -O1 is totally a
"speed favored over size" consideration. The reason for this is that the
nature of loops is such that they tend to be executed often. For example
speed is the overriding consideration. A command like

  cl -Ols test.c is

essentially an oxymoron. Unfortunately, this is probably not as well
documented as it ought to be.

Your example also illustrates another problem. One new optimization
(intended for speed) is to pull the exit code for a return up to the return
statement, instead of jumping to the exit code whenever there is a return
statement. It turns out that avoiding the jump increases speed, especially
on benchmarks dominated by function call/return overhead. Obviously, this
generates larger code. Unfortunately, it is not controlled by -Os, but by a
different switch, -Or, which may not be well documented either. -Os should
disable this optimization and will in future releases. In any event, if you
compile your code fragment using cl -Osr -G1 test.c then the difference in
code size with Microsoft C 4.0 is 3 bytes. Two of the 3 bytes are caused by
a change in the way code is generated for loops (even without -Ol) in order
to make the loop optimizer's job easier. The last byte is a nop used to pad
routines with an odd number of bytes so that CodeView(R) will be able to
disassemble the last instruction correctly.


════════════════════════════════════════════════════════════════════════════


Vol. 3 No. 5 Table of Contents

Bridge/386: A Tool for Integrating Applications Under Windows/386

Bridge/386 from Softbridge Microsystems is a powerful control system built
specifically for the Microsoft(R) Windows/386 environment. It allows the
end user to create dialog boxes and menus and to control and automate
access to both Windows and non-Windows programs.


A Guide to Understanding Even the Most Complex C Declarations

C declarations and declaration extensions are particularly perplexing to
both the novice and experienced programmer. A knowledge of these
extensions, as well as an understanding of the structure and syntax of
standard declarations, is essential for the professional programmer.


Techniques for Debugging Multithread OS/2 Programs with CodeView(R) 2.2

Debugging an OS/2 program with multiple threads of execution running
concurrently within the same program can seem daunting. CodeView 2.2, a
powerful tool for the OS/2 programmer, provides new and enhanced commands
for debugging multithread and protected-mode programs.


Exchanging Data Between Applications Using the Windows Clipboard

The Windows clipboard is the standard mechanism for data exchange between
applications. Diverse data formats and asynchronous events can make using
the clipboard tricky. This article explores some techniques, tricks, and
traps for applications that exploit the clipboard.


Using Microsoft(R) C Version 5.1 to Write Terminate-and-Stay-Resident
Programs

Terminate-and-stay-resident (TSR) programs have traditionally been written
in assembly language. Microsoft C Version 5.1 includes new features that
make it easier to create TSRs in C. We examine the new interrupt keyword
and some tricks for writing TSRs.


Customizing the Features of the M Editor Using Macros and C Extensions

The Microsoft(R) Editor (M) is a powerful tool that runs under both
MS-DOS(R) and OS/2. Traditional editing features are enhanced by the
ability to run compilers from within the editor and the ability to create
macros and extend the editor by adding user-written C functions.


Dynamically Creating Dialog Boxes Using New Windows 2.x Functions

Previously, dialog boxes had to be created statically in an resource file.
Windows 2.x includes two new functions that allow programmers to create
dialog boxes dynamically. These two new functions are detailed, and
suggestions are offered for building them into your code.


EDITOR'S NOTE

The introduction of any advance in computer hardware or operating system
technology is usually approached by software developers with healthy
skepticism and caution. The advent of OS/2 has been no different. In the
twelve months since the announcement of OS/2, thousands of developers
around the world have attended training seminars, and many are hard at
work on OS/2 applications.

But at the same time much of the computer community has, like Vladimir and
Estragon in Waiting for Godot, lamented the absence of a protagonist.
Where is the single "killer app" that will establish OS/2?

We suspect that theatrics aside, OS/2 is really an evolutionary change in
which no single application dominates the stage. The importance of OS/2 is
that it makes possible an entire class of cooperating applications. The
large memory, multitasking, and interprocess communications capabilities
of OS/2 allow applications to co-reside and communicate. For instance, a
word processor might use a chart or a table from a spreadsheet that in
turn receives its input from a database application.

Dynamic data exchange (DDE) and the clipboard are among the facilities
that will make such scenarios possible. The adoption of a standard user
interface in which "cut" and "paste" mean the same thing in different
applications and data is viewed in a consistent manner will make these
connections between applications comprehensible to users.

We have explored this theme in a number of articles. In our last issue we
looked at DARWIN from Merrill Lynch & Co., Inc., which links a SQL data
manager with Microsoft(R) Excel. In this issue, Bridge/386(c) from
Softbridge demonstrates how even "uncooperating" applications can be
combined to create a whole that is more powerful than the sum of its
parts. We also offer a practical guide to exploiting the clipboard. While
these explorations have focused on Microsoft Windows, they are even more
appropriate for OS/2.

In the future, the successful applications will not be one-man shows but
instead solid team efforts. The star performers will be applications that
work seamlessly with others, permitting the user to mix and match
capabilities. For the developer the challenge is to maximize functionality
and compatibility.──Ed.


Masthead

JONATHAN D. LAZARUS
Editor and Publisher

EDITORIAL

TONY RIZZO
Technical Editor

KAREN STRAUSS
Assistant Editor

JOANNE STEINHART
Production Editor

GERALD CARNEY
Staff Editor

KIM HOROWITZ
Editorial Assistant

ART

MICHAEL LONGACRE
Art Director

VALERIE MYERS
Associate Art Director

CIRCULATION

STEVEN PIPPIN
Circulation Director

WILLIAM B. GRANBERG
Circulation Manager

L. PERRIN TOMICH
Assistant to the Publisher

JAANA NIEUWBOER
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, the Microsoft logo, CodeView, MS, MS-DOS, and Multiplan are
registered trademarks of Microsoft Corporation. Bridge/286, Bridge/386,
Flint, Positioning Decision Support System, Softbridge Commercial Banker,
and Softbridge Financial Planner are trademarks of Softbridge Microsystems.
ClickArt is a registered trademark of T/Maker Company. dBASE III is a
registered trademark of Ashton-Tate Corporation. Inboard is a trademark
of Intel Corporation. Intel is a registered trademark of Intel Corporation.
Lotus and 1-2-3 are registered trademarks of Lotus Development Corporation.
Manuscript is a trademark of Lotus Development Corporation. Macintosh is a
registered trademark of Apple Computer, Inc. Illustrator is a trademark of
Adobe Systems, Inc. PostScript is a registered trademark of Adobe Systems,
Inc. ProKey is a trademark of RoseSoft, Inc. SideKick is a registered
trademark of Borland International, Inc. UNIX is a registered trademark of
American Telephone and Telegraph Company.

████████████████████████████████████████████████████████████████████████████

Bridge/386: A Tool for Integrating Applications Under Windows/386

Matt Trask☼

In the past year, a novel Windows application has been designed by
Softbridge Microsystems. Originally a by-product of in-house development,
Bridge/386(TM) has taken on a life of its own by allowing end users to
customize the Windows environment.

Until recently, Softbridge, based in Cambridge, Mass., has specialized
primarily in vertical market applications products that include software
for shipping management (the Positioning Decision Support System(TM)),
financial planning (the Softbridge Financial Planner(TM)), insurance (the
Flint(TM) expert system), and banking (the Softbridge Commercial Banker(TM)).

In the process of developing these applications, Softbridge programmers
often built their own proprietary system tools, many of which were
developed for the Microsoft(R) Windows programming environment. Softbridge
has been working with Windows since March 1984, making it one of the first
independent software vendors to do so. In the last 4 years Softbridge has
gained a great deal of experience with Windows, and it was this experience
that led the company to focus its attention beyond its traditional
vertical market products.

Softbridge concluded that although Windows offered an excellent array of
presentation objects, such as windows, scroll bars, and dialog boxes, it
offered the user little in terms of accessing these objects. A programmer
could certainly take advantage of these Windows facilities, but an end
user could only use these objects as an application allowed. For example,
there was no way for an end user to design a dialog box.

Under DOS, a user has the system's batch language available, with which
rudimentary menus can be created or repetitious tasks can be automated.
Under Windows, the user loses even this simple capability. Softbridge
wanted to give some of this functionality back to the user and began to
work on developing a set of tools with which to control the Windows
environmentnot at the programming level, but at the user level.

Softbridge soon elaborated on this idea and began to develop a system that
would permit users to integrate not only Windows applications but standard
applications such as Lotus(R) 123(R) and dBASE III(R) as well. Eventually
what emerged was a tool that would allow an end user to automate the
operation of both Windows and non-Windows programs under the Windows
environment, in a manner similar to that available under the DOS batch
language.

The result is a complete system that permits a company's information
center, for example, to control, manage, and integrate applications for
its users. Perhaps more importantly, the software enables the integration
of different software products under the common user interface of Windows.
It gives the PC manager, and the user as well, the ability to create
customized software to control applications.

When Microsoft introduced the most recent versions of Windows, Windows/286
and Windows/386, Softbridge was ready to announce Bridge/286(TM) and
Bridge/386. Furthermore, in the fourth quarter of 1988, Softbridge will
introduce Bridge/OS, a version of Bridge for OS/2 and the Presentation
Manager. This will allow workstations being developed using Bridge and
Windows/386 or Windows/286 to migrate easily to OS/2.

Unlike Softbridge's vertical applications, the Bridge/286 and /386
products are systems software that let a user extend and build Windows
applications. According to Fred Ciaramaglia, vice president of technology,
"Softbridge is now interested in earning a reputation as a provider of
systems software, not just applications." Thus, the Bridge product, which
began as an internal applications development tool, takes on new life as a
retail systems product.

One particularly exciting aspect of Bridge is that it can operate across
any NetBIOS-compatible LAN, which enables it to transparently integrate
applications across a network, while the user has what seems like a local
view. PC managers can then spare the user much of the inherent complexity
of networks. The real power of Bridge/386, however, lies in its Windows
interface, its ability to give some of the Windows tools directly to the
user, and, of course, its ability to multitask applications on 80386-based
machines.


Bridge/386

Bridge/386 is best described as a "batch language" built specifically for
Windows. It allows the user to automate repetitive Windows procedures and
to use different application programs without exiting Windows. I used
Bridge/386 on an Intel(R) InBoard(TM) 386 to build a Windows application
that I call the integrated programmer's workstation (PWS), which I will
examine later.

Bridge/386 starts with capabilities very similar to those available with
the DOS batch language. It adds the following to Windows:

  ■  user-defined menus

  ■  user-defined dialog boxes

  ■  enhanced conditional processing

  ■  subroutine calls

  ■  array variables

  ■  messaging between applications

  ■  network capabilities

  ■  "on" conditions (branching and special actions that occur due to
     some future condition)

  ■  parameters that can be passed to called files

  ■  support for named arrays (enhancing the DOS %1-%9 variable
     capability)

  ■  an ERROR variable serving as a more comprehensive version of the DOS
     ERRORLEVEL variable

  ■  the ability to place more than one command on a line (separated by
     semicolons)

As with the DOS batch facility, with Bridge/386 you first create a plain
ASCII text file that contains the Bridge/386 commands. Instead of the BAT
extension, however, you create a file with a BR extension, which serves to
identify the file as a Bridge/386 batch file. From Windows/386 you then
run BRIDGE.EXE, which is the control program for Bridge/386. BRIDGE.EXE is
an actual Windows program (see Figure 1☼) that looks like any other Windows
program. It has its own window and serves as the control center for the
application you are developing.

The Bridge/386 window can be customized so that it appears at any place on
the screen a user chooses. It can be any size or shape, and the Bridge
window caption bar can say anything the user wants it to say (see Figure
2☼). This customizing facility allows the user to camouflage Bridge/386
while it operates and makes it appear that the user is executing his or
her own Windows application. In fact, by including a standard WIN.INI line
such as BR=BRIDGE.EXE ^.BR in the WIN.INI file, a user can call any
application from the MSDOS Executive that has an extension of BR, and it
will appear to be a Windows program.

Only one Bridge/386 batch file can be executed at a time, but if you give
commands for more, Bridge will queue their execution until previous batch
files are completed.

When Bridge is first started, you will see the standard system menu in the
top left corner of the window (see Figure 3☼). This menu allows direct
entry of Bridge commands or the execution of batch files. Two of the more
interesting options that appear on this menu are Browser and Preferences.
Browser allows you to examine and control the current Bridge process, and
Preferences is used to select such  options as colors, scroll bars, Bridge
font, and whether executing batch programs will have trace output sent to
the Bridge window. Browser will be explained in more detail when I discuss
how it was used to test and debug PWS.


Components of Bridge

Bridge has two major parts: the Bridge engine and the various APIs that
permit access to this engine. The kernel is the main part of this engine.
It can open, activate, and close applications, as well as capture and play
back keystrokes between applications. The kernel also manages menus and
dialogs that can be used to create user interfaces for Bridge programs.
On-key and on-message conditions can be registered with the kernel so that
action can be taken when a hot key is pressed or a message is received
from another program. The Bridge window is directly controlled by the
kernel and, as mentioned earlier, can be recaptioned under program control
and used to display menus for user interaction.

The Bridge message manager is the second component of the engine. It
handles the opening, monitoring, and closing of mailboxes. These mailboxes
are used to send, receive, and read messages that are sent between
application programs in a system or across a network to applications
running on another system.

Finally, the DOS supervisor is the part of the Bridge kernel that controls
the execution of nonWindows applications. Its capabilities include reading
from the DOS screen, capturing and playing back keystrokes, and managing
data exchange via the Windows clipboard for nonWindows programs.

This supervisor permits the use of dialog boxes while running ordinary DOS
applications in fullscreen mode. With Windows/286 and Bridge/286 this
control is limited to sending a single line of keyboard input and the
ability to call the Bridge engine from DOS batch files using BR.COM.
Figure 4 summarizes the various parts of Bridge and what they do, and
Figure 5☼ shows the interactions between these parts.

There are APIs under development for Microsoft Excel, dBASE III, and the C
programming language. These APIs are not fully supported in the first
release of the product. The beta versions that I saw provide functionality
that is very similar to the Batch API, allowing Microsoft Excel macros to
call Bridge functions such as MSG SEND and MENU LOAD.


Programming Bridge

Bridge Batch, like any other programming language, provides various
control structures such as FOR, NEXT, IF, and GOSUB, as well as the
infamous GOTO. Figure 6 shows a partial list of the Bridge commands.

Variables do not have to be defined before they are used, and Bridge/386
data types include strings, integers, arrays, and floating point. Among
the objects that can be manipulated by Bridge are menus, windows, dialog
boxes, and mailboxes for interprocess communication. All the normal
arithmetic functions are available, and comparisons can be done with data
objects of the same type. Variables can be passed in function calls by
using a syntax that is similar to the %1 naming convention of the DOS
batch language.

Windows can be positioned and sized by using screen_units, a measure that
is based on a percentage of screen size. Individual applications can be
minimized, restored, or maximized under control of a batch program.

Many functions are provided for operating on the various data objects (see
Figure 7☼ for a partial list). One example is FILE_PARSE( ), which is used
to extract the drive, path, name, and extension parts of a filename. The
array and string operators are reminiscent of PostScript(R) programming
because the data types are similar.

Much of Bridge's power lies in the commands for manipulating menus and
dialog boxes. These are used to build sophisticated user interfaces and to
handle error conditions that arise while batch programs are running. See
Figure 8 for a partial list of dialog commands.

Developing Bridge

Several conditions attracted Softbridge to develop Bridge for the Windows
environment. The main attraction, according to Ciaramaglia, was "the
world-class applications such as Microsoft Excel that are being created
for the Windows presentation manager." This, combined with a long-standing
relationship between Softbridge and Microsoft that goes back to
Multiplan(R) in the p-code days, was enough encouragement.

Bridge was developed in C and assembly with DOS as the development
environment. Ciaramaglia says that the most important part of the
development effort was the relationship between Softbridge and Microsoft.
"Lots of DIAL, access to early beta releases, and an occasional phone call
to Phil Barrett [the author of Windows/386] were what it took to create
Bridge/386," he says.

One of the problems Softbridge encountered was the strange way that
Windows handles time-slicing when multitasking. The foreground task gets
about 60 percent of all available CPU cycles, and the remaining cycles are
split between all background tasks. This made it difficult to provide
predictable performance when Bridge was controlling other applications. A
number of enhancements were added to Windows at Softbridge's request; in
particular, a few of the "debug hooks" that were scheduled for removal
after beta testing were left in because they were so useful to Bridge/386.


Developing PWS

Figure 9 is the complete source code to a simple integrated programmer's
workstation. The ASCII file PWSHELP.TXT, also shown here, is displayed in
NOTEPAD when Help is selected from the PWS menu.

The time to develop this application was about one day, including the time
it took to learn Bridge and test and debug PWS. I found the interactive
windowed environment to be extremely useful when debugging PWS. The
individual windows that were started by PWS could be sized or minimized so
that the program execution trace could be watched in the main Bridge
Window. As mentioned earlier, this trace option can be enabled by using
Bridge's Browser to select program trace, message trace, or both. Browser
can be used at any time to examine Bridge variables and mailboxes.

Among the parts of PWS worth noting are the three lines that set up hot
keys. These commands define Ctrl-E, Ctrl-D, and Ctrl-C as hot keys that will
activate the editor, debugger, and communications program, respectively.

The majority of the PWS program is contained in the approximately 30 lines
following MENU DEFINE PWS. This menu definition creates the various
pull-down menus and other menu choices that drive the programmer's
workstation. After starting PWS, the programmer would usually select Set
Current Drive and Set Current Directory to set defaults for the Edit,
Make, and Debug commands. Edit and Debug show the use of dialog boxes to
select a file from the current directory.

PWS is a fairly simple example of the Bridge/386 batch facility. It can
further be enhanced by using BRDOS (the DOS supervisor) instead of PIF
files to run the Edit, Make, and Debug programs, offering greater control
and interprocess communication possibilities. Also, the application code
could be written to test for the existence of necessary files such as
EDITOR.PIF and automatically invoke PIFEDIT if they are not defined. Such
automatic installation can be provided in any Bridgebased application.

Conclusion

Bridge/286 and Bridge/386 are probably best suited for use by systems
integrators who are designing complete applications that include
Windows/286 or Windows/386. Large corporations could use Bridge/286 and
Bridge/386 to configure individual PCs with simplified user interfaces
that are more meaningful to workers. Software vendors could use Bridge for
automating installation and online tutorials for their own products.

PWS is intended as a quick and easy example of Bridge/386 programming, not
as a complete demonstration of Bridge's capabilities. I can imagine a
workstation that changes personality under Bridge/386 mediation as I find
myself wearing the various hats of programmer, writer, and accountant.

One fascinating possibility would be the design of an automated stock
trading system that reads quotes from an on-line ticker machine, makes
buy/sell decisions after massaging the data in a Microsoft Excel
worksheet, and uses BRDOS to control a communications program that calls
in the orders to a stock broker's computer.

Although I haven't yet figured out how to make Reversi compete with
another copy of itself, there does seem to be adequate power in Bridge to
do most anything else I could want in Windows. With Bridge, Softbridge
Microsystems has something to add to Windows in a way that is going to
interest many personal computer users. Bridge/386 and Bridge/286 may very
well change the way people use Microsoft Windows/386 and Windows/286
presentation managers.


Figure 4:  The Various Parts of Bridge

  BRIDGE.EXE     The Bridge kernel and Bridge batch interface.

  BRDYN.EXE      APIs for other Bridge programming interfaces.

  BRMSG.EXE      The Bridge Message Manager.

  BRDOS.EXE      The Bridge DOS Supervisor and DOS batch interface.

  BR.COM         Used to issue commands to the Bridge Kernel while running
                 DOS.Commands can also be given by programs using an
                 Interrupt 16H interface.


Figure 5:  The interrelationships and possible connections for Bridge/386.

                      ╔═══════════════╗
 ╔════════════════╗   ║    DOS 3.x    ║
 ║  Windows 2.x   ║   ║Virtual Machine║   ╔═══════════════════╗   ╔══════════
 ║ ┌────────────┐ ║   ║ ┌───────────┐ ║   ║  Dos 3.x Machine  ║   ║   Other
 ║ │   Windows  │ ║   ║ │   DOS     │ ║   ╟───────────────────╢   ║Window/386
 ║ │ Application│ ║   ║ │Application│ ║   ║ DOS Application   ║•••║or DOS 3.x
┌─►│ (Microsoft │ ║   ║ ├───────────┤ ║   ╟───────────────────╢   ║ Machines
│║ │   Excel)   │ ║   ║ │ Bridge/386│◄──┐ ║   Bridge/386 or   ║   ╚══════════
│║ └────────────┘ ║   ║ │ DOS Module│ ║ │ ║   Bridge/286 DOS  ║
│║ ┌────────────┐ ║   ╚═══════════════╝ │ ║    Module and     ║
│║ │   Windows  │◄─┐  ╔═══════════════╗ │ ║ Message Controller║
│║ │ Application│ ║│  ║    DOS 3.x    ║ │ ╚═══════════════════╝
│║ └────────────┘ ║│  ║Virtual Machine║ │           
│║ ┌────────────┐ ║│  ║ ┌───────────┐ ║ │           │
│║ │ Bridge/386 │ ║│  ║ │   DOS     │ ║ │           │
└─►│   Master   │◄─┘  ║ │Application│ ║ │           │
 ║ │ Controller │ ║   ║ ├───────────┤ ║ │           │
 ║ └────────────┘ ║   ║ │ Bridge/386│ ║ │           │
 ║               ║   ║ │ DOS Module│ ║ │           │
 ╚═══════│════════╝   ╚═══════════════╝ │           │
         ▼                    ↕         ▼           │
░░░░░ Bridge/386 Message Controller ░░░░░░░         │
                    ↕                               ▼
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ LOCAL AREA NETWORK ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒


Figure 6:  Sample Bridge Batch Commands

Programming  Constructs

  call
  for
  gosub
  goto
  halt
  if
  return

Batch-Style Programming

  set
  shift

Debug Support

  trace
  warning

File/DOS Commands

  rename
  chdir
  delete
  rmdir

Mailbox Management

  msg create
  msg delete
  msg netclose
  msg netinit
  msg query
  msg read
  msg send

Bridge Window Control

  rows
  columns
  background
  font
  vscroll
  hscroll

Task Control

  foreground
  background
  fullscreen
  maximize
  minimize
  window


Figure 7:  Sample Bridge Functions

  abs
  app.exist
  array.len
  exact
  file.size
  find
  left
  upper


Figure 8:  Sample Bridge Dialog Box Commands

DIALOG button_option [default_option] prompt [caption] where button_option
is:

     OK             OKCANCEL       RETRYCANCEL
     YESNO          YESNOCANCEL    ABORTRETRYIGNORE

   and default_option is:

     DEFBUTTON1     button_number

DIALOG FILE file_spec prompt [caption][RESULT variable_name]

DIALOG INPUT prompt [NUMBER][INTEGER][caption][RESULT variable_name]

DIALOG LOAD
   BEGIN
      [AT x y]
      [SIZE width height]
      control ...
   END

   where control is:

      CAPTION text
      CHECKBOX [AT x y] text [DEFAULT value][RESULT
         variable_name]
      EDIT [WIDTH characters][AT x y][DEFAULT
         text][NUMBER]
         [INTEGER][RESULT variable_name]
      ICON {HAND|EXCLAMATION|QUESTION|ASTERISK}[AT x y]
      PUSHBUTTON [AT x y][DEFAULT value][CANCEL] text
         [WIDTHcharacters]TEXT [AT x y][text][WIDTH characters]


Figure 9:  PWS.BR, An Integrated Programmer's Workstation

PWS.BR  Source Code Listing

REM  PWS: A Programmer's Workstation Using Win/386 and Bridge/386
REM  By: Matt Trask  On: 23 APR 88

REM  This program assumes that you have defined your
REM  editor, make program, debugger, and comm program as
REM  EDITOR.PIF, MAKE.PIF, DEBUGGER.PIF, and COMMS.PIF.


REM *** make the bridge window big enough for the menu bar ***

select bridge; size 100 12; cls

REM *** load main menu for programmer's workstation ***

caption "Programmer's Workstation" menu load pws

REM *** set up hot keys ***

select windows; on key ^e gosub callprog editor editor.pif
select windows; on key ^d gosub callprog debugger debugger.pif
select windows; on key ^c gosub callprog comms comms.pif return

REM *** main menu for programmer's workstation ***

menu define pws
begin
   popup En&vironment
   begin
      menuitem "Set Current D&rive"       gosub set_drive
      menuitem "Set Current D&irectory"   gosub set_cwd
      menuitem "Online Communication"     gosub comm
      menuitem "DOS &Command Line"        gosub doscmd
   end

   popup "&Programming Tools"
   begin
      menuitem "     &Edit      "      gosub edit
      menuitem "     &Make      "      gosub make
      menuitem "     &Debug     "      gosub debug
      menuitem "     &SCCS      "      gosub sccs
   end

   popup "&Windows Tools"
   begin
      menuitem &Calculator     gosub callprog calc calc
      menuitem Calen&dar       gosub callprog calendar
                               calendar
      menuitem Cloc&k          gosub callprog clock clock
      menuitem &Notepad        gosub callprog notepad
                               notepad
   end

   popup &Email
   begin
      menuitem "&Check Email"   gosub pwsemail Check
      menuitem "&Send Email"    gosub pwsemail Send
   end
   menuitem &Help               gosub pwshelp
   menuitem "E&xit PWS"         goto pwsexit
end

REM *** Set the current working directory ***

:set_cwd
dialog input "Directory name?" result cwd
if %error% == 2 return          ; REM cancel button
if %cwd% == "" return           ; REM none specified
cd %cwd%
return

REM *** Set the current drive ***

:set_drive
dialog input "New drive name?" result drive
if %error% == 2 return          ; REM cancel button
if %drive% == "" return         ; REM none specified
%drive%:
return

REM ***  Run COMMAND.COM ***

:doscmd
exec /N:doscmd commandw.pif
return

REM *** Run A Communication Session  ***

:comm
exec /N:comms comms.pif
return

REM *** Edit a file ***

:edit
dialog file *.asm "Edit which file?" result edfile
if %error% == 2 return          ; REM cancel button
exec editor.pif %edfile%
return

REM *** Build a program ***

:make
exec make.pif
return

REM *** Debug a program ***

:debug
dialog file *.exe "Debug which program?" result dbgfile
if %error% == 2 return          ; REM cancel button
exec debugger.pif %dbgfile%
return

REM *** Run Source Code Control System ***

:sccs
dialog ok "Not implemented yet" "SCCS"
return

REM *** Subroutine to invoke a Windows App ***

:callprog
REM if application already exists, just activate it and
return if app.exist(%1) select %1; activate; return
REM otherwise start it up
exec /N:%1 %2
return

REM *** Stub for Email System ***

:pwsemail
dialog ok "Email system not available in PWS demo" "%1 Email"
return

REM *** Display help text using Notepad ***

:pwshelp
menu load   ; REM load Exit Help menu into Bridge Window
begin
   menuitem "E&xit Help" menu load pws; warning off;
   select help; close
end
exec /N:help notepad.exe pwshelp; move 0 12 100 88
return


REM *** Clean up apps and exit ***

:pwsexit

if app.exist(comms) then
   select comms; activate
   dialog ok "Exit halted, you must close this app
   first" Comms
   halt
end if

if app.exist(editor) then
   select editor; activate
   dialog ok "Exit halted, you must close this app
   first" Comms
   halt
end if

if app.exist(debugger) then
   select debugger; activate
   dialog ok "Exit halted, you must close this app
   first" Comms
   halt
end if

REM *** clear hot keys ***

warning off; select windows; on key ^c
warning off; select windows; on key ^d
warning off; select windows; on key ^e

REM *** close Windows apps ***

warning off; select calc; close
warning off; select calendar; close
warning off; select clock; close
warning off; select notepad; close

REM *** exit from Windows ***
warning off; select msdos; close


PWS Help File (called from PWS.BR)

Help Text for PWS, the Programmer's Workstation

This is an example of a programmer's workstation including:

  ■  Setting the default drive and working directory
  ■  Running a communications program as a background task
  ■  Running a DOS shell and command line
  ■  Some desktop tools
  ■  A link to an electronic mail system
  ■  Help documentation specific to the workstation
  ■  "Hot keys" for quickly switching between applications

COMMANDS:

    Set Current Drive - use this to log the drive containing your
      project's source tree.

    Set Current Directory - specify the current directory for your
      project.

    Online Communication - start a comm program that can be used for
      background file transfers while working.

    DOS Command Line - runs COMMAND.COM in a virtual machine for normal
      access to DOS capabilities.

    Programming Tools - edit, make, debug, and SCCS the parts of your
      current project.  These commands assume that you have already set
      the current drive and directory. You must create PIF files for each
      of these choices that define your particular tools.

    Windows Tools - Provides access to four desktop tools-calculator,
      calendar, clock, and notepad.

    Email - Allows checking or sending of electronic mail messages. (The
      email system is not available for this demo.)

    Help - Displays this help information.

    Exit - Closes all open applications and exits from PWS.


HOT KEYS:

The following hot keys are available to quickly switch to a specific
application.

     Ctrl-C (for Comms)    - switch to the communication session
     Ctrl-D (for Debugger) - switch to the current debugging session
     Ctrl-E (for Editor)   - switch to the current edit session

████████████████████████████████████████████████████████████████████████████

A Guide to Understanding Even the Most Complex C Declarations

Greg Comeau☼

The C language has been around for a good number of years, but many parts
of its syntax are still not clearly understood, even by professional
programmers. The reason for this ambiguity is the lack of unified
documentation covering all the options, especially concerning pointers,
that are now available within the language, including the American
National Standards Institute (ANSI) and Microsoft extensions to the
language.

Unfortunately, as long as these features are not clearly understood, or
worse, misunderstood, programmers will not be utilizing C to its full
capacity. Instead, code is being written, even by talented programmers,
that is more complicated than it needs to be and may in some instances be
incorrect. The purpose of this article is to clarify some of the typical
constructs of C declarations that baffle novice and expert C programmers
alike. As a starting point, let's look at a declaration that many C
programmers find difficult to comprehend:

  struct vtag far * (far * const far var[5])();

If you know for certain what this declaration is saying, just skip to the
end of this article and work on the declaration offered there as a
challenge. If you are at that stage of the game where I was just a short
time ago, that is, you think you know but aren't quite sure, then stay
around. You will be able to read and make use of the preceding identifier
by the end of this article, and you should even be able to explain reading
and writing C declarations to others. Only then can you use the complete
power of the language.


Declaration Syntax

To use a language, you must know something about its structure and syntax.
The first thing to look at regarding C declarations is how they are organ-
ized. Within a given declaration arrangement, different attributes can be
specified. Depending on what attributes are used, the type of an
identifier can be determined. The syntax for explicitly declaring
identifiers in C is shown in Figure 1.

A declaration can contain many of the specifiers shown in Figure 1, but it
must contain at least a type and a declarator. Note that some compilers
will allow only a declarator to be specified if the identifier is declared
outside any function; C programmers should not use such coding practices,
because it is bad programming style and may lead to routine errors and
maintenance problems. Furthermore, the draft ANSI proposal has made this
practice obsolete; therefore, this article will not discuss cases where
more than one identifier is being declared in a declaration.


Declarations: Theory

Many of us can read declarations such as

  int i;
  char *p;

and we can even decipher

  int *ia[3];
  int  (*ia)[3];

thanks to Brian W. Kernighan and Dennis M. Ritchie, The C Programming
Language (Prentice-Hall, Inc., 1978)-or K&R, as I will refer to it from
here on. By now we've all simply memorized them. Our good fortune is that
these probably account for 85 percent of the declarations you will
encounter. However, understanding the other 15 percent can be an uphill
battle, and that's where the trouble begins.

Many programmers suffer through complex declarations and the usage of the
identifiers declared by them. Guessing is usually the only recourse one
has on short notice because of poor documentation. Sooner or later it will
burn you since guessing means making generalizations that are not
necessarily true. Even though your guess may be close, the low-level code
generated by your compiler may be completely different from what you
wanted.

I wish that I had not been forced to play the guessing game when I first
learned C. This is especially bothersome now since the actual theory
behind reading declarations is very simple. You only need to understand
that declarations are based on the C operator precedence chart, the same
one you use to evaluate expressions. In the case of declarations this
means bindings in the order of

  ( ) or [ ]  highest, associatively from left to right, lowest

with parentheses overriding normal bindings. That's all there is to it.
Knowing this, all we have to do is formulate a set of rules such as the
one suggested in Figure 2.

Let's go through some sample declarations. Using page 200 of K&R as a
reference point, study the declarations shown in Figure 3, which are
parenthesized according to the C language precedence chart.

As you can see, once a declaration has been parenthesized, deciphering it
is merely a case of stating what each parenthesized expression is. This is
similar to parenthesizing arithmetic expressions, for instance, where a
certain addition may have precedence over a certain multiplication. The
only difference is that multiplication and division are binary operators
(they must work on two operands) whereas here we are dealing with unary
tokens, which need only one operand.

Note that every parenthesized declaration in Figure 3 is also a valid
declaration syntactically; they are not shown merely for their
equivalence.


Declarations: Usage

Although the rule stated in Figure 1 is simple, its disadvantage is that
you've got to sit there and parenthesize the declaration. Instead of
spending a lot of time parenthesizing "subdeclarations," it would be
helpful to generalize the theory into something more useful. The set of
rules in Figure 4, sections 1, 2, 3, and 5 permit you to read declarations
"on the fly." At first glance these rules appear to be more complex than
the rule in Figure 1, but they are merely expanded versions of that rule.

After going through several sample declarations, you'll see how natural
these rules are. They are really the same as the rule shown in Figure 1,
but since we know about operator precedence, there's no sense in actually
parenthesizing the declaration. Taking the examples from Figure 3, we will
get the results shown in Figure 5.

Creating several obvious derivations from K&R's declarations we get
declarations such as the ones shown in Figure 6. And using derivations
from examples in the Microsoft(R) C Compiler (MSC) Version 5.0 Language
Reference we get the declaration in Figure 7. We know this declaration
must be different from "char *(*(abc()))[10]", in which abc is a function
returning a pointer to a pointer to an array of 10 pointers to char, which
can also be written as "char *(**abc())[10]". Finally, Figure 8 shows an
example of a union declaration.


Writing Declarations

Rules for writing declarations from English descriptions are every bit as
simple since it's exactly what we've done so far, only in reverse. Figure
4, sections 1, 2, 4, and 5 show the rules for accomplishing this. The
examples from Figures 5, 6, 7, and 8 are listed in Figure 9, but in
reverse translation.


ANSI Extensions

The X3J11 committee of the ANSI has enhanced the K&R definition of C by
adding keywords (see Figure 10) and nomenclature (mainly function
prototypes, which we will not get into) to its C language standardization
proposal.

The keywords we will discuss are const and volatile. The const type
qualifier specifies that the object associated with that type will not be
modified, that is, it will not be assigned, incremented, or decremented.
The volatile type qualifier specifies that the object associated with that
type must be evaluated according to the sequence rules of C, which
guarantee that C statements and objects used by these statements must
follow a specific order of execution. Since the sequence rules generally
rule out a large class of optimizations that can be applied to a given
piece of code, the values of volatile objects can be modified by something
other than the program "owning" the object that has been declared without
fear of inconsistency.

Points to note regarding keywords include:

  ■  a const cannot be assigned to

  ■  a nonvolatile const can be placed in ROM

  ■  a volatile can be changed by such mechanisms as DMA, asynchronous
     processes, or shared memory

  ■  a volatile cannot be "optimized out"─the real machine must follow
     the abstract C machine involving these identifiers

  ■  an object without any of these attributes, that is, an unqualified
     object, can be read from or written to without fear of distortions
     of any kind, except synchronously via another object, such as a
     pointer

To understand these keywords better let's look at a practical example, an
excellent one from the proposed standard that provides a declaration for a
memory-mapped input port connected to a real-time clock. This is given as

  extern const volatile
  int real_time_clock;

which declares an integer that cannot be modified by the program (const),
but can be modified by some external event (volatile), the clock.

Now that we have some idea of what these keywords accomplish, we must
integrate their usage into our rules. An addendum to our previous rules is
shown in Figure 11. The ramifications of this are that type qualifiers
modify the type of an object, whether it is the base type or a pointer
type. So, in the declaration

  const int  i;

i is declared as an integer that cannot and will not be modified, for
example, through assignment. However, in such a situation, i may be
initialized when it is being declared since it is an error to have a
statement assigning i = some value after you've declared a given
identifier as a const. It is essential to understand that const will
modify what one can do with i, but it will not change anything about the
basic type directly; this is true of all the qualifiers. An analogy would
be that painting a big green house another color (changing one of its
characteristics) would not change its size or the fact that it is a
house.

Besides qualifying an identifier or object, qualifiers can also qualify
pointers. You must understand here how to code a qualified pointer, which
is probably one of the most difficult aspects of reading declarations. For
instance, would a constant pointer be coded as "int const * p" or "int *
const p"? If "int * p" says that p is a pointer to an int, then "int const
* p" says p is a pointer to a const int. So, since p is a pointer to a
constant, then it must be a constant pointer, right? Maybe, since in the
case of "int * const p," p itself is a const that is a pointer to an int,
so it is also a constant pointer, right?

It can only be one or the other. Let's explore this in more detail, as a
similar problem will come up later on when we discuss the Microsoft
extensions (near and far keywords) to C. The point is to understand the
difference between a "constant pointer" and a "const pointer." Although it
is a small difference, it resolves an ambiguity that crops up in using
generic terms like constant pointer.

The difference is that a constant pointer does not change; it is constant,
a pointer whose value cannot change. Pointers that are not constant are
variable. A const pointer (const *), on the other hand, is a pointer to a
constant of some given type. Please note the use of the English word
constant in one explanation and the keyword const in the other; they are
not the same.

Here are some examples to reinforce this:

  int  i;

i is a variable integer; it can be assigned various values.

  const int  i;

i is a constant integer; it cannot be assigned any values after it has
been declared.

  int  *p;

p is a variable pointer to a variable integer; both p and the integer that
it points to can be assigned values.

  int  * const p;

p is a constant pointer to a variable integer; p cannot change value, the
integer it points to can.

  const int  *p;

p is a variable pointer to a constant integer; p can be modified, the
integer it points to cannot.

  int const  *p;

p is a variable const pointer to an int. Since a const pointer is a
pointer to a constant integer, p is a variable pointer to a constant
integer, the same as const int  *p.

  const int  * const p;

p is a constant pointer to a constant integer. Neither p nor the integer
it points to can be modified.

  const  *p;

p is a variable pointer to a constant integer. This confirms const int *
p and int const *p since there is no other way to read it. However,
note that ANSI considers the lack of an explicit type declaration to be
bad form as it encourages sloppiness in coding declarations.

With these examples under your belt, you can now go back and look at some
derivations of the examples used earlier on. For simplicity, I will only
use the const attribute in the examples below. Microsoft C only
acknowledges the volatile keyword syntactically, not semantically. Also,
we will leave out the so-called variable attribute that I introduced since
that is understood.

  const int  i;

i is a constant int.

  const int  *i;

i is a pointer to a constant int.

  int  * const i;

i is a constant pointer to an int.

  const int * const i;

i is a constant pointer to a constant int.

  const int  *i[3];

i is an array of three pointers to constant int.

  int  * const i[3];

i is an array of three constant pointers to int.

  const int  * const i[3];

i is an array of three constant pointers to constant int.

  const int  (*i)[3];

i is a pointer to an array of three constant ints.

  int (* const i)[3];

i is a constant pointer to an array of three ints.

  int (const *i)[3];

This is an error since const cannot immediately follow a left
parenthesis.

  const int (const * const i)[3];

This is also an error since const cannot immediately follow a left
parenthesis.

  const int (* const i)[3]

i is a constant pointer to an array of three constant ints.

  const int  *i();

i is a function returning a pointer to a constant int.

  int  * const i();

i is a constant function returning a pointer to an int. Many compilers
will choose to ignore the qualification of function types. To ease
parsing, ANSI does allow it without requiring that a compiler issue a
diagnostic; however, ANSI has classified this as "undefined behavior." At
first glance, qualification of function types appears to be meaningless
but it might be used to specify that the function be placed in ROM.

  const int (*i)();

i is a pointer to a function returning a constant int.

  int   (* const i)();

i is a constant pointer to a function returning an int.

  int   (const *i)():

This is an error since const cannot immediately follow a left
parenthesis.

  const int   **i;

i is a pointer to a pointer to a constant int.

  int   ** const i;

i is a constant pointer to a pointer to an int.

  int   * const *i;

i is a const pointer to a pointer to an int; i is a pointer to a constant
pointer to an int.

  int   const **i;

i is a pointer to a pointer to a constant int.

  int   const * const *i;

i is a pointer to a constant pointer to a constant int.

  const int const * const *i;

This is a syntax error, since the first two const keywords are being
applied to the type and not to the first asterisk.

  const int * const * const i;

i is a constant pointer to a constant pointer to a constant int.

You may have noticed in the descriptions above, in particular those of
"const int *i[3]", "int * const i[3]", and "const int * const i[3]", that
the terminology used is not "constant array," but is "array of constant
..." instead. A brief explanation is in order.

If, while parsing a declaration, the last type is an array type and the
current type is a qualified type, we say that the elements of the array
should inherit the qualified type and not the array type itself. This
makes sense when you consider some of the idiosyncrasies of array types
and arrays, specifically that an array represents an address and does not
have many other attributes beyond that since the address is merely some
memory location. Besides, it's actually the elements of the array that we
want to give the qualification to because we do not want anything to be
assigned to them. In addition, you should keep in mind that an array
invocation is also a constant.


MSC Extensions

Along with ANSI, Microsoft has also considered it necessary to add new
keywords to the C language (see Figure 12). Four of them are applicable to
attributes given to functions.

The first three, cdecl, pascal, and fortran, deal with calling functions
written in a language other than C (BASIC, FORTRAN, MASM, or Pascal) or
letting C functions be called from other languages. These special keywords
are really compiler directives that tell the compiler to generate code so
that parameters to a function are handled as if they were in the language
used in the function being called. So, in order to declare a function from
C that might be written in Pascal, you would use:

  extern int pascal func(long, int);

A pointer to such a function might look like this:

  int (pascal *fp)(long, int);

The interrupt keyword is also a compiler directive and thus informs the
compiler to generate special code. In this case the code would allow the
function that is given the interrupt attribute to function according to a
certain sequence of events that interrupt handlers should follow.

The other three special keywords, near, far, and huge, deal with
addressing an object on a machine that supports a segmented architecture
such as that of the Intel microprocessor family, the 80286/88 and 8086/88
series). These keywords, especially the far attribute, will be used often
in the mixed-memory model programming typical of this architecture.

Microsoft C treats all the special keywords as if they were declarator
(not declaration) qualifiers, that is, they can only modify syntactic
entities on the right-hand side of a declaration and cannot modify base
types, such as int, directly. The Microsoft C manual states that the
special keywords modify the "item" immediately to their right in a manner
similar to the const keyword. The manual's description is incomplete on
this, though.

Recalling the discussion on the ANSI additions, this means that the
special keywords either modify an identifier or pointer, that is, they
modify objects or pointers to objects. There is a difference between the
ways these are handled, however, which is not clear from the manual. In
Microsoft C, the syntax is organized so that pointers can be modified by
one of the special keywords. Therefore a sequence such as

  int (far *p);

is valid, but since ANSI prohibits such syntax

  int (const * p);

is not. What this boils down to is that Microsoft C will accept a
declarator syntax of

  MODIFIER * more_declarator_info

while ANSI only accepts

  * QUALIFIER more_declarator_info

where MODIFIER and QUALIFIER are optional positional parameters.

Keep in mind, though, that both far and near were made part of the
Microsoft C language long before const and volatile came along. The reason
for this unfortunate difference is that ANSI borrowed the syntax of type
qualifiers from C++ (a superset of C supporting some object-oriented
programming features), which was being developed independently around the
same time that Microsoft introduced its modifiers.

The discussion concerning the difference between const pointer and
constant pointer holds true here as well. In the case of these attributes,
say far, the situation is even more ambiguous since far is already an
English word. In other words, is int * far p a far pointer (since p is a
pointer located in far memory), or is int far * p a far pointer (since it
is a pointer that points to a far address)?

We resolve it in exactly the same manner as we do the const case. We
remain consistent and declare that "far *" (a keyword modifying a pointer)
means far pointer. As for the other case, we say that a pointer located in
far memory is called a distant pointer; this is similar to the variable
versus constant idea used to clarify the const keyword.

One final point to remember is that the Microsoft C memory model keywords
cannot be applied to automatic variables. This makes sense since they are
stack-based variables and there is no mechanism for you to control their
nearness or farness. This does not imply that you cannot have local static
variables that are near or far, nor does it mean that you cannot have
automatics that are pointers to data in segments other than the segment
that the function containing the automatic variable is located in. All it
says is that the automatic itself cannot have the attribute.

Finally, here are several more examples to emphasize these points:

  int * far p;

p is a distant pointer to an int.

  int far * p;

p is a far pointer to int. Since a far pointer is a pointer to far, p is a
pointer to a far int.

  int far * far p;

p is a distant pointer to a far int.


Down the Road

As of this writing I do not have Version 5.1 of the Microsoft C Compiler.
It will be interesting to see if it has any new surprises. In the
meantime, you should certainly be familiar enough with C declarations and
the rules of the road by now to be able to go back and solve the
declaration put forth at the beginning. Try to figure it out before
reading on. Just to be sure we are all in agreement,

  struct vtag far * (far * const far var[5])();

says: var is a constant distant array of five pointers to far functions
returning pointers to far struct vtags.

It's not really all that difficult, is it? Now, let's see if you really
understand it. What does the following declare?

  unsigned long(far * (far * const(far * far const V[2])[4])())[6];

As a final thought, consider the possibility of writing a program that
takes as its input either a C declaration or an English declaration and
produces either the appropriate English translation or the appropriate C
declaration. Remember to take into consideration the case where there may
be more than one declarator, such as int far * p, far q;──should this
occur, immediately stop all processing and issue a stern warning declaring
this to be poor programming style. Have fun.


Figure 1:  Standard Syntax for C Declarations

The syntax of a C declaration is: storage-class type qualifier declarator
= initializer; where storage-class is only one of the following:

  typedef
  extern
  static
  auto
  register

Type could be one or more of the following:

  void
  char
  short, int, long
  float, double
  signed, unsigned
  struct ...
  union ...
  typedef type

A declarator  contains an  identifier and one or more, or none at all, of
the following in a variety of combinations:

  *

  ()

possibly grouped within parentheses to create different bindings.


Figure 2:  Rules for Reading and Writing K&R Declarations

  1.  Parenthesize declarations as if they were expressions.

  2.  Locate the innermost parentheses.

  3.  Say "identifier is" where identifier is the name of the variable.
      Say "an array of X" if you see [X].
      Say "pointer to" if you see *.

  4.  Move to the next set of parentheses.

  5.  If more, go back to 3.

  6.  Else, say "type" for the remaining type left (such as short int).


Figure 3:  Interpreting Declarations by Parenthesizing

int      i;
int      (i);
│         │
│         ▼
│         i is
▼
an int
───────────────────────────────────────────
int      *i;
int      (*(i));
│         │ │
│         │ ▼
│         │ i is
│         ▼
│         a pointer to
▼
an int
───────────────────────────────────────────
int      *i[3];
int      (*((i)[3]));
│         │  │
│         │  ▼
│         │  i is an array of 3
│         ▼
│         pointers to
▼
int
───────────────────────────────────────────
int      (*i)[3];
int      ((*(i))[3]);
│          │ │
│          │ ▼
│          │ i is
│          ▼
│          a pointer to
▼          an array of 3
ints
───────────────────────────────────────────
int      *i();
int      (*((i)()));
│         │  │
│         │  ▼
│         │  i is a function returning
│         ▼
│         a pointer to
▼
an int
───────────────────────────────────────────
int      (*i)();
int      ((*(i))());
│          │ │
│          │ ▼
│          │ i is
│          ▼
│          a pointer to a function returning
▼
an int


Figure 4:  Reading and Writing K&R Declarations

1. Given: Intermediate attributes are [], (), and *, implying array,
   function, and pointer, respectively.

2. Memorize right-to-left rule: Look to the right (within parentheses),
   pick up intermediate attributes if any, then look to the left and pick
   up intermediate attributes, if any.

3. To convert a C declaration to English:
   a. Locate the identifier in the declaration. Say "identifier is" where
      identifier is the name of the variable.

   b. Look to the right of the identifier for the intermediate attributes
      () or []. Note there may be none.
      Say "an array of" if you see [].
      Say "an array of x" for each [x] you see.
      Say "an x-by-y array of" if you see [x][y].
      Say "an x-by-y by ... array of" if you see [x][y][...].
      Say "functions returning" for () if the last right attribute found
      was [].
      Say "a function returning" for ().

   c. Now look to the left of the identifier (per the right-to-left
      rule), and look for any further intermediate attributes. We're only
      concerned about asterisks here, and any other attribute would be an
      error. Note there may be none. Also, be aware of parentheses. So,
      for each *, say "pointers to" if the last attribute found on the
      right was [] and the current attribute is *, otherwise, say "a
      pointer to" if you see *.

   d. Look to the right again for any more intermediate attributes. There
      may be none. Be careful of parentheses here. If any, go back to b.

   e. You should be left with terminating attributes: char, int, short,
      long, float, double, struct, union, and/or their respective
      modifiers-signed, unsigned, static, register, and extern.
      Say "struct of type y" for struct y.
      Say "union of type y" for union y.
      Say "attribute" read left-to-right (verbatim) for the terminating
      attributes.

4. Rules for converting English to a C declaration:
   a. Write "identifier", where identifier will be the name of the
      variable.

   b. We will need to keep track of whether the last attribute in our
      processing was an asterisk by using a flag. We will call our flag
      "active-*" and set it equal to 0.

   c. Write "*" to the left of what you've written down as long as you see
      "pointer to" or "pointers to" in the English description. Also set
      active-*=1 (signal that the last attribute was an asterisk).

   d. Write "(previous_written_down_attributes )" if active-*=1.
      Write "[x]" to the right if you see "array of x."
      Write "[x][y]..." to the right if you see "an x-by-y array of."
      Write "[]" to the right if you see "array of."
      Write "()" to the right if you see "function returning" or
      "functions returning."

   e. Go to b if there are still more intermediate attributes in the list.

   f. Write "simple attribute" to the left of everything, where simple
      attribute is one of the combinations of terminating attributes
      (keywords) shown in Figure 1.
      Write ";" to right of everything.

5. Notes:
   a. You cannot have an array of functions. You can have an array of
      pointers to functions, though. The declaration "int a[5]()" is
      invalid.

   b. A function cannot return an array. A function can return a pointer
      to an array, though. The declaration "int a()[]" is invalid.

   c. A function cannot return another function, only a pointer to one,
      meaning that "int a()()" is invalid.


Figure 5:  Interpreting Declarations Using Rules

int    i;
│      │
│      ▼
│      i is
▼
an int
───────────────────────────────────────────────────────────────────────────
int    *i;
│      ││
│      │▼
│      │i is
│      ▼
│      a pointer to        (nothing to the right, so look to the left)
▼
an int                     (terminating attribute is all that's left)
───────────────────────────────────────────────────────────────────────────
int    *i[3];
│      ││
│      │▼
│      │i is an array of 3 (first look to the right)
│      ▼
│      pointers to         (no more attributes to right, so look to left)
▼
an int                     (terminating attribute is all that's left)

───────────────────────────────────────────────────────────────────────────
int    (*i)[3];
│       ││
│       │▼
│       │i is
│       ▼
│       a pointer to       (found parentheses-nothing to right, * to left)
│
│       an array of 3      (finished paren attributes, look to right)
▼
ints                       (terminating attribute is all that's left)
───────────────────────────────────────────────────────────────────────────
int    *i();
│      ││
│      │▼
│      │i is a function
│      │returning          (found () to the right)
│      ▼
│      a pointer to        (nothing more to right, * on the left)
▼
an int                     (terminating attribute is all that's left)
───────────────────────────────────────────────────────────────────────────
int    (*i)();
│       ││
│       │▼
│       │is is
│       ▼
│       a pointer to       (* is within parentheses)
│       a function
│       returning          ( () is attribute to the
│                            right of the parenthesis)
▼
an int                     (terminating attribute is all that's left)


Figure 6:  Interpreting Declarations Using Rules

int    **i;
│      │││
│      ││▼
│      ││i is
│      │▼
│      │a pointer to           (no attributes to right, * to left)
│      ▼
│      a pointer to            (another * to the left)
▼
an int
───────────────────────────────────────────────────────────────────────────
int    *(*i)();
│      │ ││
│      │ │▼
│      │ │i is
│      │ ▼
│      │ a pointer to a        (nothing to right, * to left)
│      │ function returning    (finished with parentheses, () is to right)
│      ▼
│      a pointer to            (no more attributes on right, try left)
▼
an int
───────────────────────────────────────────────────────────────────────────
int    *(*i[])();
│      │ ││
│      │ │▼
│      │ │i is an array of     (stay with parens, look to right first)
│      │ ▼
│      │ pointers to           (now to left)
│      │ functions returning   (finished with parens so look to their right)
│      ▼
│      pointers to
▼
an int


Figure 7:  Interpreting Declarations Using Rules

char    *(*(*i)())[10];
│       │ │ ││
│       │ │ │▼
│       │ │ │i is
│       │ │ ▼
│       │ │ a pointer to a function returning
│       │ ▼
│       │ a pointer to an array of 10
│       ▼
│       pointers to
▼
a char


Figure 8:  Interpreting Declarations Using Rules

union sign    *(*i[5])[5];
│             │ ││
│             │ │▼
│             │ │is is an array of 5
│             │ ▼
│             │ pointers to arrays of 5
│             ▼
▼             pointers to
union sign


Figure 9:  Deriving Declarations from English

i is -► an int
│       │
▼       │
i       │
        ▼
        int i;
───────────────────────────────────────────────────────────────────────────
i is -► a pointer -► to an int
│       │            │
▼       │            │
i       │            │
        ▼            │
        *i           │
                     ▼
                     int *i;
───────────────────────────────────────────────────────────────────────────
i is -► an array -► of 3 pointers -► to int
│       │           │                │
▼       │           │                │
i       ▼           │                │
        i[]         ▼                │
                    *i[3]            │
                                     ▼
                                     int *i[3];
───────────────────────────────────────────────────────────────────────────
i is -► a pointer -► to an array -► of 3 ints
│       │            │              │
▼       │            │              │
i       ▼            │              │
        *i           ▼              │
                     (*i)[]         │
                                    ▼
                                    int (*i)[3];
───────────────────────────────────────────────────────────────────────────
i is a function returning  -► a pointer -►  to an int
│                             │             │
▼                             │             │
i()                           ▼             │
                              *i()          │
                                            │
                                            ▼
                                            int *i();
───────────────────────────────────────────────────────────────────────────
i is a pointer -► to a function returning  -►   an int
│                 │                             │
▼                 │                             │
*i                │                             │
                  ▼                             │
                  (*i)()                        │
                                                ▼
                                                int (*i)();
───────────────────────────────────────────────────────────────────────────
i is a pointer  -► to a pointer   -► to an int
│                  │                 │
▼                  │                 │
*i                 │                 │
                   ▼                 │
                   **i               │
                                     ▼
                                     int **i;
───────────────────────────────────────────────────────────────────────────
i is a pointer -► to a function returning -► a pointer -► to an int
│                 │                          │            │
▼                 │                          │            │
*i                ▼                          │            │
                  (*i)()                     ▼            │
                                             *(*i)()      ▼
                                                          int *(*i)();
───────────────────────────────────────────────────────────────────────────
i is array-► of pointers-► to functions returning-► pointers-► to int
│            │             │                        │          │
▼            │             │                        │          │
i[]          ▼             │                        │          │
             *i[]          ▼                        │          │
                           (*i[])()                 ▼          │
                                                    *(*i[])()  ▼
                                                               int*(*i[])();
───────────────────────────────────────────────────────────────────────────
i is a pointer-► to a    -► returning-► to an array -► to char
│                function   a pointer   of 10 pointers │
│                │          │           │              │
▼                │          │           │              │
*i               ▼          │           │              │
                 (*i)()     ▼           │              │
                            *(*i)()     ▼              │
                                        (*(*i)())[]    │
                                        (*(*i)())[10]  │
                                        *(*(*i)())[10] ▼
                                                       char*(*(*i)())[10];
───────────────────────────────────────────────────────────────────────────
i is an array-► of 5 pointers-► to arrays of 5-► union signs
│               │               pointers to      │
▼               │               │                │
i[]             ▼               │                │
                i[5]            ▼                │
                *i[5]           (*i[5])[]        │
                                (*i[5])[5]       │
                                *(*i[5])[5]      ▼
                                                 union sign *(*i[5])[5];


Figure 10:  ANSI Extensions

The syntax of a C declaration as mandated by the Draft ANSI proposal:

  storage-class type qualifier declarator = initializer;

Additional new qualifiers can be one or more of the following:

  const
  volatile

Additional new types can be one of the following:

  void
  signed char
  unsigned char
  unsigned int
  unsigned long
  long double
  enum ...


Figure 11:  Reading and Writing ANSI Declarations

Reading and writing ANSI declarations is similar to reading and writing
K&R declarations. However, there are a few additional concerns due to the
addition of type qualifiers. They are as follows:

  1.  Our main concern with proposing an extension to the rules given in
      Figure 4 is that the ANSI additions are keywords and don't
      necessarily have a precedence. Therefore keywords don't follow a
      consistent pattern per se.

  2.  Disregarding other type specifications, each qualifier (const,
      volatile) has a corresponding pointer type (const *, volatile *).

  3.  A missing type specifier is taken to be int. For example, "const x"
      means "int const x". However, this should be considered obsolescent
      style.

  4.  Type qualifiers and type specifiers may be intermixed without
      concern for their order. It will not change the resulting type in
      question. For instance, "const int var" and "int const var" are the
      same.

  5.  Intermixing type qualifiers within  declarators (the part of a
      declaration specifying the identifier and function, array, and
      pointer attributes) does change the meaning of a declaration.
      Therefore the binding of qualifiers changes depending upon context.

  6.  To further clarify (d) and (e) and to make clear a case such as
      "const int * p", we need to propose the following: type qualifiers
      modify the type of a declarator. Also, the case of a declaration
      involving pointers such as the case of "type * type-qualifier(s)
      declarator" (say, "int * const  var") is said as "declarator
      qualifier(s) pointer to type" (var is a constant pointer to int).

  7.  Nonqualifier declarations (those using  default qualifications) can
      be read as such. So, nonconst is "variable"  and nonvolatile is
      "nonvolatile." It is usually good practice to include the default
      qualifications of an identifier when translating declarations, even
      though the default qualification is not a keyword.


Figure 12:  Microsoft Extensions

Microsoft has  extended the  K&R and  ANSI definitions with extensions of
two keywords in the area of qualifiers, as follows:

  far
  near
  huge
  cdecl
  pascal
  fortran
  interrupt

████████████████████████████████████████████████████████████████████████████

Techniques for Debugging Multithread OS/2 Programs with CodeView  2.2

Charles Petzold☼

Debugging is never a pleasant job, even when the program being debugged is
a relatively simple one. But imagine what it's like debugging an OS/2
program with multiple threads of execution. All the threads run
concurrently within the same program. Sometimes a thread works
independently of the other threads, but the threads often interact with
each other.

At first, the very idea of attempting to debug a multithread OS/2
program seems like a nightmare, and at times it certainly is. However,
Microsoft(R) CodeView(R)Version 2.2, included with the OS/2 versions of
Microsoft's language products (BASIC 6.0, C 5.1, FORTRAN 4.1, Macro
Assembler 5.1, and Pascal 4.0), has some new and enhanced commands to aid
the debugging of multithread programs.

Though I encountered some problems, this release of CodeView, Version
2.2, is a powerful tool for the OS/2 programmer. We'll examine this new
support of multithread debugging by using a program called THREADS and
going through some debugging exercises.


Multithread Demo

The THREADS program shown in Figure 1 creates four additional threads of
execution. Each of these four threads writes its thread ID number in
random locations on a quadrant of the video display. You can exit THREADS
by pressing the Escape key.

THREADS makes use of the new multithread (MT) header files and run-time
library (LLIBCMT.LIB) included with Microsoft C 5.1. All the C library
functions in this library are re-entrant and can safely be called from
multiple threads in a C program.

THREADS creates the four threads that are in its main function, each of
which uses the same thread function, called ThreadFunction. Rather than
call DosCreateThread to start each of these threads, THREADS calls the
function _beginthread, a new function in C 5.1 that must be used in C
programs that make use of the multithread library.

Besides providing support for the multithread library functions,
_beginthread has another advantage for the programmer──it allows a far
pointer to be passed to the thread function. In the case of THREADS, this
far pointer addresses the THREADPARAM structure and is used to pass to
each thread the row and column coordinates of its screen quadrant.

Each thread running in ThreadFunction obtains its thread ID from the
local information segment. The thread ID is stored in an automatic
(stack) variable. Because each thread has its own stack, all stack
variables are private to each thread. (Any static variables in a
function are shared among all threads that call the function.)

The rand function is used to write the thread ID in a random location
within a quadrant of the display. The rand function in the LLIBCMT.LIB
library generates an independent pseudo-random sequence for each thread
that calls it, which means that the function would normally return the
same pseudo-random sequence for each thread. In the THREADS program, this
is not quite what we want. To obtain different random sequences for each
thread, ThreadFunction sets a different seed (by calling srand), which is
set equal to the thread ID number.

Note that the two functions CheckKey and WriteChar are called from only
one place in the program and need not be separate functions. This was
done so that breakpoints could be set at these functions.

The THREADS make file shows the -Zi switch used in the compile step and
the /CO switch used in the link step. These two CodeView switches add
debugging information to the THREADS.EXE file.


Freezing and Unfreezing

When debugging a multithread program, it may be desirable to run only
one thread at a time or to stop the execution of one thread while you let
all the others run. For this reason CodeView introduces the concepts of
"freezing" and "unfreezing" threads. A frozen thread does not run. You
freeze and unfreeze threads explicitly with commands entered in the
dialog window at the bottom of the CodeView screen. Other commands
implicitly freeze and unfreeze threads temporarily while the command is in
progress. We'll examine these other commands shortly.

To load THREADS.EXE into the OS/2 version of CodeView, you use the
command:

  CVP THREADS

You can use the /2 switch to run CodeView on a second monitor if you have
one.

The CodeView dialog window displays a prompt:

  001>

This new CodeView prompt indicates the ID number of the "current thread."
When an OS/2 program begins execution, it only has one thread, which
always has an ID number of 1. (Thread 1 has some special characteristics;
for example, it is the thread that handles signals if a signal handler
has been installed by calling DosSetSigHandler.) As a program starts up
additional threads, the thread IDs of the new threads are 2, 3, 4, and so
forth. The _beginthread function and the new multithread library permit a
program to have 32 threads going at once.

Now try the new CodeView Status command:

  001>~

This command lists all threads in the process and their current status. In
this case, the command lists only thread 001 and the status Runnable.

Set a breakpoint at the CheckKey function and enter the Go (G) command:

  001>BP CheckKey
  001>G

The program creates the four threads, and they run normally, displaying
their thread IDs in random locations on the screen.

Suspend the program by pressing the spacebar. Because you've set a
breakpoint on the CheckKey function, pressing any key will cause CodeView
to break at the beginning of the function. Enter the Status command
again:

  001>~

You'll see the display shown in Figure 2☼. Threads 001 through 005 are all
listed as Runnable.

If you use a second monitor for CodeView, you'll notice that the program
being debugged is entirely suspended when CodeView has control. (If
you're using one monitor, you can switch to the session in which THREADS
is running to ensure that it is indeed suspended.) A program being
debugged does not run when CodeView has control.

Now try freezing thread 2. The command is:

  001>~2F

All the new CodeView thread commands begin with the tilde (~). The 2
refers to thread 2; F stands for Freeze. You can see that thread 2 is
frozen by entering the Status command again:

  001>~

Thread 002 is now listed as "Frozen." When you enter the Go command
again:

  001>G

you'll see that thread 2 has stopped.

Now press the spacebar again to break at the CheckKey function. You can
freeze all threads by using an asterisk rather than a number:

  001>~*F

The Status command:

  001>~

now lists all threads as Frozen. Let's Go again:

  001>G

You may wonder what the Go command is doing in this case if all threads
are frozen. Actually, Go temporarily unfreezes the current thread, which
is indicated by the prompt as thread 001, before letting the program run.
You can thus break again by pressing the spacebar.

Now unfreeze thread 5 and Go:

  001>~5U
  001>G

The Unfreeze command is U; it uses the same syntax as the Freeze command.
Only the first and fifth threads run now.

Press the spacebar, unfreeze all threads, and Go:

  001>~*U
  001>G

All threads are now running normally. Press the Escape key to end the
THREADS program. CodeView will break at the CheckKey function. You can
exit normally by another Go command:

  001>G

and then a Q command to quit CodeView:

  001>Q


Command Syntax

All the new multithread commands in CodeView begin with a tilde (~). The
Status command is either a tilde by itself or followed by a number,
period, pound sign, or asterisk, as shown in Figure 3.

Thus, the ~ and ~* commands are the same-they list all active threads. You
can get the status for only one thread by following the tilde by a thread
number. The period gives the status of the current thread, which is the
thread indicated by the CodeView prompt. The ~# command gives the status
of the "last executed" thread. This is usually the same as the current
thread but may not be if you change the current thread without executing
any code.

The Freeze and Unfreeze commands are shown in Figure 4. They permit
selective freezing or unfreezing of a specific thread, the current
thread, the last executed thread, and all threads.


Breakpoints

In the exercise with freezing and unfreezing, we used the normal BP
command to set a breakpoint at the CheckKey function. The CheckKey
function is called only from thread 1, so there was no ambiguity about
how CodeView interprets this command. But what if you want to set a
breakpoint at a code line shared by several threads (such as a line in
ThreadFunction)? Does the breakpoint apply to one thread or all threads?
The answer is whatever you want.

Load THREADS into CodeView again:

  CVP THREADS

and set a breakpoint at the CheckKey function:

  001>BP CheckKey

Also set a breakpoint at ThreadFunction:

  001>BP ThreadFunction

The BP command by itself applies to all threads. You can easily confirm
this by entering the Go command:

  001>G

CodeView will return control to you when one thread (probably but not
necessarily thread 2) starts up and reaches the beginning of
ThreadFunction. (Although CodeView breaks at the beginning of
ThreadFunction, this does not mean that thread 2 has not executed any code
before it breaks. Before reaching ThreadFunction, the thread has already
executed some code associated with the function _beginthread.)

When CodeView returns control to you, you'll see a prompt indicating that
thread 2 (or whatever) is now current:

  002>

The display in CodeView's code window shows the cursor placed at the
beginning of ThreadFunction, as demonstrated in Figure 5☼

You can continue by entering another Go command:

  002>G

Now thread 3 will break at the ThreadFunction. Continue execution with
another Go:

  003>G

Thread 4 breaks. Enter another Go command:

  004>G

Thread 5 breaks. Another Go:

  005>G

Now all five threads of the program are running normally.

Press the spacebar to break thread 1 at the CheckKey function. Now set a
breakpoint at the WriteChar function. Although the WriteChar function is
called from threads 2 through 5, this breakpoint will apply only to thread
5:

  001>~5BP WriteChar

As with all thread-specific commands, the command begins with a tilde. It
is followed by the thread ID number and BP, the normal breakpoint
command. Go again:

  001>G

Now thread 5 will break at WriteChar. The code window shows the cursor at
the beginning of WriteChar and the prompt indicates thread 5:

  005>

You can get a list of all breakpoints with the normal BL command:

  005>BL

CodeView displays the information shown in Figure 6☼.

For breakpoints that apply to a specific thread (such as breakpoint 2 in
this example), the listing shows a tilde and thread ID. A breakpoint that
applies to all threads, such as breakpoints 0 and 1, does not have this
information.

Clear breakpoint 2 and set a new breakpoint at WriteChar that breaks when
thread 5 executes WriteChar 100 times:

  005>BC 2
  005>~5BP WriteChar 100
  005>G

The program will run for awhile (more slowly than normal) and will then
break at the WriteChar function. Clear the breakpoint and set one that
breaks when WriteChar is executed 100 times regardless of the thread that
executes it:

  005>BC 2
  005>BP WriteChar 100
  005>G

This time CodeView returns faster because the count applies to all threads
rather than just thread 5. When the breakpoint occurs, the current thread
might be 2, 3, 4, or 5. (I've used a question mark below to indicate
this.)

You can clear all the breakpoints by using the BC command and Go:

  00?>BC*
  00?>G

Press the Escape key to end the program and quit CodeView:

  001>Q


The Breakpoint Command

Figure 7 shows the variations of the CodeView breakpoint commands. The BP
and ~*BP commands are equivalent and apply to all threads. For example,
when you set a breakpoint at the CheckKey function you can use either:

  001>BP CheckKey
  or:
  001>~*BP CheckKey

Because only thread 1 calls the CheckKey function, you can also use:

  001>~1BP CheckKey

If thread 1 is the current thread (as indicated by the CodeView prompt),
you can also use:

  001>~.BP CheckKey

You can set a breakpoint for a thread that does not yet exist. For
example, when you first load THREADS into memory, you can use:

  001>~5BP ThreadFunction

CodeView will only break when thread 5 begins execution of
ThreadFunction. You can use line numbers or absolute addresses with any
variation of the breakpoint command. You can also use the pass count and
command options of BP. When you set a breakpoint using the mouse (by
clicking on a line of code) or the F9 key, that breakpoint applies to all
threads. Only by using the BP command in the dialog window can you set a
breakpoint for a particular thread.


The Go Command

We've used the Go command in the exercises involving freezing and
unfreezing and breakpoints, and it's been working as we might expect. The
Go command simply allows the program to run; control returns to CodeView
when a breakpoint is encountered. The only oddity encountered was in the
freezing and unfreezing exercise when we froze all the threads and
discovered that the Go command allowed thread 1 to run.

Figure 8 shows the variations of the CodeView Go command. As indicated by
the last column in Figure 8, the Go command always temporarily unfreezes a
particular thread if the thread is frozen. When control returns to
CodeView, it restores the status of the thread to its former state. When
using the thread-specific versions of the Go command (~nG, ~.G, or ~#G),
CodeView temporarily freezes all threads except the thread you've
specified. This lets you use the Go command to control execution of a
single thread while other threads are prevented from running.

Let's run through a short exercise to see how this works. Load THREADS
into CodeView:

  CVP THREADS

Set a breakpoint at the function WriteChar that applies only to thread 5
and Go:

  001>~5BP WriteChar
  001>G

CodeView will break when thread 5 reaches the WriteChar function. The
prompt indicates thread 5 as the current thread:

  005>

You can enter a few successive Go commands and observe that all threads
run between the breaks:

  005>G

Now try a Go command that applies only to thread 5:

  005>~5G

In this case, only thread 5 runs. CodeView temporarily freezes all other
threads before letting the program run. Clear the breakpoint and Go:

  005>BC*
  005>G

End the program by using the Escape key and exit CodeView:

  005>Q

The Go command executed from the menu or the F5 key works the same as G
entered in the dialog window. However, when a breakpoint occurs, it will
not be immediately apparent which thread caused the break. For this
reason, it's often preferable to use the G command in the dialog window
when debugging multithread programs.


Breakpoints and Singlestepping

Before proceeding, it will be helpful to review the debugging facilities
supported by the 80286 microprocessor. A debugger can set a breakpoint
at an assembly language instruction by replacing the first byte of the
instruction with CCH, which is an Interrupt 3 instruction. When the
program executes that instruction, the resulting Interrupt 3 passes
control to the debugger. The debugger can then restore the original byte
before allowing the program to continue.

A debugger can single-step through a program's code by setting the Trap
flag. When the Trap flag is set, an Interrupt 2 will be generated after
every instruction.

In OS/2, a debugger does not set the Trap flag directly. This is handled
by the DosPTrace function, the OS/2 debugging function. Similarly, when
an Interrupt 2 or 3 is generated, control passes to OS/2, which then
returns from an earlier DosPTrace call made by the debugger. DosPTrace
is documented in the Programmer's Reference manual included in the
Microsoft OS/2 Programmer's Toolkit.

When you set a breakpoint with the BP command, CodeView sets the
breakpoint interrupt in your code, which is obvious. When you trace code
using the T (Trace), P (Program Step), and E (Execute) commands, you may
think that CodeView uses the single-step facility, but that's not
necessarily true. When you trace through source code statements, CodeView
uses a breakpoint at the end of the group of assembly language
instructions that constitute a particular statement. When using the P
command to trace through assembly language instructions, CodeView uses a
breakpoint to skip over function calls.

For this reason, the Trace, Program Step, and Execute commands work
somewhat differently in source mode and assembly language (or mixed)
mode when debugging multithread programs.


Tracing

Let's load THREADS into CodeView for an experiment with tracing:

  CVP THREADS

Set a breakpoint at ThreadFunction that applies only to thread 5 and
Go:

  001>~5BP ThreadFunction
  001>G

You should still be in source mode at this time. To trace through source
code statements you can enter this command several times:

  005>~5T

Each time you enter this command, CodeView allows one statement in your
source code to run. You can also use:

  005>~.T

The period indicates the current thread, which is thread 5.

You'll note that none of the other threads runs when you execute this
trace command. When you trace through a single thread with ~.T and ~nT
CodeView temporarily freezes all threads except the one you're tracing.
This is true in both source code mode and assembly mode.

The most difficult command is T used by itself, which works differently in
source mode and assembly mode. To explore this, first switch to mixed
mode:

  005>S&

Now just enter successive T instructions:

  005>T

At the calls to the OS/2 or C library functions you'll want to use the P
command to avoid tracing through function code. Note that none of the
other threads runs while you're tracing. The T command in assembly or
mixed mode works the same as ~.T.

Now switch back to source mode:

  005>S+

and enter several T instructions:

  005>T

In source mode, the T instruction allows all the other threads to run.
Basically, CodeView sets a breakpoint at the end of the group of assembly
language instructions that make up the current source code instruction and
then lets the program run.

However, CodeView does not check which thread breaks at the breakpoint. In
the THREADS program, threads 2 through 5 share the same code. So while
you're using the T instruction in source mode, you'll find that it often
breaks with a different current thread. Watch out for this when you're
tracing through source code that is shared by multiple threads. It's
tempting to use the T command by itself, but you'll probably want to use
~.T or ~nT instead to prevent the current thread from changing.

To end this exercise, just  execute Go:

  00?>G

press the Escape key, and enter Q to exit CodeView:

  001>Q

The syntax of the various Trace, Program Step, and Execute commands are
shown in Figures 9 and 10. Running the Trace and Program Step commands
from the menu or using the F8 and F10 keys work the same as the T command
entered by itself.

If you ever find that the current thread is not the thread you expect or
want, you can change the current thread with the CodeView Select command
~nS. This and other variations of the Select command are shown in Figure
11.

As you've seen, all the thread-specific CodeView commands can use the
pound sign (#) to indicate the last executed thread, which, defined more
precisely, is always the current thread unless you change the current
thread with one of the Select commands. The last executed thread is not
changed by the Select command, only by executing a Go, Trace, Program
Step, or Execute command, in which case the last executed thread is the
current thread after one of these commands returns control back to
CodeView.


Windows and Threads

Although the new CodeView commands are obviously very helpful in debugging
multithread applications, they are not entirely satisfactory. For the
most part, the commands are restricted to the command line interface in
the CodeView dialog window. The use of a single display that is switched
among different threads is also not an adequate visual analog of the
organization of a multithread program.

As CodeView evolves to keep pace with the increasingly complex
architecture of OS/2 applications, a more suitable visual interface
might involve multiple windows to view and control the code in each
thread. The OS/2 Presentation Manager would be an ideal environment for
such a debugger.


Figure 1:  The THREADS Program

#-------------------
# THREADS make file
#-------------------

threads.obj : threads.c
     cl -c -Alfw -G2s -W3 -Zi threads.c

threads.exe : threads.obj threads.def
     link threads, /align:16, NUL, /nod llibcmt doscalls, threads /CO

/*----------------------------------------------------------
   THREADS.C -- Multithread OS/2 Program for CodeView Demo
                Programmed by Charles Petzold, 6/88
  ----------------------------------------------------------*/

#define INCL_DOS
#define INCL_VIO
#define INCL_KBD

#include <os2.h>
#include <mt\process.h>
#include <mt\stdlib.h>

     /*------------------------------------------------
        Structure definition and function declarations
       ------------------------------------------------*/

typedef struct
     {
     SHORT xLeft ;
     SHORT yTop ;
     SHORT xRight ;
     SHORT yBottom ;
     }
     THREADPARAM ;

typedef THREADPARAM FAR *PTHREADPARAM ;

BOOL            CheckKey       (CHAR ch) ;
VOID _CDECL FAR ThreadFunction (PTHREADPARAM ptp) ;
VOID            WriteChar      (CHAR ch, USHORT usRow,
                                USHORT usCol) ;
VOID PASCAL FAR ExitRoutine    (USHORT usTermCode) ;
VOID            ClearScreen    (BOOL fCursorOff) ;

     /*-----------------
        main() function
       -----------------*/

INT _CDECL main (VOID)
     {
     static INT  iStack [4][2048] ;
     static CHAR szErrorMsg[] = "THREADS: Cannot start thread" ;
     KBDKEYINFO  kbci ;
     SHORT       i ;
     THREADPARAM tp[4] ;
     VIOMODEINFO viomi ;

          /*---------------------------------------------
             Clear screen and get video mode information
            ---------------------------------------------*/

     DosExitList (EXLST_ADD, ExitRoutine) ;
     ClearScreen (TRUE) ;

     viomi.cb = sizeof viomi ;
     VioGetMode (&viomi, 0) ;

          /*-----------------------
             Start up four threads
            -----------------------*/

     for (i = 0 ; i < 4 ; i++)
          {
          tp[i].xLeft = i % 2 ? viomi.col / 2 : 0 ;
          tp[i].yTop  = i > 1 ? viomi.row / 2 : 0 ;

          tp[i].xRight  = tp[i].xLeft + viomi.col / 2 ;
          tp[i].yBottom = tp[i].yTop  + viomi.row / 2 ;

          if (-1 == _beginthread (ThreadFunction, iStack[i],
                                  sizeof iStack[i], tp + i))
               {
               VioWrtTTY (szErrorMsg, sizeof szErrorMsg - 1, 0) ;
               return 1 ;
               }
          }

          /*--------------------------
             Wait for Esc key to exit
            --------------------------*/
     do
          KbdCharIn (&kbci, IO_WAIT, 0) ;
     while (!CheckKey (kbci.chChar)) ;

     return 0 ;
     }

     /*-----------------------------------------------------
        CheckKey() function -- Returns TRUE if ch is Escape
       -----------------------------------------------------*/

BOOL CheckKey (CHAR ch)
     {
     }

     /*------------------
        ThreadFunction()
       ------------------*/

VOID FAR _CDECL ThreadFunction (PTHREADPARAM ptp)
     CHAR   ch ;
     TID    tid ;
     USHORT usRow, usCol ;

          /*----------------------------------------
             Get thread ID and convert to character
            ----------------------------------------*/

     DosGetInfoSeg (&selGlobal, &selLocal) ;
     tid = ((PLINFOSEG) MAKEP (selLocal, 0)) -> tidCurrent ;
     ch  = (CHAR) tid + '0' ;
     srand (tid) ;

          /*---------------------------------------
             Display thread ID in random locations
            ---------------------------------------*/

     while (TRUE)
          {
          usCol = ptp->xLeft + rand () % (ptp->xRight - ptp->xLeft) ;
          usRow = ptp->yTop  + rand () % (ptp->yBottom - ptp->yTop) ;
          WriteChar (ch, usRow, usCol) ;
          }
     }

     /*----------------------------------------------------------
        WriteChar() function -- Displays TID, pauses, then blank
       ----------------------------------------------------------*/

VOID WriteChar (CHAR ch, USHORT usRow, USHORT usCol)
     {
     VioWrtCharStr (&ch, 1, usRow, usCol, 0) ;
     DosSleep (100L) ;
     VioWrtCharStr (" ", 1, usRow, usCol, 0) ;
     }

     /*------------------------------------------------
        ExitRoutine() function -- Exit list processing
       ------------------------------------------------*/

VOID FAR PASCAL ExitRoutine (USHORT usTermCode)
     {
     usTermCode ;   /* to prevent compiler warning */

     ClearScreen (FALSE) ;
     DosExitList (EXLST_EXIT, NULL) ;
     }

     /*--------------------------------------------------------
        ClearScreen() function -- Also turns cursor off and on
       --------------------------------------------------------*/
VOID ClearScreen (BOOL fCursorOff)
     {
     USHORT        usAnsi ;
     VIOCURSORINFO vioci ;

     VioGetAnsi (&usAnsi, 0) ;
     VioSetAnsi (ANSI_ON, 0) ;
     VioWrtTTY ("\33[2J", 4, 0) ;
     VioSetAnsi (usAnsi, 0) ;

     VioGetCurType (&vioci, 0) ;
     vioci.attr = fCursorOff ? -1 : 0 ;
     VioSetCurType (&vioci, 0) ;
     }

;------------------------------------
;  THREADS.DEF module definition file
;------------------------------------

NAME           THREADS   WINDOWCOMPAT

DESCRIPTION    'Multithread Demo Program by Charles Petzold, 1988'
PROTMODE
HEAPSIZE       1024
STACKSIZE      4096


Figure 3:  Variations of the CodeView Status command.

Command      Applies to

~            All threads

~n           Thread n

~.           Current thread

~#           Last executed thread

~*           All threads


Figure 4:  Variations of the CodeView Freeze and Unfreeze commands.

Freeze       Unfreeze
Command      Command     Applies to to

~nF          ~nU         Thread n

~.F          ~.U         Current thread

~#F           ~#U        Last executed thread

~*F          ~*U         All threads


Figure 7:  Variations of the CodeView Breakpoint command.

  Breakpoint Command

Command     Applies to

BP          All threads

~nBP        Thread n

~.BP        Current thread

~#BP        Last executed thread

~*BP        All threads


Figure 8:  CodeView Go command variations.

            Temporarily     Temporarily
Command     Freezes         Unfreezes

G           ──              Current thread

~nG         All threads     Thread n
            except n

~.G         All threads     Current thread
            except current

~#G         All threads     Last executed
            except last     thread
            executed

~*G(+)      All threads     Current thread
            except current

 (+)This is a bug. The ~*G command should  work the same as G.


Figure 9:  Variations of the CodeView Trace command in source mode.
           (The P and E commands work similarly.)

               Trace Command (Source Mode)

           Temporarily      Temporarily       Returns
Command    Freezes          Unfreezes         Control to

  T        -                Current thread    Not predictable

y~nT        All threads      Thread n          Next statement
           except n                           in thread n

~.T        All threads      Current thread    Next statement
           except current                     in current thread

~#T        All threads      Last executed     Next statement
           except last      thread            in last
           executed                           executed thread

~*T☼       All threads      Current thread    Next statement
           except current                     in current thread


Figure 10:  Variations of the CodeView Trace command in assembly or mixed
            mode. (The P and E commands work similarly.)

          Trace Command (Assembly Mode or Mixed Mode)

           Temporarily       Temporarily         Returns
Command    Freezes           Unfreezes           Control to

  T        All threads        Current thread      Next instruction
           except current                         in current thread

~nT        All threads        Thread n            Next instruction
           except n                               in thread n

~.T        All threads        Current thread      Next instruction
           except current                         in current thread

~#T        All threads        Last executed       Next instruction
           except last        thread              in last
           executed                               executed thread

~*T☼       All threads        Current thread      Next instruction
           except current                         in current thread


Figure 11:  Variations of the CodeView Select command.

      Select Thread Command

Command     Current Thread Set to

~nS         Thread n

~.S         Current thread

~#S         Last executed thread

████████████████████████████████████████████████████████████████████████████

Exchanging Data Between Applications Using the Windows Clipboard

───────────────────────────────────────────────────────────────────────────
Also see the related article:
  Encapsulated Postscript
───────────────────────────────────────────────────────────────────────────

Kevin P. Welch and David E. West☼

Transferring data between applications has long been a problem for
computer users. Even novices want to move information from one program to
another. In the early days of personal computers the only effective way
to do this was to transfer the information manually. The next level of
power and usability arrived when file standards such as Comma Separated
Variable (CSV) and Data Interchange Format (DIF) were developed,
facilitating the information transfer through intermediate data files.
Later, integrated packages became available in which the most commonly
used applications were encapsulated into one monolithic program. Although
this reduced the need for data exchange between programs, the central
issue of how to transfer data still remained.

Continuing the movement toward greater power and usability, Microsoft(R)
Windows expanded on the concepts pioneered by the Macintosh(R) and offered
developers and users a well-connected desktop environment within which
applications could naturally cooperate. Within Windows two methods
rapidly emerged as the de facto standard mechanisms for interprogram
communication and data exchange──the clipboard and dynamic data exchange
(DDE).

Of these two, the clipboard is used much more often. With the clipboard a
user can effortlessly select a range of cells in a spreadsheet, copy them
to the clipboard, and subsequently paste them into a document being edited
with a word processor. Despite the possibility that the two programs may
have been developed by competitive vendors, they communicate
effortlessly and appear to the user as one well-integrated desktop.

The clipboard is by nature a shared system resource. In order to function
correctly it is vitally important that each application interact with the
clipboard properly to prevent system crashes, endless message loops, and
other strange and disconcerting events from occurring. This article will
explain from a development perspective how to properly use the clipboard
and prevent some of the more common mistakes. The techniques and insights
that are provided here were developed and utilized by the authors during
the creation of T/Maker Company's ClickArt(R) Scrapbook+, a powerful
system utility that provides many enhancements to the Windows clipboard.


Clipboard Fundamentals

From the user's perspective the clipboard is a temporary data storage area
accessible through an application's Edit pull-down menu. The following
commands, as defined in the Windows Style Guide, enable the user to work
with the clipboard:

  Cut        copy selection to the clipboard and delete
  Copy       copy selection to the clipboard
  Paste      paste clipboard data into application

Unfortunately things are a little more complicated for the software
developer. Besides the Cut, Copy, and Paste operations the developer must
contend with such issues as clipboard ownership, multiple rendering,
delayed rendering, and the clipboard viewer chain.

One clipboard fundamental is clipboard ownership. Because the clipboard is
a shared system resource, Windows lets applications temporarily block
other programs from using it while changes are made. After completing the
changes the clipboard is immediately released so that other
applications can access it.

Conceptually, from a user's perspective, the clipboard appears to contain
only one data item at a time. In reality though it is capable of
maintaining several data items simultaneously, each supposedly
representing the same object but in a different format. This concept of
multiple rendering enables applications such as Microsoft Excel to supply
the clipboard with up to thirteen different formats or representations
of the same information when a copy operation is performed. The pasting
application can then choose the most desirable format from those
available on the clipboard. Figure 1☼ shows an example of multiply
rendered data.

When the clipboard contains multiply rendered data it is left up to the
pasting application to decide which format best suits the needs of the
user. In most cases astute applications place data on the clipboard in
priority order, starting with the format of greatest information density.
The order of enumeration is usually random, however, and should not be
relied on. The pasting application can then enumerate these formats and
obtain alternative representations in priority order as determined by
the originator.

As you can imagine, maintaining multiply rendered clipboard data can
consume prodigious amounts of precious system memory. Fortunately another
concept known as delayed rendering attempts to limit the allocation of
memory to only those formats that are actually used. Delayed rendering
in effect allows the copying application to place a "promise" on the
clipboard for each potential rendering. These promises remain on the
clipboard until some application requests a particular data item. When
this request is received, the copying application must fulfill the promise
by supplying the data in the format requested, even if it was copied to
the clipboard some time before. The end result is that data is allocated
for only those formats that are actually used, thus consuming less system
memory and eliminating the computation necessary to produce unwanted
formats. Delayed rendering will also save processing time for simply
rendered items.

In addition to the multiple and delayed rendering concepts, the Windows
clipboard provides a mechanism whereby interested applications can be
automatically notified whenever clipboard changes occur. This
facility, which is called the clipboard viewer chain and is shown in
Figure 2, causes the system to send a WM_DRAWCLIPBOARD message  to all
participating windows. Receipt of this message indicates that a clipboard
change has occurred that might be of interest to the application. After
processing this message the application is also given the responsibility
of passing this message to the next application in the chain.


Clipboard API

Applications that wish to use the clipboard can do so by calling
functions provided by the Windows Application Programming Interface
(API) and handling certain related messages. Figure 3, extracted in part
from the Windows Software Development Kit (SDK), briefly describes the
available functions. Besides these functions, the clipboard-related
messages listed in Figure 4 are supported by Windows.


Clipboard Data Formats

Windows predefines seven different clipboard formats, ranging from
standard ASCII text to graphical data in binary form. Besides these
predefined formats, Windows offers a mechanism with which applications
can create additional user-defined formats. Among these user-defined
extensions are six that are commonly supported by most major
applications. Figure 5 briefly describes each of these formats. Note
that those formats whose names begin with the CF_ prefix refer to
predefined clipboard formats.

User-defined clipboard formats are registered with Windows by calling
RegisterClipboardFormat with a unique character string. Each application
that uses this format is expected to reregister this format and in doing
so obtain the same format handle. Once defined, other applications can
retrieve this registered name by calling the GetClipboardFormatName
function with the format handle as a parameter.

Note that calling GetClipboardFormatName with a predefined clipboard
format will return a NULL string. Also, when using a CF_OWNERDISPLAY
object, the actual name of the format can be retrieved by sending the
clipboard owner a WM_ASKCBFORMATNAME message.

Of the commonly used clipboard formats, all but CF_BITMAP and
Printer_Bitmap are represented by handles to global blocks of system
memory. Memory for these formats is allocated by using GlobalAlloc with
the GMEM_MOVEABLE flag as a parameter. When the resulting handle is
transferred to the clipboard (via a SetClipboardData call), the system
changes the memory attributes to GMEM_DDESHARE with a GlobalReAlloc and
assigns ownership of the block to Windows. This enables the memory to be
retained after the copying application is terminated, even though in
normal circumstances memory allocated by an application is marked for
deletion when that application terminates. Note that once an
application has placed an object on the clipboard it becomes the
property of the system and the application should not continue to use
the data.

Now let's look at clipboard formats and analyze their potential uses and
abuses.

CF_BITMAP. This format is used for the transfer of GDI bitmap images.
Bitmaps can be created by using CreateBitmap, CreateBitmapIndirect, or
CreateCompatibleBitmap. The resulting handle can be placed on the
clipboard with SetClipboardData without any further modification. Note
that the CreateDiscardableBitmap function should not be used when creating
bitmaps for transfer through the clipboard.

Due in part to GDI, the CF_BITMAP format comes in two flavors. The first
of these is a conventional monochrome bitmap. The byte arrangement of this
format is fixed, supporting the direct manipulation of the bitmap data.
In addition, this standardized byte arrangement permits monochrome bitmaps
to be transported from one machine to another, albeit subject to
display/aspect ratio dependencies. The second flavor is the color bitmap
which, unfortunately, is strictly device-dependent in Windows, and the
portability of such an object between systems is not assured. Furthermore,
color bitmaps must be considered to be in an unknown byte arrangement,
precluding the use of any direct manipulation of the data.

CF_DIF. Software Arts' DIF format consists of ASCII text whose lines are
separated by a single carriage return-line feed (CR-LF) character pair.
Unlike CF_TEXT, the DIF data block is not null-terminated as the end is
encoded within the data. The simplest method to determine the extent of
the data without parsing the information is to use GlobalSize and retrieve
the block size. With this method a single function can be written to
handle several different text-related clipboard formats.

CF_METAFILEPICT. This format refers to a block of global memory containing
a METAFILEPICT structure. This structure contains four fields: mm, the
mapping mode to be used when displaying the metafile; xExt and yExt, from
which the requested size of the image drawn by the metafile can be
calculated (see the Windows SDK Programmer's Reference manual for more
information); and hMF, a handle to the actual metafile. To make a copy of
this data, use GlobalAlloc to allocate memory for the METAFILEPICT
structure, then use CopyMetafile to make a copy of the metafile referred
to by the hMF field.

CF_OWNERDISPLAY. The CF_OWNERDISPLAY clipboard format is designed to
enable applications to maintain their own display representation of
internal data. In order to display such information, the clipboard
viewer is limited to sending messages to the supplying application
whenever the viewport needs repainting.

This unusual painting method is how the clipboard viewer application
(CLIPBRD.EXE) displays Write-formatted text. When using this format the
clipboard owner receives a series of messages that correspond to those
normally encountered by the clipboard viewer. The owner is thus
responsible for performing the appropriate action whenever a paint,
size, or scroll operation is performed.

One annoying feature of the CF_OWNERDISPLAY format is the way in which the
client area of the clipboard viewer is handled. In the case of Windows
Write it automatically changes the style of the viewport to include an
additional set of horizontal and vertical scroll bars, regardless of the
fact that they may be already present as child windows. Furthermore, the
CF_OWNERDISPLAY data provided by most applications is typically in some
proprietary internal format and is of little use to other applications.

CF_SYLK. This clipboard format, a memory representation of the SYLK
file format, is similar to the CF_DIF format in that each line of ASCII
text is terminated by a CR-LF pair. As is the case with CF_DIF, the data
block is not null-terminated; hence GlobalSize should be used to
determine the actual block size, subject to the usual 16-byte paragraph
limitations.

CF_TEXT. The CF_TEXT format is one of the simplest supported by the
clipboard. Each line of text is terminated with a CR-LF pair and the
entire text block with a null character. Any application supporting the
transfer of textual data via the clipboard should consider using this
format in addition to any others, such as Rich Text, that provide for the
exchange of character-, paragraph-, and document-formatting
information.

CF_TIFF. The CF_TIFF format is used for transferring monochrome, gray-
scale, or color images between cooperating applications. Although
designed for use with very large images, this format is rapidly gaining
acceptance for images of all sizes as it represents an exact memory image
of the original Microsoft/Aldus file specification. Due to memory
limitations, applications that support this format should also be able
to import TIFF files directly from disk.

In recent months a number of software developers have been proposing
extensions to the CF_TIFF clipboard specification to include the
definition of a file-assisted mechanism for the clipboard transfer of
large images. However, it is unlikely that this proposal will be
supported by Windows as it would require modifications to the internal
clipboard manager to handle the associated temporary files shared between
applications. Refer to the Windows SDK Extensions Manual for additional
information on the CF_TIFF file and clipboard specification.

BIFF. This user-defined clipboard format is currently supported by a
number of applications, the most notable of which is Microsoft Excel.
BIFF (for Binary File Format) is the file format in which Excel documents
are saved on disk. A BIFF file is a complete self-contained package
consisting of a sequence of variable-length records. For example, one
record type describes the size and location of a window into a document,
another describes a formula entered into a cell, while another describes a
picture format.

Although individual BIFF record types contain different information, each
record follows the same format: record type, record length, and record
data (variable-length).

In the future the BIFF clipboard and file format will probably become a
standard much as TIFF has. Several independent software developers are
already supporting this format for the interapplication transfer of binary
financial data. Contact Microsoft for more information about this file and
clipboard format.

CSV. Software developers have long been familiar with the CSV file format;
it is perhaps the least common denominator of data exchange formats
(others may argue that TEXT is). Pioneered on the clipboard by Microsoft
Excel, this user-defined format is treated in Windows exactly like
CF_TEXT; each ASCII text line is separated by CR-LF pairs with the entire
block terminated by a single  null character.

Within each line of text every data item is comma separated with text
strings enclosed between quotation marks. Due in part to its simplicity,
CSV is widely used, and any application that utilizes the kind of data
suited to this format should support this standard.

PostScript(R). Encapsulated PostScript (EPS) is an emerging standard for the
exchange of text and graphics between cooperating applications. The
PostScript user-defined format was developed by its authors for the
transfer of EPS images via the clipboard. Currently supported by
Scrapbook+ and several other Windows applications, the PostScript text is
usually accompanied by one or more display representations, typically a
CF_TIFF or CF_METAFILEPICT image.

The PostScript format is conceptually identical to CF_TEXT, except that
each text line is terminated with a single CR character and the entire
block by a null character. Refer to the accompanying sidebar,
"Encapsulated PostScript," for more information on the transfer of EPS
images via the clipboard.

Printer_Bitmap. This user-defined format is identical to CF_BITMAP except
that it is limited to the output characteristics of the currently
selected printer. For example, using this format a color bitmap would be
rendered in monochrome when a laser printer was defined as the default
output device. The use of this format enables the copying application to
perform any necessary conversions that might be difficult for other
programs.

Printer_Picture. The Printer_Picture format, like Printer_Bitmap, is
identical to CF_METAFILEPICT but is limited to the output
characteristics of the currently selected printer. The application
supplying this format is responsible for performing any and all
necessary conversions for the default output device. Although simple in
theory, the definition of this format can be difficult when it involves
the mapping of fonts, colors, and other GDI objects to a specific output
device (not unlike printing).

Rich Text. The Rich Text Format (RTF), a relative newcomer to Windows,
was developed by Microsoft as a method of encoding formatted text and
graphics for easy transfer between applications. Using RTF, text and
graphics can be exchanged between different applications, environments, or
even operating systems.

An RTF object consists of unformatted 7-bit ASCII text that embeds
specially formatted commands in the document. These control words are used
to define printer commands and provide document management information.


Copying to the Clipboard

The general method for copying data to the clipboard is to allocate global
memory, format memory with desired data, and transfer the memory handle to
the clipboard. There are two models for copying data to the clipboard, the
replacement model and the supplemental model. The replacement model
requires that the clipboard be emptied before copying data to it,
ensuring that only the data from that particular copy operation is on the
clipboard. The supplemental model requires that when copying data to the
clipboard, the clipboard should not be emptied; rather, only data of the
same format as the data you are copying should be replaced, leaving all
other formats intact. In almost all circumstances the replacement model
is preferable because it takes up less memory and is much simpler
conceptually. There are cases, however, where the supplemental model is
appropriate; one such case will be discussed below.

The code fragments shown below and in Figures 6, 7, 8, and 9 demonstrate
how to copy multiply rendered data to the clipboard by using both normal
and delayed rendering schemes. Note that the copy routine uses the
replacement clipboard model, calling EmptyClipboard before calling
SetClipboardData. The replacement model must be employed when
providing data with a delayed rendering scheme because Windows sends
the WM_RENDERFORMAT message to the clipboard owner whenever a
particular format is requested. The supplemental model, not using an
EmptyClipboard call, does not transfer clipboard ownership. If it were
used in such a situation the format-rendering messages requesting the data
might never be received.

Both the copy and paste code fragments use the following data structure
and definitions:

  /* clipboard format data table */
  typedef struct {
     WORD      wFormat;
     HANDLE    hGlobalData;
  }  CLIPBOARD;

  /* global application data */
  WORD         wFormats;
  CLIPBOARD    Clipboard[6];

The code fragment shown in Figure 6 defines three alternative global data
blocks for use in a multiply rendered copy operation to the clipboard.
Alternatively, if a delayed rendering scheme is desired, the data
formatting operations can be postponed until a request is made for the
information, as is shown in Figure 7.

Once the data has been defined the copy operation can be performed by
using the code fragment shown in Figure 8. Note that Windows
automatically reallocates the memory blocks supplied, changing the
memory options used to include the GMEM_DDESHARE flag.

If a delayed rendering scheme is used, the promised data must be supplied
at some undetermined time in the future. Although the actual details of
creating the data can be rather complicated, the code that supplies this
request is simple. The main window function shown in Figure 9 demonstrates
how such format requests might be handled.


Pasting

Pasting data from the clipboard is usually a little easier than an
equivalent copy operation. The general method for pasting data from the
clipboard is as follows:

  ■  open the clipboard

  ■  determine if the clipboard does contain the desired rendering

  ■  obtain the handle to the object, the clipboard, or the GDI object
     handle

  ■  make a local copy of the data

  ■  close the clipboard

Using the data structure defined above, the code fragment in Figure 100
demonstrates how the paste operation would be accomplished using the
constructs established for the examples in Figures 6, 7, 8, and 9.

In Figure 10 EnumClipboardFormats is repeatedly called until all
available clipboard formats are enumerated. Through the use of an
alternative technique, IsClipboardFormatAvailable could also be
repeatedly called with the desired format as a parameter. This function
returns TRUE when the requested format is available and FALSE otherwise.
Note that the clipboard does not have to be open to use this function.

Another item to consider is the manner in which the handle returned by
GetClipboardData is treated. Due to the shared nature of clipboard data,
this handle is the property of Windows and the data it represents must
not be modified or released. Furthermore, this handle becomes invalid
once CloseClipboard is called.


Clipboard Viewer Chain

As mentioned previously, Windows offers a mechanism to inform interested
applications whenever changes to the clipboard occur. This linked list of
window handles, called the Clipboard Viewer Chain, is maintained in a
cooperative fashion by each of the participating applications.

An application enters this chain by calling the SetClipboardViewer
function with its window handle as a parameter. In response Windows
defines this window as the "current clipboard viewer" and returns a handle
to the previous viewer, if one exists. The participating application
must save this handle as it represents the only link to the remainder of
the chain. The chain is built in reverse with the most recent entry at the
head of the list. Figure 11 illustrates an addition to the viewer chain.

When changes occur to the clipboard, a WM_DRAWCLIPBOARD message is sent
by Windows to the current clipboard viewer. This window is responsible
for passing the message down the chain by using an explicit SendMessage
call and then performing any action deemed necessary. Failure to pass
this message on to the next window effectively destroys the viewer chain
and can sometimes result in abnormal behavior from those applications
further down the list. Note that the WM_DRAWCLIPBOARD message is generated
during the CloseClipboard call, following one or more calls to
SetClipboardData, and that control does not return until the entire
viewer chain has processed the message.

An application can remove itself from the viewer chain by calling the
ChangeClipboardChain function through the use of its handle and the
handle to the next application in the chain as parameters. If the window
in question is the current clipboard viewer, Windows simply resets the
current viewer to the next window in the list; this is analogous to
removing the first entry in a linked list. In all other cases a
WM_CHANGECBCHAIN message is sent to the current clipboard viewer for
subsequent transmission down the chain. When an application receives this
message it must check to see if the window being removed is the one
next in the viewer chain. If this is the case, the hNextWnd variable is
reset to reference the window following the one that is being removed.
Figure 12 demonstrates the removal of a window.

The code fragment in Figure 13 demonstrates these concepts within the
context of a traditional window function. In this example the window
joins the clipboard viewer chain at creation and removes itself when
destroyed.


Tips, Tricks, and Traps

Using the clipboard effectively can be a challenging exercise for any
software developer, especially when large or complicated formats are
involved. Clipboard support becomes even more difficult when several
large programs compete for limited system resources.

When using the clipboard a number of techniques can make the entire
process operate more efficiently. Here are some programming tips,
tricks, and traps to note.

Tip──Limit code segment size. When one or more large applications compete
for resources, system performance rapidly deteriorates; this is
especially true when working with the clipboard. This situation can be
improved by designing your applications to suit Windows' memory
management style.

In particular, dramatic improvements in performance can sometimes be
obtained by marking each code segment as movable and discardable while
limiting its size to 8Kb. When this is done, Windows can efficiently
rearrange memory when resources become scarce. This rearrangement is made
much easier when code segments are uniformly small in size.

Be aware however that the desired 8Kb segment size also applies to the
_TEXT code segment. This segment usually contains a large portion of the
C run-time library. In order to effectively reduce the size of this
segment you will have to purchase the run-time library source code and
recompile it into different segments depending on your needs.

Trick──Build in recovery techniques. Many of the code fragments
accompanying this article use less-than-ideal error recovery techniques.
When working with the clipboard, it is especially important to
incorporate several robust mechanisms that can be invoked when a
particular operation fails. In almost all cases you would be well advised
to incorporate them into your application early in the development cycle
before error recovery becomes an issue.

An example of the need for such an error recovery technique occurs when
data is copied to the clipboard. If the initial memory allocation and
formatting fails, then an alternative approach could be invoked that
releases some discardable memory and uses a less demanding, and perhaps
less efficient, method. The use of such techniques distinguishes great
applications from average ones.

Trap──Using fixed code or data segments. The use of fixed code or data
segments can impair Windows memory management. Programs that employ such
techniques run the risk of precluding the use of other well-behaved
applications. Although Windows attempts to locate fixed code or data
segments away from critical areas, situations exist where they can
cause serious degradation of system performance. This is especially
obvious when using the clipboard to exchange large contiguous data
items between applications.

Tip──supplied by Windows return a value when completed. In most
circumstances it is inadvisable to ignore these return values, as unusual
conditions sometimes occur when functions fail. Furthermore, many Windows
functions have numerous side effects that can lead to unforseen results.
In addition, it is likely that the API code might itself change with a new
version of Windows, resulting in new and unanticipated situations.

Trick──Batch up error messages. When using the clipboard, it is possible to
consume all available system memory. In such a situation almost any
complicated operation will fail, possibly resulting in a rapid-fire
sequence of annoying message boxes.

Wherever possible, it is desirable to batch up these messages and
concatenate them into a single error. The typical user is often
uninterested in the exact cause of the failure, instead being more
concerned with the current state of the application and what he or she can
do to make it work.

Trap──Inaccessible error recovery code. One easily encountered trap occurs
when an out-of-memory condition is reached that requires the use of
complex error recovery routines. If these routines, and their associated
resources, need even small amounts of extra memory they too are likely to
fail. The end result can be as benign as a garbled error message or as
disastrous as a system crash. Generally speaking, it is advisable to
keep error recovery code simple, keep it in memory whenever possible,
and make it independent of discardable system resources.

Tip──Avoid long periods of dead air. Applications that work with the
clipboard sometimes require long periods when intense computation is
being performed. An example of this would be when a large compressed
gray-scale TIFF image is displayed on a monochrome display. In this case
the application must uncompress the image in bands while converting the
gray-scale to monochrome. As one would imagine, this process can consume
a considerable amount of system resources and take several seconds.

In such situations it is best to use one of the following techniques:

  small jobs    change the mouse pointer to an hourglass
  medium jobs   display a modal dialog box with a cancel button
  large jobs    display a modeless dialog box with a cancel button

Trap──Null rendering failure. In order for the delayed rendering process
to be transparent to other applications, the clipboard owner must handle
all interactions correctly. When the clipboard owner receives a
WM_RENDERFORMAT or WM_RENDERALLFORMATS message, it should attempt to
render the requested data. If it cannot render the data because of
insufficient memory or some other condition, the clipboard owner should
not call the SetClipboardData function with a NULL handle. Doing so will
cause a spurious WM_DRAWCLIPBOARD message to be sent down the viewer
chain as Windows will assume that the clipboard has changed yet again,
while in actuality the data will still be null rendered. If the data
cannot be rendered, doing nothing is advisable. The next time the null-
rendered data is requested, Windows will send another WM_RENDERFORMAT
message. The request may succeed this time as the exact memory
configuration may be different from the time of the  last request.

Trap──Using delayed rendering when not ready. When CloseClipboard is
called after copying data to the clipboard, a WM_DRAWCLIPBOARD message is
immediately sent down the viewer chain. If one of the clipboard viewers
immediately requests the data, a WM_RENDERFORMAT message will be sent to
the current clipboard owner (note the re-entrancy as the CloseClipboard
function has not yet completed).

This immediate response by one or more of the applications in the viewer
chain may occur before the copying application is ready to provide the
data. In this case an incorrect response is made to the WM_RENDERFORMAT
message, leaving the clipboard in an undetermined state and causing a
number of other problems. The way to avoid duplicating this problem is to
call CloseClipboard only after the data can be satisfactorily rendered.

Trap──Extra WM_DRAWCLIPBOARD messages. Another trap to be avoided occurs
when a member of the clipboard viewer chain requests null-rendered data.
When the application calls GetClipboardData, Windows sends a
WM_RENDERFORMAT message to the current clipboard owner. The clipboard
owner then formats the requested data and places it on the clipboard using
SetClipboardData.

Behind the scenes Windows sends an extra WM_DRAWCLIPBOARD message down
the viewer chain in response to the SetClipboardData call. The pasting
application receives this spurious message because it is also a member of
the chain and can be tricked into thinking that the clipboard contents
have changed yet again. The only way of preventing this is maintaining a
semaphore that separates meaningful WM_DRAWCLIPBOARD messages from
spurious ones.


Conclusion

Although incorporating good clipboard support into an application can be
challenging and time-consuming, the benefits are worth it. As more
software becomes available for Windows, products are increasingly
evaluated on their connectivity as well as their features.
Comprehensive clipboard support is a natural and logical way to provide
the kind of interconnectivity that users have come to expect.

Despite the complexity of this article a considerable amount of
information has not yet been discussed. The more complicated clipboard
formats each have their own specifications. Several good reference
materials are available that cover clipboard programming in great
detail. The best of these is Programming Windows by Charles Petzold
(Microsoft Press, 1988). With this and other reference materials even the
most difficult clipboard problems can be resolved with a little patience.


───────────────────────────────────────────────────────────────────────────
Encapsulated Postscript
───────────────────────────────────────────────────────────────────────────

Encapsulated PostScript (EPS) is rapidly becoming the standard means for
transferring graphics and text between dissimilar software applications
in diverse environments. The industrywide acceptance of this standard
has resulted in the integration of EPS support into many popular
programs.

EPS has been enthusiastically received in the Microsoft(R) Windows
environment for IBM-compatible personal computers. Windows has proven
itself to be a highly consistent and user-friendly environment that
facilitates the development of graphically oriented applications. EPS is
of special interest to such applications as it can serve as a device-
independent means for transporting graphical objects between
applications and environments.

Several applications now exist under Windows that support this standard,
and many more are in development. These applications are currently
limited to importing and exporting EPS files directly from disk, which
normally results from an explicit user command and is somewhat cumbersome
to use. Also, it further complicates the seamless, permissive
environment of Windows.

This situation would be greatly improved with the specification and
definition of a clipboard data format for the EPS standard. This would
enable PostScript images to be transferred through the clipboard, as well
as through Dynamic Data Exchange (DDE). Applications would then be able to
share text and graphics through the clipboard by using the same standard
mechanisms available for other predefined data formats.


Requirements

Any definition of a new clipboard data format for EPS must consider
several design issues and requirements, which serve to focus the
specification. These include, but are not limited to, the following
issues:

1. The specification should consider the multiple-rendering capabilities
   of the clipboard and offer as many standard representations of the image
   as possible for use by existing Windows applications. This would give
   such programs as Windows Write and Paint access to display and printer
   renderings of PostScript images.

2. The specification does not necessarily have to handle arbitrarily large
   and complex PostScript images. As is the case with the CF_TIFF clipboard
   format, large images might be best transferred with another mechanism.

3. The interapplication transfer of small EPS images, say, between 10Kb
   and 64Kb, should be performed as efficiently as possible, occurring
   completely in memory by using standard clipboard operations.

4. The future definition of a bitmap mask for any display renderings
   provided as part of this specification should be allowed for. This would
   enable the destination application to overlay nonrectangular display
   renderings over existing graphics and text.

5. There should be some separability between the PostScript text and the
   various display renderings included as part of the encapsulated
   standard. This would provide for possible future hardware platforms that
   would use Display Postscript, reducing the need for special screen
   representations.

Also, a preliminary analysis of EPS art available on IBM-compatible PCs
suggests that the images range in size from approximately 2Kb to in excess
of 3Mb. Most of the less complicated images are smaller than 64Kb and
are thus consistent with the design goals of this specification.


Specification Proposal

The following proposal for a specification describes the mechanisms and
standards used to transfer EPS images via the clipboard. This discussion
employs the existing EPS File (EPSF) definition and does not address any
future device-independent variations of this standard.

1. EPSF objects are a concatenation of standard PostScript text with one
   or more supplemental display renderings, such as a Windows Metafile, a
   TIFF image, or possibly a Macintosh PICT representation.

2. Each application supporting the clipboard transfer of EPSF images
   would be required to preregister a PostScript clipboard format. This
   would be accomplished by passing the following character string to the
   Windows RegisterClipboardFormat routine:

     Postscript

   Multiple registration of this format would result in the return of the
   same ID number with an internal reference count being incremented.
   Several cooperating applications could then share the same format
   numbers, although this number might vary from session to session.

3. An application transferring an EPSF image through the clipboard would
   supply a PostScript text with one or more display renderings. The donor
   application would be responsible for determining whether sufficient
   memory is available for the copy operation and which of the supplemental
   renderings to provide.

   The supplemental renderings would normally be simple clipboard copies of
   the display renderings provided with the PostScript text. In some
   situations the donor application may wish to generate additional
   representations from the display renderings. For example, a Windows
   Metafile rendering could be worked into a monochrome bitmap, enabling
   applications such as Windows Paint to gain access to a crude
   representation of the PostScript image. The donor application in this
   example would be responsible for determining the dimensions of this
   supplemental bitmap representation.

   Note that in most cases the donor application would provide the
   PostScript and display renderings on a delayed basis so as to avoid the
   production of unwanted formats and the consumption of precious memory.

4. The PostScript clipboard object would consist of standard PostScript
   text taken directly from the EPSF file. Applications wishing to display
   the PostScript text could do so by using standard ASCII text-handling
   mechanisms.

5. In certain situations it would be desirable to provide a shadow mask
   for any bitmap representation of the EPSF image. This would permit
   pasting applications to overlay the bitmap onto existing text  and
   graphics. Despite the appeal of this capability, we suggest that at the
   present time an additional format not be defined for this mask.

   Note that many CF_TIFF display renderings, including those created by
   Adobe Illustrator(TM), do contain a TIFF mask as part of the entire
   representation.

6. The copying application would be responsible for releasing any
   global memory associated with the PostScript text when the data is
   replaced by another entry, although this is usually the responsibility of
   Windows itself. The receipt of a WM_DESTROYCLIPBOARD message would
   indicate that this has occurred. The copying application, which in this
   scheme does not have to be part of the clipboard viewer chain, can then
   release the memory associated with the clipboard PostScript data object.


Implementation

This specification is just a proposal at this point. The definition
will probably be changed once all interested parties have commented on it.
The final acceptance and implementation of this standard can be achieved
in one of three ways, which are discussed here in descending order of
desirability.

First, Microsoft defines a new clipboard format, for example,
CF_POSTSCRIPT, and makes it part of a future Windows Software Development
Kit (SDK). This would require an updated version of WINDOWS.H and revised
documentation. Microsoft would then become the keeper of the
definition and be responsible for its support and possible future
extension. The Windows clipboard would then become responsible for
releasing the global memory consumed by the PostScript text, relieving the
donor application of this task and ensuring that unused resources are not
accumulated.

Second, the most likely scenario is that a representative group of
software developers, after reaching consensus on the specification, makes
the definition public and physically implements it in several
complementary products. Each of these applications would be
responsible for preregistering the PostScript clipboard format and
supporting it however necessary. In time the clipboard protocol for
handling this format would become an industry standard accepted by all
until something better displaced it. Under this approach the keeper of the
definition would be a committee of developers responsible for the
specification until it warranted inclusion in the Windows SDK.

The last and most undesirable means for establishing the standard is for
a small number of software developers to reach consensus on the
specification, make it part of their products, but not release it to the
general public. This might in the short run give these complementary
programs a slight market advantage until the standard was made public or
a new public one replaced it. However, from a historical perspective,
under such an approach the standard would probably be short-lived and
never reach the level of usage necessary to qualify it as an accepted
industry standard.


Future Possibilities

The development of a means for transferring EPS images through the Windows
clipboard opens up some exciting possibilities. The specification and
proposed definition of a standard means for accomplishing this task is an
attempt to solve the attendant problem and make it a reality. Many
applications currently in use and under development could use this
specification and further entrench PostScript as the page
description language of choice for Microsoft Windows.

This document does not represent the last word on the topic. Interested
developers are urged to respond as quickly as possible to this document
and offer specific recommendations on how to improve it. Please forward
any comments, suggestions, or criticisms to the following address:

Kevin P. Welch, President
Eikon Systems, Inc.
989 East Hillsdale Blvd.,
Suite 260
Foster City, CA 94404
(415) 349-4664


Figure 2:  Clipboard Viewer Chain

╔══╤═══════════════════════════════╤═══╗       current
╠══╧═══════════════════════════════╧═╤═╣   clipboard viewer
║ ██████████████████████████████████ ├─╢  ┌───────────────┐   ┌──────────────
║ █████████████████████████████████ ───────►  hNextWind  ──────►  hNextWind
║ ██████████████████████████████████ │ ║  │               │█  │
║ █████████ Windows Kernal █████████ │ ║  │ 3             │█  │ 2
║ ██████████████████████████████████ │ ║  └───────────────┘█  └────────────│─
║ ██████████████████████████████████ │ ║   ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀   ▀▀▀▀▀▀▀▀▀▀▀▀│▀
║ ██████████████████████████████████ ├─╢                      ┌────────────▼─
╟─┬────────────────────────────────┬─┼─╢                      │   hNextWind
╚═╧════════════════════════════════╧═╧═╝                      │
                                                              │ 1
                                                              └──────────────
                                                               ▀▀▀▀▀▀▀▀▀▀▀▀▀▀


Figure 3:  Available Functions for the Clipboard

╓┌─────────────────────────────┌─────────────────────────────────────────────╖
Function                    Description

ChangeClipboardChain        Removes a window from the clipboard viewer
                            chain.
Function                    Description
                            chain.

CloseClipboard              Closes the clipboard.

CountClipboardFormats       Returns the number of available formats on
                            clipboard. Clipboard need not be open.

EmptyClipboard              Empties the clipboard and reassigns clipboard
                            ownership. Clipboard must be open.

EnumClipboardFormats        Enumerates the available clipboard formats.
                            Clipboard must be open.

GetClipboardData            Retrieves data from the clipboard (in
                            requested format). Clipboard must be open.

GetClipboardFormatName      Retrieves the ASCII name of a
                            nonpredefined format.

GetClipboardOwner           Retrieves the window handle associated with
Function                    Description
GetClipboardOwner           Retrieves the window handle associated with
                            the current clipboard owner.

GetClipboardViewer          Retrieves the handle of the first window in
                            the clipboard viewer chain.

IsClipboardFormatAvailable  Returns TRUE if the data in the given
                            format is available on the clipboard.

OpenClipboard               Opens the clipboard.

RegisterClipboardFormat     Registers a new clipboard format.

SetClipboardData            Copies a data handle to the clipboard.
                            Clipboard must be open.

SetClipboardViewer          Adds a window handle to the clipboard viewer
                            chain.


Figure 4:  Clipboard-related Messages

╓┌────────────────────────┌──────────────────────────────────────────────────╖
Message                  Description

WM_ASKCBFORMATNAME☼      Requests the name of the
                         CF_OWNERDISPLAY format.

WM_CHANGECBCHAIN         Notifies viewer chain members of a
                         change in the chain.

WM_DESTROYCLIPBOARD      Signals the clipboard owner that the
                         clipboard contents are being destroyed.

WM_DRAWCLIPBOARD         Notifies an application in the viewer
                         chain of a change in the clipboard.

WM_HSCROLLCLIPBOARD☼     Requests horizontal scrolling for the
                         CF_OWNERDISPLAY format.

WM_PAINTCLIPBOARD☼       Requests painting of the
Message                  Description
WM_PAINTCLIPBOARD☼       Requests painting of the
                         CF_OWNERDISPLAY.

WM_RENDERALLFORMATS      Notifies the clipboard owner that it must render
                         all promised clipboard data.

WM_RENDERFORMAT          Notifies the clipboard owner to format
                         the data copied to the clipboard.

WM_SIZECLIPBOARD☼        Notifies the clipboard owner that the
                         viewer window size has changed.

WM_VSCROLLCLIPBOARD☼     Requests vertical scrolling for the
                         CF_OWNERDISPLAY format.


Figure 5:  Clipboard Data Formats

Format                Description

CF_BITMAP☼            Windows GDI bitmap
CF_DIF☼               Software Arts╒ Data Interchange Format
CF_METAFILEPICT☼      Windows metafile picture (METAFILEPICT)
CF_OWNERDISPLAY       Owner display format
CF_SYLK☼              Microsoft Symbolic Link format (spreadsheet data)
CF_TEXT☼              Conventional ASCII text
CF_TIFF☼              Tagged Image File Format
BIFF                  Binary Interchange File Format
CSV☼                  Comma Separated Variable
Postscript☼           PostScript text
Printer_Bitmap☼       Printer bitmap (first defined by Microsoft Excel)
Printer_Picture☼      Printer metafile picture (first defined by Microsoft
                      Excel)
Rich Text☼            Rich Text Format
[private formats]     Other user-defined formats


Figure 6:  Defining Global Data Blocks

/* local data */
LPSTR            lpMemory;
LPMETAFILEPICT   lpMetaFile;

/* define number of formats */

wFormats = 3;

/* define text data */
Clipboard[0].wFormat = CF_TEXT;
Clipboard[0].hGlobalData = GlobalAlloc( GHND, (DWORD)100 );
if ( Clipboard[0].hGlobalData ) {
   lpMemory = GlobalLock( Clipboard[0].hGlobalData );
   lstrcpy( lpMemory, "Some sample text for the clipboard." );
   GlobalUnlock( Clipboard[0].hGlobalData );
} else
<...warn user - insufficient memory for copy...>

/* define bitmap data */
Clipboard[1].wFormat = CF_BITMAP;
Clipboard[1].hGlobalData = CreateBitmap( 50, 50, 1, 1, NULL);
if ( Clipboard[1].hGlobalData ) {
<...define bitmap bits...>
} else
<...warn user - insufficient memory for copy...>

/* define metafile data */
Clipboard[2].wFormat = CF_METAFILEPICT;
Clipboard[2].hGlobalData = GlobalAlloc(GHND,(DWORD)sizeof(METAFILEPICT));
if ( Clipboard[2].hGlobalData ) {
   lpMetaFile = (LPMETAFILEPICT)GlobalLock(Clipboard[2].hGlobalData );
   lpMetaFile->mm = MM_ANISOTROPIC;
   lpMetaFile->xExt = 0;
   lpMetaFile->yExt = 0;
   lpMetaFile->hMF = <...define a metafile and initialize it...>
   GlobalUnlock( Clipboard[2].hGlobalData );
} else
<...warn user - insufficient memory for copy...>


Figure 7:  Data Format Definitions

/* define number of formats */
wFormats = 3;

/* define text data */
Clipboard[0].wFormat = CF_TEXT;
Clipboard[0].hGlobalData = NULL;

/* define bitmap data */
Clipboard[1].wFormat = CF_BITMAP;
Clipboard[1].hGlobalData = NULL;

/* define metafile data */
Clipboard[2].wFormat = CF_METAFILEPICT;
Clipboard[2].hGlobalData = NULL;


Figure 8:  Code Fragment for the Clipboard Copy Operation

/* local variables */
WORD   i;
if ( OpenClipboard(hWnd) ) {
   EmptyClipboard();
   for ( i=0; i<wFormats; i++ )
      SetClipboardData( Clipboard[i].wFormat, Clipboard[i].hGlobalData );
} else
<...warn user - unable to open clipboard...>


Figure 9

LONG FAR PASCAL WndProc(
   HWND        hWnd,
   WORD        wMessage,
   WORD        wParam,
   LONG        lParam)
{
   /* local variables */
   WORD             i;
   LPSTR            lpMemory;
   LPMETAFILEPICT   lpMetaFile;

   /* process messages */
   switch(wMessage)
   {
   case WM_RENDERFORMAT : /* render a single data item */
      /* search for entry in table - wParam = requested format */
      for (i=0; (i<wFormats)&&(Clipboard[i].wFormat!=wParam);i++);
      if (i < wFormats) {
         /* define data */
         switch(wParam)
         {
         case CF_TEXT : /* request for text data */
            Clipboard[i].hGlobalData=GlobalAlloc(GHND, (DWORD)100);
            if (Clipboard[i].hGlobalData) {
               lpMemory = GlobalLock(Clipboard[i].hGlobalData );
               lstrcpy(lpMemory, "Some sample text for the clipboard." );
               GlobalUnlock(Clipboard[i].hGlobalData );
            } else
                 <...warn user - insufficient memory for copy...>
            break;
         case CF_BITMAP : /* request for bitmap data */
            Clipboard[i].hGlobalData = CreateBitmap(50, 50, 1, 1, NULL );
            if (Clipboard[i].hGlobalData) {
               <...define bitmap bits...>
            } else
                 <...warn user - insufficient memory for copy...>
            break;
         case CF_METAFILEPICT : / *request for metafile data */
            Clipboard[i].hGlobalData = GlobalAlloc(GHND,
               (DWORD)sizeof(METAFILEPICT) );
            if (Clipboard[i].hGlobalData) {
               lpMetaFile =
               (LPMETAFILEPICT)GlobalLock(Clipboard[i].hGlobalData );
               lpMetaFile->mm = MM_ANISOTROPIC;
               lpMetaFile->xExt = 0;
               lpMetaFile->yExt = 0;
               lpMetaFile->hMF = <...define a metafile and initialize
                    it..>
               GlobalUnlock(Clipboard[i].hGlobalData );
            } else
               <...warn user - insufficient memory for copy...>
            break;
         }
         /* copy data to the clipboard */
         SetClipboardData(wParam,Clipboard[i].hGlobalData);
      } else
           <...warn user - unfulfilled request...>
      break;
   case WM_RENDERALLFORMATS:/*render all promised formats */
      /* open the clipboard */
      if (OpenClipboard(hWnd)) {
         for (i=0; i<wFormats; i++)
            if (Clipboard[i].hGlobalData == NULL) {
               /* define data */
               switch(Clipboard[i].wFormat)
               {
               case CF_TEXT : /* request for text data */
                  Clipboard[i].hGlobalData = GlobalAlloc(GHND,(DWORD)100);
                  if (Clipboard[i].hGlobalData)
                  {
                     lpMemory = GlobalLock( Clipboard[i].hGlobalData );
                     lstrcpy(lpMemory, "Some sample text for the
                         clipboard." );
                     GlobalUnlock(Clipboard[i].hGlobalData );
                  } else
                     <...warn user - insufficient memory for copy...>
                  break;
               case CF_BITMAP : /*request for bitmap data*/
                  Clipboard[i].hGlobalData = CreateBitmap(50, 50, 1, 1,
                     NULL);
                  if (Clipboard[i].hGlobalData) {
                     <...define bitmap bits...>
                  } else
                     <...warn user - insufficient memory for copy...>
                  break;
               case CF_METAFILEPICT : /* request for metafile data */
                  Clipboard[i].hGlobalData =
                    GlobalAlloc(GHND,(DWORD)sizeof(METAFILEPICT));
                  if (Clipboard[i].hGlobalData) {
                     lpMetaFile =(LPMETAFILEPICT)GlobalLock(
                        Clipboard[i].hGlobalData);
                     lpMetaFile->mm = MM_ANISOTROPIC;
                     lpMetaFile->xExt = 0;
                     lpMetaFile->yExt = 0;
                     lpMetaFile->hMF = <...define a metafile and
                         initialize it...>
                     GlobalUnlock(Clipboard[i].hGlobalData);
                  } else
                     <...warn user - insufficient memory for copy...>
                  break;
               }
               /* copy data to the clipboard */
               SetClipboardData(Clipboard[i].wFormat,
                  Clipboard[i].hGlobalData );
            }
      } else
         <...warn user - unable to open clipboard...>
      break;
   default : /* some other message - pass through */
      return(DefWindowProc(hWnd, wMessage, wParam, lParam));
      break;
   }
   /* normal return */
   return(0L );

}


Figure 10:  Code Fragment of a Typical Paste Operation

/* local variables */
WORD        wCrntFormat;

if ( OpenClipboard(hWnd) ) {

   /* initialization */
   wFormats = 0;
   wCrntFormat = EnumClipboardFormats(0);
   /* enumerate available formats */
   while ( wCrntFormat ) {
      /* paste format if desired */
      switch( wCrntFormat )
         {
      case CF_TEXT : /* text available - assume < 64kb */
         {
            LPSTR       lpSrce;
            LPSTR       lpDest;
            HANDLE      hSrceText;
            HANDLE      hDestText;

            hSrceText = GetClipboardData( CF_TEXT );
            if ( hSrceText ) {
               hDestText = GlobalAlloc( GHND, GlobalSize(hSrceText) );
               if ( hDestText ) {

                  /* duplicate text block */
                  lpSrce = GlobalLock( hSrceText );
                  lpDest = GlobalLock( hDestText );
                  lstrcpy( lpDest, lpSrce );
                  GlobalUnlock( hDestText );
                  GlobalUnlock( hSrceText );

                  /* add block to format list */
                  Clipboard[wFormats].wFormat = CF_TEXT;
                  Clipboard[wFormats++].hGlobalData = hDestText;

               } else
                <...warn user - insufficient memory for paste...>
            } else
                <...warn user - unable to paste text...>
         }
         break;
      case CF_BITMAP : /* bitmap available */
         {
            HDC      hDC;
            HDC      hSrceDC;
            HDC      hDestDC;
            HBITMAP  hSrceBitmap;
            HBITMAP  hDestBitmap;
            HBITMAP  hOldSrceBitmap;
            HBITMAP  hOldDestBitmap;

            hSrceBitmap = GetClipboardData( CF_BITMAP );
            if ( hSrceBitmap && GetObject(hSrceBitmap,
               sizeof(BITMAP),(LPSTR)&Bitmap) ) {

               /* define two compatible memory DCs */
               hDC = GetDC( hWnd );
               hSrceDC = CreateCompatibleDC( hDC );
               hDestDC = CreateCompatibleDC( hDC );
               ReleaseDC( hWnd, hDC );

               if ( hSrceDC && hDestDC ) {
                  hDestBitmap = CreateBitmapIndirect( &Bitmap );
                  if ( hDestBitmap ) {
                     /* duplicate bitmap */
                     hOldSrceBitmap = SelectObject( hSrceDC, hSrceBitmap);
                     hOldDestBitmap = SelectObject( hDestDC, hDestBitmap);
                     BitBlt(hDestDC,0,0,Bitmap.bmWidth,
                         Bitmap.bmHeight,hSrceDC,0,0,SRCCOPY);
                     SelectObject(hDestDC, hOldDestBitmap );
                     SelectObject(hSrceDC, hOldSrceBitmap );
                     /* add bitmap to data list */
                     Clipboard[wFormats].wFormat = CF_BITMAP;
                     Clipboard[wFormats++].hGlobalData = hDestBitmap;
                  } else
                    <...warn user - insufficient memory for paste...>

               } else
                   <...warn user - unable to paste bitmap...>

               /* delete DCs */
               if ( hDestDC )
                  DeleteDC( hDestDC );
               if ( hSrceDC )
                  DeleteDC( hSrceDC );

            } else
              <...warn user - unable to paste bitmap...>

         }
         break;
      case CF_METAFILEPICT : /* metafile available */
         {
            HANDLE            hSrceMF;
            HANDLE            hDestMF;
            LPMETAFILEPICT    lpDestMF;
            LPMETAFILEPICT    lpSrceMF;

            hSrceMF = GetClipboardData( CF_METAFILEPICT );
            if ( hSrceMF ) {
               hDestMF = GlobalAlloc( GHND, (DWORD)sizeof(METAFILEPICT) );
               if ( hDestMF ) {

                  /* duplicate metafile */
                  lpSrceMF = (LPMETAFILEPICT)GlobalLock( hSrceMF );
                  lpDestMF = (LPMETAFILEPICT)GlobalLock( hDestMF );
                  lpDestMF->mm = lpSrceMF->mm;
                  lpDestMF->xExt = lpSrceMF->xExt;
                  lpDestMF->yExt = lpSrceMF->yExt;
                  lpDestMF->hMF  =CopyMetaFile(lpSrceMF->hMF,(LPSTR)NULL);
                  /* add block to format list */
                  if ( lpDestMF->hMF ) {
                     GlobalUnlock( hDestMF );
                     Clipboard[wFormats].wFormat = CF_METAFILEPICT;
                     Clipboard[wFormats++].hGlobalData = hDestMF;
                  } else {
                     GlobalUnlock( hDestMF );
                     GlobalFree( hDestMF );
                 <...warn user - unable to duplicate metafile...>
                  }

                  GlobalUnlock( hSrceMF );

               } else
                <...warn user - insufficient memory for paste...>
            } else
             <...warn user - unable to paste metafile...>

         }
         break;
      default : /* something else - ignore */
         break;
      }
      /* retrieve next format */
      wCrntFormat = EnumClipboardFormats( wCrntFormat );
   }

   /* cleanup */
   CloseClipboard();

} else
  <...warn user - unable to open clipboard...>


Figure 13

This code fragment represents a way of both joining a window to the
clipboard chain and removing it when the window is destroyed.


LONG FAR PASCAL ViewerWndProc(
      HWND       hWnd,
      WORD       wMessage,
      WORD       wParam,
      LONG       lParam
   )
{
   /* global variables */
   static HWND   hNextWnd;
   /* message processing */
   switch( wMessage )
   {
   case WM_CREATE : /* window being created */
      hNextWnd = SetClipboardViewer( hWnd );
      break;
   case WM_DRAWCLIPBOARD : /* clipboard contents changing */
      if ( hNextWnd )
         SendMessage( hNextWnd, wMessage, wParam, lParam );
          <...do your own processing here...>
      break;
   case WM_CHANGECBCHAIN : /* viewer chain changing */
      if ( hNextWnd == (HWND)wParam )
         hNextWnd = LOWORD(lParam);
      else
         SendMessage( hNextWnd, wMessage, wParam, lParam );
      break;
   case WM_DESTROY : /* window being destroyed */
      ChangeClipboardChain( hWnd, hNextWnd );
      PostQuitMessage( 0 );
      break;
   default :
      return( DefWindowProc(hWnd,wMessage,wParam,lParam) );
      break;
   }
   return( 0L );
}


Figure 11:  Addition to the Viewer Chain

╔══╤═══════════════════════════════╤═══╗
╠══╧═══════════════════════════════╧═╤═╣
║ ██████████████████████████████████ ├─╢  ┌───────────────┐   ┌──────────────
║ █████████████████████████████████ ───────►  hNextWind  ──────►  hNextWind
║ ██████████████████████████████████ │ ║  │               │█  │
║ █████████ Windows Kernal █████████ │ ║  │ 3             │█  │ 2
║ ██████████████████████████████████ │ ║  └──────────────┘█  └────────────│─
║ ██████████████████████████████████ │ ║   ▀▀▀▀▀▀▀│▀▀▀▀▀▀▀▀▀   ▀▀▀▀▀▀▀▀▀▀▀▀│▀
║ ███████████████████████████████│██ ├─╢                      ┌────────────▼─
╟─┬────────────────────────────────┬─┼─╢          │           │   hNextWind
╚═╧══════════════════════════════│═╧═╧═╝                      │
                                   ┌──────────────│┐          │ 1
                                 └ ─►  hNextWind   │█         └──────────────
                                   │               │█          ▀▀▀▀▀▀▀▀▀▀▀▀▀▀
                                   │ 4             │█
                                   └───────────────┘█
                                    ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
                                       new current
                                     clipboard viewer


Figure 12:  Removal from the Viewer Chain

╔══╤═══════════════════════════════╤═══╗       current             window
╠══╧═══════════════════════════════╧═╤═╣   clipboard viewer     being removed
║ ██████████████████████████████████ ├─╢  ┌───────────────┐   ┌──────────────
║ █████████████████████████████████ ───────►  hNextWind  ──────►  hNextWind
║ ██████████████████████████████████ │ ║  │               │█  │
║ █████████ Windows Kernal █████████ │ ║  │ 4             │█  │ 3
║ ██████████████████████████████████ │ ║  └────────────│──┘█  └────────────│─
║ ██████████████████████████████████ │ ║   ▀▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀   ▒▒▒▒▒▒▒▒▒▒▒▒│▒
║ ██████████████████████████████████ ├─╢               │                   │
╟─┬────────────────────────────────┬─┼─╢       New link       ┌────────────▼─
╚═╧════════════════════════════════╧═╧═╝       in chain└ ─ ─ ─ ►  hNextWind
                                                              │
                                                              │ 2
                                                              └────────────│─
                                                               ▀▀▀▀▀▀▀▀▀▀▀▀│▀
                                                              ┌────────────▼─
                                                              │   hNextWind
                                                              │
                                                              │ 1
                                                              └──────────────
                                                               ▀▀▀▀▀▀▀▀▀▀▀▀▀▀

████████████████████████████████████████████████████████████████████████████

Using Microsoft C Version 5.1 to Write Terminate-and-Stay-Resident
Programs

By Kaare Christian☼

TSRs have their own mystique among PC software programs. This is in part
due to the fact that many of them pop up, as if by magic, at the press of
a button and then vanish when no longer needed. However, the TSR mystique
comes more from the macho philosophy behind them. TSR programmers have a
"damn the operating system, I'll code it my way" attitude, and they take
this attitude to the limit.

TSR stands for terminate-and-stay resident. TSRs are programs that extend
the services provided by DOS. Some TSRs work silently; they provide a
useful service, but they don't need to interact directly with the user.
Other TSRs are more interactive; when they sense that their hot key has
been struck, they pop up onto the screen, manage a user dialogue, perform
some service, and then vanish, restoring the machine to its original
state. The most famous example of a TSR program is Borland
International's SideKick(R).

The first commercially successful TSR was RoseSoft's ProKey(TM), which was
introduced in 1982, but it was SideKick that really started the TSR mania.
Even today it remains one of the most important DOS software programs.
What the software engineers behind these programs noticed, before most of
the rest of us, is that DOS has a mechanism for growth.

By executing Interrupt 27H (or DOS system call 31H) a program can exit to
DOS but keep its memory image in core. (Normally, of course, when a
program exits, DOS reclaims its memory, so that it can be used by the next
application.) Such a program is now an extension to DOS because, like DOS,
it exists to service hardware and software interrupts.

The only distinction between a TSR and DOS itself is that DOS services are
usually dull things like file I/O and keyboard I/O. These services are
vital to the operation of the PC, but they certainly aren't glamorous
enough to inspire users to reach for their checkbooks. Pop-ups such as
SideKick, on the other hand, are the stuff that dreams are made of.


TSRs in C

TSRs are one of the last aces in the hole for the crowd that argues that
assembly language is best. UNIX(R) has demonstrated that efficient
operating systems could be written predominantly in a high-level
language. And although the first generation of DOS apps were written
primarily in assembler, current practice is to write mostly in C, with
pieces in assembly language added as necessary. But in the TSR business,
assembly language is still king, and there are good reasons for it to
continue its reign.

This article will show you how to write TSRs in C, but you should know
what you will gain and what you will lose. C is not a silver bullet; even
if you program in C, to write a TSR you must understand how the PC
hardware, DOS, interrupts, and the stack all work. Furthermore, most
published information about TSRs is written by assembly language
programmers. True, you won't have to buy the assembler or learn how it
works, but whether in C or assembly, writing a TSR is not like writing the
Hello World program.

As you might expect, TSRs written in C take more space than assembly
language equivalents. There are several reasons for this. One is that
compilers, even good ones such as Microsoft's, just don't generate code
that is as compact as that of a first-rate human coder. For most programs
the difference is insignificant, but in a TSR, every byte counts and is
counted by reviewers and users. The more important reason for the extra
size of C TSRs is that most C programs drag in a lot of the C library,
usually more than you realize. Look at a MAP file of something simple,
like Hello World, to see what I mean.

Again as you might expect, C TSRs tend to be less time efficient than
assembly language TSRs. This isn't usually a problem, but I am, after
all, pretty firmly on the side of high-level languages for most
applications.

The benefit of writing TSRs in C is similar to the benefit of using C in
other areas. C is easier to write, debug, and maintain than assembly
language. Also, there are more good C programmers than good assembly
language programmers. And many excellent programmers, though they are
able to work in assembly, prefer to tap the extra productivity of C.
If you are writing the next SideKick, the traditional assembly language
approach is probably best, but for less ambitious projects, such as a TSR
for in-house use, C should be seriously considered.

We will detail the Microsoft(R) C features and techniques for writing TSRs
and discuss a few of the often misunderstood but necessary C features,
such as function pointers. The goal is not to provide a generic primer on
TSR techniques and pitfalls, although some of that information does
appear at the end of the article. A sample TSR is provided that pops up
and displays the time on the screen. It is a good example because of its
brevity and simplicity, but it is not a comprehensive TSR that
demonstrates every possibility.

Although Microsoft C possesses the basic features required for writing a
TSR, it is not a convenient entry to the TSR world. If you use Microsoft
C, you will need to develop your own safety strategies for dealing with
the deficiencies of DOS and coexisting with other TSRs. One alternative to
this is to obtain the /*resident_C*/ subroutine package from Essential
Software, located in Maplewood, New Jersey ((201) 762-6965).
/*resident_C*/ contains about a dozen subroutines in to facilitate
writing TSRs. Although Essential Software has handled some of the details,
the information presented here will still be useful even if you do look
into /*resident_C*/.


Traditional TSRs

DOS is not known for software that plays by the rules. In more
comprehensive and controlled operating systems, such as UNIX and MS(R)
OS/2, programs are forced to work within the model provided by the
system. But almost all PC programs play it fast and loose, ignoring DOS
except when they want to access files.

The structure of a normal DOS application is that it is loaded into memory
by DOS, and then DOS transfers control to it. Once the application starts
to run, it works under the assumption that it has rights to the machine.
It just waits around idly if there is nothing to do and continues to run
until it decides to exit. It only relinquishes control to DOS when there
is something it wants DOS to do, such as read or write a chunk of a
file.

The entire structure and set of assumptions is different for a TSR. In the
beginning the TSR and normal applications are the same. Like an ordinary
application, the TSR is loaded by DOS, and then DOS transfers control to
the TSR. The TSR then initializes itself and permanently relinquishes
control to DOS, but in such a way that its load image is preserved-the
program continues to be resident in memory. It is at this point that the
magic begins. The TSR is now like a voyeur at the grand ball; it watches
what is going on, but must not do anything to call attention to itself.
However, if its services are later requested, it springs into action,
confidently performs its task, and then slips back into the shadows to
wait for another request.


Interrupts

Even novice PC users quickly learn about two kinds of interrupts,
hardware and software. Hardware interrupts occur when a device asserts its
interrupt line. Each hardware device is programmed to request interrupts
at certain times, and handlers must exist to service the interrupts.
Software interrupts occur when the INT instruction is executed. In either
case, when an interrupt occurs, control passes to an interrupt handler,
whose address is stored in a table in low memory on the computer. The
interrupt handler routine does whatever is necessary and then issues an
IRET instruction that returns control to whatever routine was active when
the interrupt occurred.

Interrupt handlers for hardware interrupts are generally forbidden to
alter aspects of a machine's state, such as its register contents,
stack, or screen contents. Obviously, though, some aspect of the machine
state is changed by a hardware interrupt. There are specific variables
or locations in memory that are reserved for communication between
interrupt handlers and ordinary software, which the interrupt handlers
can change. For example, the timer interrupt alters the clock ticks
variable. It would be intolerable for the timer tick handler to change the
AX register, because that would make most software appear to fail
randomly.

Interrupt handlers for software interrupts are called explicitly and
often return results by modifying registers. For example, the AX register
is modified by most DOS calls. This behavior is allowable and expected.
However, when a TSR intercepts a software interrupt, it should be careful
not to alter any registers.

In assembly language it is relatively easy to create a routine that
doesn't change any registers. The key is to note which registers are
used by the routine so that they can be PUSHed on entry and POPed before
exit. C routines, however, aren't so scrupulous about saving
registers. In a normal Microsoft C subroutine, the AX and DX registers
can return values, the flags register can be altered, the ES register can
access FAR data, and so on. Actually, all that is required is that a
routine must save SI, DI, DS, SS, BP, and SP. This behavior is permissible
because the C compiler generates code that respects these conventions.

Microsoft C has a little-known and not thoroughly documented function
declaration keyword called interrupt. When a function is declared as
interrupt, then additional register saving may occur, and the return
instruction will be IRET instead of RET. Another feature of an interrupt
function is that you can declare function parameters that correspond to
the registers. This lets you use the value of a register, or assign a
value to a register.

Figure 1 shows a C language interrupt routine and the corresponding
assembly code that is emitted by the Microsoft C compiler. Note that
interrupt routines must always be declared far, because both hardware
and software interrupt handlers are called by reloading both CS and IP.

An interrupt procedure can be called as if it were an ordinary procedure.
The compiler is able to deduce that an interrupt procedure should be
called by pushing flags, CS, and then IP. On exit, these items are popped
off the stack by the IRET instruction. However, the compiler does not
realize that an interrupt procedure, even though it may declare 13 formal
parameters, should be called without any actual parameters.

Using the old-style K&R declaration [Brian W. Kernighan and Dennis M.
Ritchie, The C Programming Language (Prentice-Hall, Inc., 1978)] shown in
Figure 1, the compiler will accept any number of actual parameters. If you
instead use the new ANSI-style declaration list, such as the following:

void interrupt far intfarfn(unsigned es, unsigned ds, unsigned di,
   unsigned si, unsigned bp, unsigned sp, unsigned bx, unsigned dx,
   unsigned cx, unsigned ax, unsigned ip, unsigned cs, unsigned flags)
   {
   }

the compiler will expect you to supply exactly 13 parameters. In either
case, any parameters that you mistakenly supply will be ignored. You must
be very careful to supply the register parameter list in the correct
order. The C compiler doesn't match things by name, instead the first
parameter is ES, the second is DS, and so on. For example, if you need to
access DI but you don't care about ES or DS, you could declare the
function as:

  void interrupt far
     intfarfn (unsignedjunk1, junk2, di)
     {
     }

The junk1 and junk2 parameters are necessary to put di in its proper place
in the parameter list.


Hooking Interrupt Vectors

When a TSR first starts to operate, it is a normal DOS task and has
control of the machine. It can do whatever it wants, within the thin
framework of DOS. However, once it terminates, the only way that it will
execute is in response to interrupts. Thus, before exiting, every TSR
must copy a far pointer to one of its functions into one of the slots in
the interrupt table. Then when the interrupt occurs, the given function
will be activated.

In most situations a further requirement is that the function must also
pass control to the original handler for the interrupt; this operation is
called hooking a vector. Most TSRs hook multiple vectors; for example,
they may hook the keyboard interrupt so that they can look for their hot
key and hook the DOS interrupt so that they can keep track of which DOS
calls are in progress. Hooking vectors isn't limited to TSRs, though;
ordinary programs also use the facility. For example, communications
programs often hook the serial data interrupts, and many programs hook
both the critical error interrupt and the Ctrl-Break interrupt.

Microsoft C contains two functions that make it easy to hook an interrupt
vector, _dos_getvect and _dos_setvect. The _dos_getvect function, which
reads a vector, is usually used first. Its parameter indicates which
vector is to be read, and its return value, a far pointer to a function,
is usually stored for later use.

The _dos_setvect function takes two parameters: the first indicates a
specific vector, and the second is a far pointer to a function that will
be the new value for the given vector. The use of _dos_getvect and
_dos_setvect is shown in the main routine of Figure 2. Notice that the
program is not a TSR; it is an ordinary program that spies on the DOS
interrupt. The program is careful to restore the vector before exiting;
if this were not done, the machine would crash as soon as the next
application was loaded into memory.

Declaring a variable that can store a far pointer to an interrupt function
is a bit tricky.  That C allows such a declaration is one of its admirable
strengths, but most agree that the notation is structured poorly. The two
declarations at the top of Figure 2 are:

   void far interrupt newint21; /* DOS /*
   void (interrupt far *oldint21);

The first declares that newint21 is a far interrupt function that doesn't
return a value. This declaration doesn't set aside any storage; it just
tells the compiler about newint21, so that the compiler can generate
correct code. The second declaration looks similar but is actually
very different. It creates a variable called oldint21 and states that
oldint21 holds a far pointer to an interrupt function that does not return
a value. Both sets of parentheses in the oldint21 declaration are
necessary.

The first set makes "interrupt far *" pertain to oldint21, and the second
(empty) set makes it a pointer to a function. C declarations are read
from the inside out: the asterisk (*) is read as "pointer to," the
parentheses (( ))  are read as "function returning," and the brackets ([])
are read as "array of." Also, items to the right of an identifier
(parentheses and array brackets) bind tighter than items to the left
(the indirection operator and keywords such as far, near, and
interrupt). The declaration of oldint21 is read "oldint21 is an interrupt
far pointer to a function returning a void." That's just what we want.

When a TSR hooks a vector, one of three possible interactions with the
original handler for that vector may occur:

  ■  ignore the original handler
  ■  CALL the original handler
  ■  chain to the original handler

TSRs often decide which of these approaches to take based on the current
situation. For example, a routine waiting for a specific hot key might
chain to the original keyboard handler each time it sees a keystroke other
than its hot key, but CALL the original handler when it detects its hot
key.

The first option, ignoring the original handler, is only possible in a few
situations. Typically the original handler must be invoked in order for
the machine to operate correctly.  The second possibility, CALLing the
original handler, permits a TSR to gain control after the original
handler has completed operation. This is not very common, but it does
occur. You can call a function whose address has been stored in a function
pointer such as oldint21 as follows:

  (*oldint21)();

Both sets of parentheses are necessary. Without the seemingly superfluous
first set of parentheses, oldint21 would look like a function, instead of
a pointer to a function. It is critical that oldint21 be declared as a far
pointer to an interrupt function, instead of a plain far pointer to a
function, because different calling conventions must be used to call
interrupt functions.

The third option, chaining to the original handler, is very common. The
idea is that the TSR handler JMPs to the original handler. Before the
JMP the TSR must restore any registers that it modified and ensure that
the stack pointer and stack segment register are exactly what they were
when the TSR handler was activated.

In an assembly language program, chaining is not hard to accomplish,
given a clear understanding of the machine's register state. In C it
is necessary to use the _chain_intr procedure call, a function that should
only be called from within an interrupt procedure. This is because it
understands how the stack has been managed by an interrupt procedure and
uses that knowledge to unravel the stack and then jump to the specified
far location. When the chained-to routine starts to execute, the register
contents and stack layout are exactly as they would have been if it had
been called directly. A rudimentary example is shown in the newint21
function of Figure 2.

When the program in Figure 2 is executed, it usually prints the message
"1 syscalls" to indicate exactly one DOS Interrupt 21H system call has been
executed. Of course, that system call is the getch call contained in the
function itself. If while the program is waiting, you run a pop-up that
executes additional system calls, its count will be displayed when the
program exits.


Stack Considerations

For the most part, C programmers don't have to worry much about the
stack. It does help to keep in mind that, on a hardware architecture like
that of the PC, C auto variables are stored on the stack. A good general
rule in C is that you shouldn't allocate huge data structures inside a
function. (One common mistake by novices is to declare a program's major
data structure in the main routine.) If the stack is too small, you simply
recompile your program to use a larger stack. Recursive programs usually
need a larger stack than the 2Kb default.

When an ordinary EXE program is executing, it is using its own stack, but
when a DOS system call is invoked, DOS switches to its own stack.
Actually there are three DOS stacks, one for system calls 0 through 0CH,
another for functions above 0CH, and a third for use during critical
errors. A TSR has two choices for stacks: it can hope that the active
stack at the time it is called has a few extra bytes or it can switch to
its own stack. Some assembly language TSRs switch just to be safe.

There are two very important things to remember if you choose the easy
way, using the current stack. The first is that you must ensure that stack
checking is turned off in your software by using the -Gs compiler option
and that you don't invoke any library routines that use stack checking.
Some library routines do, some don't. When you compile, use the -Fm
compiler option to generate a MAP file, and examine that file for
references to chkstk. If you find any such references, systematically
eliminate library routines until you have excised chkstk from your
program.

The second thing to remember is to be extremely careful in your use of
stack space. Making local variables static (instead of the default, auto)
will avoid using the stack for local variables. Remember that static
locals persist and must be initialized explicitly each time a routine
starts; you can't rely on the initialization in a declaration for any
function activation except the first. To minimize stack use, don't use
recursion, don't allocate more than a handful of variables on the stack,
and don't deeply nest your procedures. If your TSR treads lightly on the
stack, you probably won't need to switch stacks.

Another difficulty caused by the stack dilemma is that DS and SS may not
be the same while a TSR is executing. You may want to use the -Aw compiler
option so that the compiler will generate code that does not assume DS and
SS are the same. If you do specify -Aw, you'll also need to specify -Asn
to create a small-model program. Usually the compiler doesn't create code
that relies on DS==SS, so you can often get away with omitting -Awsn.
Because of this problem, you should avoid taking the address of auto local
variables. As mentioned previously, local variables themselves should be
avoided in TSRs to save wear and tear on the stack, so refraining from
taking their addresses is not much more of a sacrifice.

A few library routines contain code that relies on the DS==SS assumption,
but the only way to find these routines is by trial and error. If a
library routine doesn't work correctly when used in a TSR, but does work
in the same situation when used in a normal program, then the DS==SS
assumption is probably the culprit.


Library Routines

One of the best features of Microsoft C is its library. In previous
versions of Microsoft C the library contained extensive I/O routines, a
versatile math library, routines for manipulating strings, dynamic
memory allocation functions, a generic OS interface, and a few PC-specific
routines. Versions 5.0 and later add graphics and more complete PC-
specific routines.

Much of the Microsoft C library cannot be used in a TSR. Some routines
cause problems because they directly or indirectly call malloc. When a
program is running, malloc manages a memory pool that it parcels out
upon request. But that memory is not available to a TSR, and any use of
malloc, or its variants, is forbidden in a TSR. Some routines use the
stack too much, while other routines call chkstk, which will usually fail
because TSR stacks are not where chkstk expects them to be.

The routines that are definitely safe are the ones you can duplicate
fairly easily. One possibility is purchasing the Microsoft C library
source code, so that you have a more detailed guide to what happens in
each function. Owning the source also lets you recompile a routine so that
it won't do any stack checking; although Microsoft routines that have
stack checking on often have it on for a good reason, they need more than
a couple of bytes on the stack. (Another way to disable chkstk is to
modify CHKSTK.ASM, which is supplied on the Microsoft C Utilities disk.)


Program Size

Although Microsoft C provides a convenient subroutine, _dos_keep, to
transform the active program into a background program, there is one
small hitch. You must tell _dos_keep how many 16-byte paragraphs to keep.
One way to figure it out is to cheat, either by looking at the MAP file or
by asking to keep more than you could possibly need. A better way is to
figure it out exactly within your program.

 MS-DOS(R) C programs have the following general layout:

   ■  program segment prefix
   ■  text
   ■  data
   ■  stack

For our current purposes, the size of the stack segment is irrelevant
because it is not used after the program terminates but stays resident.
The task is to find the size of the other parts.

The program segment prefix (PSP) is easy; it's always 256 bytes (100H). To
refine a method for automatically determining the size of the text and
data segments, I wrote the LOCS.C program, which is shown in Figure 3. Its
key is a pair of huge pointers, startofitall and endofitall. I set
startofitall to point at the start of the PSP and endofitall to point at
the end of the data segment. Finding the start is easy; the variable _psp
always contains the segment address of the start of the PSP, and the
offset of the start address is always 0. The end of the data segment is
located by taking the address of a variable called end, which is always
the last datum in the data segment. Subtracting the huge pointers (they
must be huge not far pointers because their segments are different)
reveals the program size, 8064 bytes in this case. Figure 4 shows an
example of the output from the LOCS program.

To verify LOCS.C's results, I consulted its MAP file. It showed that the
first text address was 0000:0000H, and the last data address was
016D:07B0H, which converts to 1E80H. (To make the segment:offset to bytes
conversion, shift segment left 4 and add to offset. 016DH shifted left 4
is 016D0H; that plus 07B0H is 1E80H.) In decimal, 1E80H bytes is 7808,
and 7808 plus 256 for the size of the program segment prefix yields 8064,
exactly the size reported by the LOCS program.

The only subtlety in the LOCS program is converting bytes to paragraphs.
Remember to round up; for example, 10 bytes is 1 paragraph, 17 bytes is
two paragraphs, and so on. The simple logic at the end of LOCS rounds up
(if necessary) and then converts to bytes by shifting right 4 (dividing
by 16).


Terminating and Staying Resident

Figure 5 shows a simple TSR that will pop up the time whenever you strike
its hot key. This simple routine only intercepts the keyboard interrupt.
When it detects its hot key, it pops up the time of day on the screen. In
this sample TSR, all of the work is done with BIOS calls and there is no
file I/O. Many useful TSRs are this easy, but others require more
extensive use of DOS.

The main routine in Figure 5 contains a blend of the code from Figure 2
and 3. At the beginning of the routine, the old interrupt 9 handler's
location is saved, and a new interrupt 9 handler is installed. Next there
is the location arithmetic that calculates how much space to save when
_dos_keep is called. Following that, is the actual call to _dos_keep,
which makes the program resident. _dos_keep doesn't restore the machine
state to exactly what it would be if exit, the normal way out of a C
program, were called. One difference is obviously that the memory for the
program text and data is preserved. Another difference is that several
vectors relating to floating point operations that were taken over by the
C startup routines aren't restored by _dos_keep as they would be by exit.

The interrupt hooking and chaining occurs in the newint9 function. Each
time a keyboard interrupt occurs, newint9 is activated. As soon as it is
sure that the hot key has not been struck, it chains to the original
keyboard interrupt handler, but when it detects the hot key, it CALLs the
original handler. This allows the original handler to retrieve the
keystroke normally and to perform any of its routine processing.
However, as soon as the normal keyboard handler returns, newint9 calls
popup, which reads in the hot key, displays the time message, waits for
another keystroke, and then finally restores the original screen.

The newint9 procedure maintains a flag variable called active that is set
to true before popup is called and then cleared afterward. This prevents
newint9 from calling popup a second time, if the hot key is detected while
popup is active. This isn't farfetched; it would occur if the user struck
the hot key once to pop up the time message, and then struck it again to
take down the message.

Figures 6 and 7 show a file called tsrbios.c, which contains several BIOS
video support subroutines that will display the date string and then
restore the original screen, and TSRBIOS.H, which contains the function
prototypes for TSRBIOS.C. These routines work via the BIOS for simplicity
and universality, and for the short message displayed in this example the
speed penalty of using the BIOS video routines is irrelevant.

However, the approach taken here will not mesh smoothly with programs that
bypass the BIOS cursor services, such as Lotus 1-2-3(R), because these
routines assume that the cursor position is truly represented by the
BIOS's cursor position variable. The only difficulty in these BIOS video
routines is preserving the cursor location. This is done by picking up
its value from the BIOS data area using the memory address returned by
scr_get_curs_addr.


Advanced TSRs

TSRs that read or write disk files, list directories, or perform other
tasks using DOS must be more sophisticated than the simple TSR discussed
here. Remember that DOS is not a multitasking system. When a request comes
in, DOS automatically switches to one of its internal stacks. Because
a normal application program is inactive while DOS is servicing one of its
requests, it is impossible in the absence of TSRs for another request to
come in while the first is being serviced. However, when there are TSRs
lurking in the shadows, it is possible for one request to occur while
another is being serviced. Unfortunately, DOS is coded in such a way
that it will crash or misbehave whenever a new request comes in while
another is underway. It isn't hard to design a more flexible system call
scheme, but it didn't seem very important way back when DOS was being
developed.

Because of this limitation, DOS maintains a busy flag. A program can
determine the location of the flag by executing DOS system call 34H. The
far pointer result is returned in ES:BX. While DOS is servicing a request,
it sets the busy flag. That doesn't mean that DOS is smart enough to avoid
problems by noticing that it is busy, but it does mean that it is possible
for a TSR to notice that DOS is busy and avoid making a deadly request.

Unfortunately, the DOS busy flag is set during certain times so that it is
possible to execute most DOS file I/O calls. For example, when DOS is
waiting for you to enter a keystroke, it could accept a file I/O request.
One way to detect this situation is to hook the Interrupt 21H vector and
keep track of exactly which system call is in progress. Then the TSR
could, if it did notice that a system call was in progress, decide if it
was in fact safe to do file I/O.

The easiest solution is to avoid all DOS calls when a call above 0CH is in
progress, but allow calls above 0CH when a call below 0CH is in progress.
Many TSRs also use the status of Interrupt 13H, the BIOS I/O routines, to
help determine when it is safe to make file I/O requests.

Another way to solve this problem is to employ a resource provided by DOS,
Interrupt 28H. When the DOS busy flag is set, but functions above 0CH are
safe, DOS periodically activates Interrupt 28H. The original use of
Interrupt 28H was for the resident part of PRINT.COM, but it is a general
mechanism now used by many TSRs. In fact, some TSRs even activate
Interrupt 28H when they ascertain that it is safe for other TSRs to pop
up over them.

Many TSRs also use the periodic 18 Hz interrupt. When their hot key is
detected, the routine that hooks the keyboard interrupt sets a flag
variable indicating that a pop-up request has been issued. A short time
later the periodic timer interrupt will occur. If DOS is idle, the routine
that hooks the timer interrupt will call the pop-up routine; otherwise,
the timer handler will try again next time. This is often combined with a
handler that responds to Interrupt 28H. Whichever handler gets there
first (and safely) will activate the pop-up routine, and then cancel the
pop-up request flag.

Two further considerations are how to handle the keyboard interrupt and
the critical error interrupt. It shouldn't be possible to break out of a
TSR by striking Ctrl-Break-havoc will ensue. Similarly a robust pop-up
should hook the critical error interrupt, Interrupt 24H, while it is doing
file I/O. If it fails to do this, a critical error (those pesky "Abort,
Retry, Ignore?" messages) may have unforeseen consequences. Both of these
interrupts should only be hooked while a pop-up is popped up and then
restored when the pop-up returns to the shadows.

Another complication for many TSRs is detecting whether they are already
loaded. It is very undesirable to load a TSR twice, but the hidden,
background nature of TSRs often leads people to try to load them a second
time when they have difficulties after the first load.

There are several ways for a TSR to detect that it is already loaded. One
simple way is to search memory (from 0 to the start of its PSP) for
something distinctive, such as its copyright notice. If the notice is
found, the TSR must already be loaded. Another technique is to use
interrupts; for example, you can add a new feature to an existing
interrupt. Before terminating, the TSR executes that interrupt with the
special request code. If the TSR is already loaded, it will return a
special code. If the TSR is not loaded, the ordinary BIOS handler will get
control and do something different (usually it will do nothing).


Conclusion

Whatever language you use, the difficulty of writing a TSR lies primarily
in DOS's inadequacies, not in the language. Assembly language
programmers have a head start, because the same tricks and techniques
that they use all the time are applicable to TSRs.

Many of the Microsoft C features that you need to write a TSR are
unfamiliar because they are rarely used for other types of programming.
However, writing a TSR in C is fairly simple once you understand the
Microsoft C tools and the basic tricks of the TSR trade.

The bottom line is this-for straightforward TSRs use whichever language, C
or assembly, is most familiar. For many programmers, that means C. But for
the most demanding TSRs, assembly language is still the way to go.


Figure 1:  Sample C Interrupt Routine and Resulting Assembler Code

A C Interrupt Function

void interrupt far  intfarfn(es, ds, di, si, bp, sp, bx, dx, cx, ax, ip,
                             cs, flags)
unsigned es, ds, di, si, bp, sp, bx, dx, cx, ax, ip, cs, flags;
{
}

The Assembler Code Emitted by the C Compiler

   PUBLIC      _intfarfn
_intfarfn   PROC FAR
    push    ax
    push    cx
    push    dx
    push    bx
    push    sp
    push    bp
    push    si
    push    di
    push    ds
    push    es
    mov     ax,SEG _DATA
    mov     ds,ax
    cld
;   es = 0
;   ds = 2
;   di = 4
;   si = 6
;   bp = 8
;   sp = 10
;   bx = 12
;   dx = 14
;   cx = 16
;   ax = 18
;   ip = 20
;   cs = 22
;   flags = 24
    pop     es
    pop     ds
    pop     di
    pop     si
    pop     bp
    pop     bx
    pop     bx
    pop     dx
    pop     cx
    pop     ax
    iret
_intfarfn   ENDP


Figure 2:  A simple program that hooks a vector and then chains to the
           original handler.

#include <dos.h>

#define DOS_INT 0x21

void far interrupt newint21();     /* DOS */
void (interrupt far * oldin21)();
unsigned long syscalls = 0;

void interrupt far newint21(es, ds, di, si, bp, sp, bx, dx, cx, ax,ip,
                   cs, flags)
unsigned es, ds, di, si, bp, sp, bx, dx, cx, ax, ip, cs, flags;
{
   syscalls++;
   _chain_intr(oldint21);
}

main()
{
    /* save old int21 vector */
    oldint21 = _dos_getvect(DOS_INT);
    _dos_setvect(DOS_INT, newint21);
    getch();
    printf("%lu syscalls\n", syscalls);
    /* restore old int21 vector */
    _dos_setvect(DOS_INT, oldint21);
}


Figure 3:   The LOCS.C program, which shows how to determine the size of a
            program.

#include <dos.h>
extern unsigned int _psp, end;
extern unsigned char _osmajor;

main()
{
     char huge *startofitall;
     char huge *endofitall;
     unsigned blength; /* byte length: psp+text+data */
     unsigned plength; /* paragraph length */
     printf("Dos version %d\n", _osmajor);
     FP_SEG(startofitall) = _psp;
     FP_OFF(startofitall) = 0;
     endofitall = (char huge *)&end;
     blength = endofitall - startofitall; /* bytes */
     plength = blength;
     if (plength & 0xf)  /* round up to next 16 byte para */
           plength += 0x10;
     plength >>= 4;      /* convert to paragraphs */
     printf("start %Fp, end %Fp, size %u (bytes) %u
           (paragraphs)\n", startofitall, endofitall, blength, plength);
}


Figure 4:  Sample LOCS Output

Dos version 3 start 0C76:0000, end 0DF3;07B0, size 8064 (bytes)
    504 (paragraphs)


Figure 5:  A TSR to Pop Up the Time

*/
 * tsr to pop up the time
 */

#include "tsrbios.h"
#include <dos.h>
#include <bios.h>
extern int _psp, end;
extern unsigned _osmajor;

extern union REGS regs;

#define HOTKEY_CODE 0x11      /* W key */
#define HOTKEY_STAT 0x4       /* ctrl */
#define KEYPORT          0x60      /* keyboard i/o port */
#define DOS_INT          0x21
#define KEYSVC_INT       0x9
#define DOS_PRINTSTRING  0x9

#define StartOfTime 6    /* 6 characters into msg string */
char msg[25] = "****  HH:MM:SS  ****";
char buf[25];

/* variables for time calculations */

unsigned long ticks;
int h,m,s;
char *sptr;

void (interrupt far * oldint9)();

/* look for the hot key, and call popup when you see it */

void interrupt far newint9(es, ds, di, si, bp, sp, bx, dx, cx, ax, ip, cs,
                           flags)
unsigned es, ds, di, si, bp, sp, bx, dx, cx, ax, ip, cs, flags;
{
      static unsigned char keycode;
      static char active = 0;
      if (active)
         _chain_intr(oldint9);
      keycode = inp(KEYPORT);
      if (keycode != HOTKEY_CODE)
         _chain_intr(oldint9);
      if (_bios_keybrd(_KEYBRD_SHIFTSTATUS) & HOTKEY_STAT) {
         /* got it */
         active = 1;
         (*oldint9)();
         /*let normal routine process hot key*/
         popup();
         active =  0;
      }
      else
         _chain_intr(oldint9);
}
main()
{
      char huge *startofitall;
      char huge *endofitall;
      unsigned length;
      /* check dos ver */
      if (_osmajor < 2) {
         dos_puts("Not loaded, requires dos v2 or
                  over.\r\n$");
         exit();
      }
      /* save old int9 */
      oldint9 = _dos_getvect(KEYSVC_INT);
      _dos_setvect(KEYSVC_INT, (void (interrupt far *) ())newint9);
      /* some location work */
      FP_SEG(startofitall) = _psp;
      FP_OFF(startofitall) = 0;
      endofitall = (char huge *)&end;
      length = endofitall - startofitall; /* bytes */
      if (length & 0xf)  /* round up to next 16 byte para */
            length += 0x10;
      length >>= 4;     /* convert to paragraphs */
      dos_puts("Loaded ok\r\n$");
      _dos_keep(0,length); /*discards stack, keeps all else*/
}

/* print a $ terminated string */

dos_puts(s)
char *s;
{
      regs.x.dx = (int)s;
      regs.h.ah = DOS_PRINTSTRING;
      intdos(&regs,&regs);
}

popup()
{
      static int oldcursor;

      /* swallow hot key keystroke */
      (void)_bios_keybrd(_KEYBRD_READ);
      /* save cursor */
      oldcursor = *scr_get_curs_addr();
      /* put time into message */
      bigben();
      /* popup message */
      scr_row = 12; scr_col = 30; scr_swapmsg(msg,buf);
      /* wait for keystroke */
      (void)_bios_keybrd(_KEYBRD_READ);
      /* erase message */
      scr_row = 12; scr_col = 30; scr_swapmsg(buf,msg);
      /* restore cursor */
      *scr_get_curs_addr() = oldcursor;
}

*/
 * Read ticks, convert to cumulative seconds, and then fill msg.
 * cum_secs = ticks/18.2065
 * (but I don't want to use floating point)
 * or, cum_secs = (ticks * 10,000) / 182065
 * (but that would overflow in a long)
 * or, cum_secs = (ticks * 2,000) / 36413
 */

bigben()
{
      _bios_timeofday(_TIME_GETCLOCK, &ticks);
      ticks *= 2000L;   /* convert ... */
      ticks /= 36413L;  /* ... to seconds */
      h = ticks / 3600;
      m = ticks / 60;
      m = m % 60;
      s = ticks % 60;
      sptr = msg + StartOfTime;
      *sptr++ = h/10 + '0';
      *sptr++ = h%10 + '0';
      *sptr++ = ':';
      *sptr++ = m/10 + '0';
      *sptr++ = m%10 + '0';
      *sptr++ = ':';
      *sptr++ = s/10 + '0';
      *sptr++ = s%10 + '0';
}


Figure 6:  Simple Video BIOS Routines

tsrbios.c - Simple video bios routines

*/
 * video BIOS routines for tsrs
 */
#include "tsrbios.h"
#include <dos.h>

#define BIOS_VIDEOINT 0x10
#define BIOS_VIDEOINT_RDCH 0x8
#define BIOS_VIDEOINT_WRCH 0x9
#define BIOS_CURSOR_ARRAY (int far *)(0x450)
#define BIOS_VIDEO_PAGE_NUM (char far *)(0x462)

union REGS regs;
int scr_row, scr_col; /* used throughout to denote screen row, col */

static int far *scr_cursaddr;
static unsigned scr_ch;

*/
 * write msg onto screen, while saving screen contents into savbuf
 * ( scr_row, scr_col must be set before calling scr_swapmsg() )
 */
scr_swapmsg(msg, savbuf)
unsigned char *msg, *savbuf;
{
        static unsigned val;
        scr_cursaddr = scr_get_curs_addr();
        while(*msg) {
        *savbuf++ = scr_ch = scr_read_ch();
                scr_ch &= 0xff00;       /* preserve attribute */
                scr_ch |= *msg++;
                scr_write_ch();
                scr_col++;
        }
        *savbuf = 0;
}

*/
 * read the screen character addressed by scr_row, scr_col into
 * scr_ch
 */
unsigned scr_read_ch()
{
        /* poke cursor val into bios loc */
        *scr_cursaddr = scr_row<<8 | scr_col;
        /* use bios int to get char */
        regs.h.ah = BIOS_VIDEOINT_RDCH;
        regs.h.bh = *BIOS_VIDEO_PAGE_NUM;
        int86(BIOS_VIDEOINT, &regs, &regs);
        return regs.x.ax;
}

*/
 * write scr_ch into the screen location addressed by scr_row,
 * scr_col
 */
void scr_write_ch()
{
        /* poke cursor val into bios loc */
        *scr_cursaddr = scr_row<<8 | scr_col;
        /* use bios int to get char */
        regs.h.ah = BIOS_VIDEOINT_WRCH;
        regs.h.al = scr_ch;     /* char */
        regs.x.cx = 1;  /* one char */
        regs.h.bh = *BIOS_VIDEO_PAGE_NUM;
        regs.h.bl = scr_ch>>8;  /* attr */
        int86(BIOS_VIDEOINT, &regs, &regs);
}

*/
 * put the cursor address of the current page into the scr_cursaddr
 * pointer
 */
static int far * scr_get_curs_addr()
{
        return BIOS_CURSOR_ARRAY + (int)(*BIOS_VIDEO_PAGE_NUM);
}


Figure 7:  Function and Global Data Declarations for the TSRBIOS.C Routines

*/
 * external declarations for the tsrbios subroutines
 */
extern  int scr_row, scr_col;
extern  int scr_swapmsg(unsigned char *, unsigned char *);
extern  unsigned scr_read_ch();
extern  void scr_write_ch();
extern  int far * scr_get_curs_addr(void);

████████████████████████████████████████████████████████████████████████████

Customizing the Features of the M Editor Using Macros and C Extensions

───────────────────────────────────────────────────────────────────────────
Also see the related article:
  C 5.2 Library Routines and Editor Extensions
───────────────────────────────────────────────────────────────────────────

Leo A. Notenboom

The Microsoft(R) Editor is a powerful text editor that runs under MS-DOS(R)
and the OS/2 systems. Its features include the ability to edit more than
one file at a time, split the screen into multiple windows, and run tools,
including Microsoft compilers, from within the editor.

This article will look at the fundamental concepts and methods for
customizing and extending the functionality of the Microsoft Editor.
Familiarity with the basic operation of the Microsoft Editor is
assumed.

One of the most important features of current text editors is
programmability and customization. Microsoft Editor supports three
types of programmability: the definition of the meanings of individual
keystrokes, the definition of macros, and the creation of editor
extension functions. We will examine each of these, with emphasis on and
examples of the latter two.


The Keyboard

Programming the keyboard in Microsoft Editor is the process of associating
or "assigning" editing functions to keystrokes. When a keystroke is
pressed, the function assigned to it is then executed.

Every editing function that Microsoft Editor can perform has a name
associated with it, which is used to assign it to one or more keystrokes
or execute the function in macros. Any editing function can be assigned to
any keystroke that Microsoft Editor recognizes.

The editor assigns a default function to every valid keystroke. It
assigns the graphic function to all keys that you use to type in text,
such as alphanumeric keys, the unassigned function to all unused keys,
and  other editing functions to the remaining keys. For example, the exit
function is assigned to F8.

Programming the keyboard simply means making new key assignments. For
example, you can assign the exit function to Alt-X. To do this you place
the following in TOOLS.INI, the initialization file (M is used here to
reference the TOOLS.INI section for editor defaults. In practice the
section name is that of the actual editor filename and would be [MEP] if
MEP.EXE were used. [M MEP] could be used to define a single section for
both.):

  [M]
  exit:alt+x

Any number of such assignments may be contained in the [M] section of
TOOLS.INI.

You can also use the assign function by entering the following
commands:

  arg exit:alt+x assign

Using default key assignments, this means you would press Alt-A to
execute the arg function, type "exit:alt+x", and press Alt-equal sign to
execute the assign function. The Microsoft Editor User's Guide has more
details on TOOLS.INI and the assign function, and I'll refer to them
throughout the article.

The same function can be assigned to more than one keystroke. For
example, the sdelete (stream delete) function can be assigned to both Del
and Ctrl-G:

  [M]
  sdelete:del
  sdelete:ctrl+g

Macros and extension functions, as you'll see, are treated just like
standard editing functions in that they can also be assigned to any
keystroke.


Programming Macros

A macro is a sequence of editing functions and text, which is treated as
a single editing function. The editing functions used can be standard
functions, extension functions, or previously defined macros. Macros,
unlike extension functions, do not add functionality to the editor.
Instead, they enable you to combine existing editing functions to perform
larger and more complex tasks easily or reduce the number of keystrokes to
perform common tasks. Figure 1 lists Microsoft Editor's standard editing
functions.

Macros can be defined "on the fly" in an editing session by using the
assign function. It is more common, however, to store macros in TOOLS.INI.
You define a macro by stating its name and its function sequence in the
following format:

  name:=function-sequence

where each item in "function-sequence" is either a function or macro name
or a string of text enclosed in quotes. Note the use of the equal sign,
which differentiates a macro definition from a keystroke assignment. A
macro to save the current file without exiting might be defined as:

  savefile:=arg arg setfile

This example defines the macro savefile to be the sequence of editing
functions arg arg setfile, which saves the current file without exiting.
To assign Shift-F2 to the savefile function, you would make the following
assignment:

  savefile:shift+f2

Now whenever Shift-F2 is pressed the current file will be saved to disk.
The default keystrokes for this operation (without using the macro) are
Alt-A, Alt-A, and F2.

A macro is in fact a new editing function. The name of the macro can be
used anywhere that the name of an editing function can be used. It can be
used in the function-sequence of another macro, or assigned to a
keystroke, as shown above.


Arguments to Macros

Macros have no explicit syntax for accepting arguments. However, any
argument that has been selected at the time the macro is executed is
passed to the first function of the macro that takes an argument.

One way for a macro to accept an argument is to begin with the copy
function, which copies its argument to the clipboard. After using copy,
you can then manipulate this text by loading a special file named
<clipboard>.

The file <clipboard> is a special type of file known as a pseudo-file.
These files function as regular text files, except that they cannot be
saved to disk without an explicit filename. Pseudo-files are defined by
filenames that are enclosed in angle brackets (<>) and can be employed
as temporary scratch buffers during editing. Some pseudo-files are created
automatically by the editor and have special meanings. The pseudo-file
<clipboard> is written to whenever you use the copy function or delete a
block of text.


Defining Macros

As an example of defining and using a simple macro, we'll create a print
function, which sends a selected region of text to the printer. The copy
function will capture the text to be printed so the macro's syntax is as
shown in Figure 2.

The macro will execute the following steps:

  ■  copy the selected text to the clipboard
  ■  save the clipboard to a temporary file on disk
  ■  send the temporary file to the printer
  ■  delete the temporary file

This macro will be defined in pieces by defining four smaller macros that
correspond to the steps above. Writing larger macros as sequences of
smaller macros improves readability and makes the parts of one macro
available for use in other macros.

The first step is easy; you just execute the copy function to place the
selected text into the clipboard. Then the setfile function will change
the current file to <clipboard>.

  print1:=copy arg "<clipboard>" setfile%

To perform the second step, you use setfile again to save the current
file, <clipboard>, to a temporary file. Then you use setfile a third time
to restore the original current file:

  print2:=arg arg "TEMP.DAT" setfile setfile

Note that even though you have changed the current file twice in these
first two steps, the screen is not updated until input is required of the
user. The user does not see the screen switch to the clipboard and back,
as he would if the functions were executed individually from the
keyboard.

The third step uses the shell function to execute a DOS command to print
the file:

  print3:=arg "PRINT TEMP.DAT" shell
The last step is to use shell again to delete the file:

  print4:=arg "DEL TEMP.DAT" shell%

Now you put the pieces together. The completed macro looks like this:

  print:=print1 print2 print3 print4

Finally, in order to use the macro, you assign it to a key:

  print:alt+p

Now whenever Alt-P is pressed, portions of the current file are printed,
based on the type of argument specified beforehand.


Flow Control in Macros

The editor's macro language supports conditional and unconditional
branching. Every function in the editor has a TRUE or FALSE return value
that can be used for branching.

As a simple example, we'll create a new macro, bigprint, which does
everything the print macro does and also recognizes the following syntax:

  meta bigprint - print the entire file

Since the use of meta does not fit into the method of using the copy
function, as we did in the print macro, you'll have to check first for
meta and act a little differently if it is on. You can just select the
entire file for copy and execute the print macro. Note that once you've
selected the entire file, you simply use the print macro to print the
file.

Let's assume that we have already defined a macro called selectall, which
selects the entire file in preparation for the copy function that starts
the print macro. The bigprint macro then looks like this :

  bigprint:=meta +>nometa cancel selectall meta :>nometa meta print

Step by step, here's what this macro does. The meta function toggles
between on and off; it has a return value to indicate the new state. Thus
you start the macro with meta to test its previous value. Note that this
action does not affect the selected argument.

+> is the macro-language "branch if TRUE" statement. This step causes the
macro to branch to the label nometa if the previous function returned
TRUE. In this example, the previous function, meta, returns TRUE if meta
is now on, which would mean meta was off when the macro was entered. If
meta was off when we entered, then the user did not specify meta, and we
would branch around the next step.

Then, cancel eliminates any argument that the user may have selected.
Since we are about to select the entire file, any user-selected argument
would be inappropriate.

Next, selectall is reached only if the initial meta function returned
FALSE, indicating that meta was on when the macro began. We now execute
the selectall macro, defined below, which selects the entire file as input
to the copy function.

The second meta turns meta back on. The macro will execute meta one more
time near the end of this sequence. Since we will flow through that step,
we turn meta on now so that it will be turned off at that time.

:> is the macro-language label-definition statement. This defines the
label nometa, which we can branch to from the +>nometa statement above or
fall through from the meta function above.

In case the macro was executed with meta off, we will have turned it on
in the process of testing it. Here we execute meta once more to turn it
back off. Then print executes the previously defined print macro.

By the time the print macro is executed, one of two things has happened:
the user turned meta on, so we selected the entire file by executing
selectall, or the user left meta off, so we've branched around the
selectall and preserved the user's argument. In the latter case, the
user's argument is passed to copy at the beginning of the print macro.

Finally, you must define selectall. To select the entire file:

  ■  go to the end of the file
  ■  define a mark there
  ■  go to the top of the file
  ■  select from that mark

Each of these steps is then translated into appropriate macros:

  select1:=arg ppage
  select2:=arg arg "endoffile" mark
  select3:=arg mpage
  select4:=arg "endoffile"
  selectall:=select1 select2 select3 select4

Note that this macro ends with an "open arg," an argument that has not yet
been passed to a function. At the end of selectall, arg has been executed
and the mark name specified, but no function has been specified to process
the argument. When used with the bigprint macro above, the next function
executed that processes an argument will be copy. The net result will be
arg "endoffile" copy, which copies from the current location (top of file)
to the mark location (end of file), placing the entire file in the
clipboard.

Now you can assign the bigprint macro to Alt-P. Assuming meta is assigned
to F9, pressing F9 followed by Alt-P prints the entire current file.


Programming Extensions

An extension function is a new editing function written in a programming
language such as C or assembly. Extension functions call internal editor
functions to manipulate text, read and write to files, and control the
editing environment.

Extensions are the most powerful and flexible approach to programming
the editor. They permit you to add completely new functions to the editor
that do not rely on existing functions, can be customized to a much higher
degree than macros, and can execute significantly faster since they are
compiled code.


The Filter Extension

We will examine the structure of an extension and some common programming
techniques by using the filter extension as an example. The source to this
extension is shown in Figure 3 and its make file in Figure 4. Note that
the extension requires Version 1.01 or later of the Microsoft Editor.

Filter takes as an argument a region within the file being edited and
replaces that region with the results of executing an arbitrary filter
program on that region. For example, the filter program we'll use as an
example will be the DOS SORT program. After selecting a region of lines
and executing filter, the lines will be replaced in sorted order.

The filter extension demonstrates how to handle some of the issues
encountered when writing extensions, such as dealing with multiple types
of arguments, using switches, reading and writing physical files from
disk, and executing other editor commands from within an extension.

Filter does its job by:

  ■  copying the user-selected data to a pseudo-file

  ■  writing that pseudo-file to a physical file on disk

  ■  executing the filter command on that file and saving its output in a
     new file

  ■  replacing the contents of the pseudo-file with the saved output of
     the filter command

  ■  copying the new data in the pseudo-file back to the original file
     being edited

Before examining filter more closely, we should first look at what exactly
comprises an extension.


Editor Extension Structure

An editor extension consists of four sections:

   ■  extension function and support routines, the code that implements
      extension editing functions

   ■  extension initialization routine, which is called when the editor
      first loads the extension

   ■  editing function definition table, which defines the functional
      content of the extension

   ■  switch definition table, which defines any switches created by the
      extension

Extensions also require certain support files and must be compiled and
linked with certain options, as outlined in the User's Guide.


Extension Initialization

When the editor loads an extension, the initialization routine
WhenLoaded, which must be present, is called. It need not do anything and
can return immediately. WhenLoaded is useful for making default keystroke
assignments and performing any other extension initialization desired.

An extension can be loaded at any time. Normally the user places a load:
assignment in TOOLS.INI, which is executed when the editor starts up. For
example:

  [M]
  load:filter.mxt

The load: assignment, like any other assignment, can be executed at any
time by using the assign function.

In the filter extension, WhenLoaded performs three functions:

   ■  outputs an informative "sign-on" message to the editor's dialog
      line

   ■  gets a file handle for the pseudo-file it will use, creating that
      file if necessary

   ■  creates a default key assignment by assigning the filter function
      to the keystroke Alt-F

The second function deserves some explanation. This is the code that
implements it:

  pFileFilt = FileNameToHandle(szNameFilt,szNameFilt);
  if (!pFileFilt) {
  pFileFilt = AddFile(szNameFilt);
  FileRead(szNameFilt,pFileFilt);
  }

The FileNameToHandle call, which is a call to an internal editor function,
returns a handle to a file that has already been opened. If the file has
not been opened, it returns NULL. You do this first, rather than just
create the file, in case the user or some other extension or macro has
already created a file of this name. If the file is already open, you just
use the existing file handle.

If the file isn't already open, you use another internal function,
AddFile, to create it. This call creates the internal structures the
editor uses to manipulate the file but does not actually read a file
from disk. The internal function FileRead is used to load the file with
data from disk. Because this is a pseudo-file, the call to FileRead
results in no data actually being read. It is necessary here to complete
the editor's initialization of the pseudo-file.


Function Definition Table

The function definition table, cmdTable, defines for the editor the
functions contained in the editor extension. Each entry contains a pointer
to a string of text that contains the function name, a pointer to the
function itself, a reserved field that should be set to 0, and a field
containing flags. The flags define:

  ■  The types of user-specified arguments this function accepts.

  ■  Whether this function is a cursor-movement function or a text-
     editing function. Cursor-movement functions are those that, rather
     than taking an argument, can be used to help define the region of an
     argument after arg is executed. For example, down is a cursor-
     movement function that does not take an argument but can be used to
     extend a box or line argument downward.

  ■  Whether this function is sensitive to the meta prefix.

  ■  Whether this function is a window-movement function. Window-
     movement functions are similar to cursor-movement functions; they
     do not affect screen highlighting. Window-movement functions differ
     in that they may take arguments. The editor's ppage function is a
     window-movement function.

The filter function is defined by the following table entry:

  {"filter",filter,0,KEEPMETA|NOARG|BOXARG|NULLARG|LINEARG|MARKARG|
         NUMARG|TEXTARG|MODIFIES}

This entry defines the name of the function as seen by the user to be
"filter". The second field contains a pointer to the actual function. The
third is reserved and must be null. The fourth parameter contains flags
that indicate that the setting of the meta indicator is not to be
affected by using this function (KEEPMETA), all the allowed argument
types that the user can specify (NOARG through TEXTARG), and the fact
that the function may modify text (MODIFIES).


Switch Definition Table

The switch definition table, like the function table, defines for the
editor the switches contained in the extension. Each entry contains a
pointer to the text containing the name of the switch, a pointer to the
location of either the switch itself or a routine to interpret textual
switches, and flags defining the characteristics of the switch.

There are three types of switches: numeric, Boolean, and textual. Numeric
and Boolean switches are changed directly by the editor when a new
assignment is made. Textual switches require a support routine in the
extension to interpret the text.

The filter function contains a single textual switch:

  {"filtcmd",(PIF)(long)(void far *)SetFilter,SWI_SPECIAL}

The string "filtcmd" defines the function's name as seen by the user.
SetFilter is a pointer to the routine to be executed when that switch is
set. (The elaborate casting performed here avoids compiler warning
messages.) SWI_SPECIAL indicates that you have a textual switch,
requiring special processing.

The editor calls SetFilter whenever a new assignment to the filtcmd switch
is made. It receives as a single parameter a pointer to the new text
assigned to the switch, which SetFile can process as desired. In this
case, the switch sets the name of the filter program we want filter to
use. We copy away the entire text of the switch to a static location for
use when filter is executed.

By providing a switch for the filter command, the user can include a
default value in TOOLS.INI. For example:

  [M]
  filtcmd:SORT <

When the filter function is executed, the SORT command will be employed
as the filter program, using redirected input. You can also change
filtcmd on the fly.

  Extension Functions

When the user executes an extension function, the editor calls the routine
defined in the function definition table. The editor passes arguments to
the C function that describe the user's arguments to the function, a flag
indicating whether the meta prefix was in effect, and the value of the
keystroke invoking the function. In our example, they would be:

  flagType pascal EXTERNAL filter (unsigned int argData,
              ARG far *pArg,flagType fMeta)

The argData parameter is the keystroke that the command was invoked with.
For example, if the key A is used, then argData is passed as 65, the ASCII
value for A. However, filter doesn't use this parameter. The graphic
function, which is the function that handles keystrokes that you use to
type in text, simply places the value of argData as a character into the
file being edited.

The fMeta parameter is a TRUE/FALSE flag indicating the state of the meta
command prefix at the time the command was executed.

Most of the information normally used by an editing function is present
in the pArg parameter, the pointer to the argument structure. This
structure is defined as a type indicator followed by a union of six
different structures; only one of these structures is used at a time. The
type of structure used is based on the type of argument the user passed to
the function.

In our example, the filter function begins by taking actions and
accessing the appropriate structure elements, based on the type of
argument passed:

  switch (pArg->argType) {
    case NOARG:
             ∙
             ∙
             ∙
    case NULLARG:
             ∙
             ∙
             ∙
    case LINEARG:
             ∙
             ∙
             ∙
    case BOXARG:
             ∙
             ∙
             ∙
    case TEXTARG:
             ∙
             ∙
             ∙
  }

At this point there is an interesting discrepancy. In the command
description table, we specified several different argument types that
the actual function does not seem prepared to handle. NUMARG, for
example, is included in the cmdDesc table, but not in the function
itself.

Some arguments, such as NUMARG and MARKARG, are converted by the editor
to another, more basic argument type prior to calling the extension. For
example, a numarg, which is a count of the lines specified by the user, is
converted to a LINEARG, which explicitly enumerates the range of lines.
While the way they are specified by the user is different, they both
define a range of lines. Therefore the manner in which they are passed to
an extension function is the same; they are both passed as a LINEARG. See
Figure 5 for a list of all user argument types and Figure 6 for a list of
the base types they can be mapped into.

The extension function's access to the files being edited and the editing
environment is defined by the internal editor functions, or "call-back"
routines, which are listed in Figure 7. An extension function generally
operates by calling internal editor functions to gain access to the file
being edited and to retrieve, add, replace, or modify text therein.


The Filter Function

Up to this point, we've defined everything that goes around our
function-the tables, the initialization routines, and the function
parameters-and we've mentioned the routines that can be called by the
extension function. Let's now take a look at the body of the filter
function.

Remember that the purpose of filter is to replace selected text with the
results of running that text through an arbitrary filter program. The
arguments to the filter function are shown in Figure 8.

Note that we rely on markarg and numarg being converted to one of the
other argument types by the editor before the function is called. Because
the editor performs this conversion automatically, any function that
takes a linearg and boxarg can easily take numarg and markarg by just
setting the appropriate flag in the cmdTable.

We noted earlier that filter operates in five basic steps. The function
will begin by calling FileNameToHandle to get a handle to the file
currently being edited. This is a common first step in any extension
function, as the handle to the current file is required in several
internal editor function calls to identify the file that is operated on.

The function will then call DelFile to delete the contents of the pseudo-
file, which was created in the initialization routine WhenLoaded.
DelFile does not remove the file, since we have to continue to operate on
it, but simply deletes the contents of the file.

The first step copies the region selected by the user from the user's
file (handle pFile) to our pseudo-file (handle pFileFilt). The type of
copy used is based on the type of argument that the user specified. For
example:

  case BOXARG:
    CopyBox(pFile,pFileFilt,pArg->arg.boxarg.xLeft,
               pArg->arg.boxarg.yTop,pArg->arg.boxarg.xRight,
               pArg->arg.boxarg.yBottom,(COL) 0,(LINE) 0);
  break;

This code fragment is executed only if the user argument defines a
BOXARG. You use CopyBox to copy the box specified by the user into the
pseudo-file. The other argument types are handled similarly, except for
TEXTARG, which is treated as a special case, so that by entering:

  arg text-argument filter

the user can specify the actual command to be used as the filter. This is
the same command as specified by the filtcmd switch. For example, assuming
the default keystroke for arg and Alt-F for filter, to use SORT with
redirected input as the filter command, the user would type:

  Alt-A "SORT <" Alt-F

When this type of argument is detected, the filter routine just calls
SetFilter to save the command string and returns.

The second step just uses a call to FileWrite to write the contents of the
pseudo-file to a disk file named FILTER1.TMP.

The third step begins by building the text of the actual filter command
using calls to the C run-time routines strcpy and strcat, which can be
used because they fall into the category of acceptable C run-time
routines available to extensions. The user-specified filter command is
concatenated with the two temporary filenames defined in the data area.
The net result of the concatenation, using the SORT command above, is:

  SORT < filter1.tmp >filter2.tmp

This sorts the contents of FILTER1.TMP and creates FILTER2.TMP.

The DoSpawn routine, described below, is called to execute the filter
command we created, and then the disk file filter2.tmp is created.

In the fourth step, before you read in the results of the filter, you use
DelFile again to delete the previous contents of the pseudo-file. Then
FileRead, the counterpart to FileWrite, is used to read in the physical
file filter2.tmp to the pseudo-file. The net result is that the pseudo-
file now contains the results of running its original contents through the
filter.

The final step begins by calculating the maximum width of the text that
resulted from the filter. In some cases, this width will be different from
the width of the original text, and for some argument types, such as
boxarg, the correct width should be used when replacing the text.

You determine the maximum width by using the following code:

  cLines = FileLength(pFileFilt);
  cbLineMax = 0;
  for (iLineCur = 0;iLineCur < cLines;iLineCur++)
  cbLineMax = max(cbLineMax,GetLine(iLineCur,buf,pFileFilt));

FileLength returns the current number of lines in the file referenced by
the handle pFileFilt. The loop reads each line of the file into a buffer.
The text is ignored, but the GetLine routine returns the length of each
line it has read.

Then, depending on the argument type again, you delete the original text
in the user's file and replace it with the contents of the pseudo-file.
Using BOXARG  as an example again:

  case BOXARG:
    DelBox(pFile,pArg->arg.boxarg.xLeft,pArg->arg.boxarg.yTop,
              pArg->arg.boxarg.xRight,pArg->arg.boxarg.yBottom);
    CopyBox(pFileFilt,pFile,(COL) 0,(LINE) 0,cbLineMax,cLines-1,
               pArg->arg.boxarg.xLeft,pArg->arg.boxarg.yTop);
    break;

DelBox deletes the box specified by the user, and CopyBox copies in the
contents of the pseudo-file as a box.

Finally, before leaving, the function uses DoSpawn again to execute DOS
commands to delete the two temporary files that you created along the
way.

DoSpawn itself could be implemented in a number of ways; unfortunately,
using the C run-time process-control routines is not one of them. An
alternative method is to use the intdosx call under DOS or call
DosExecProgram under OS/2 to have the operating system execute the
program. Our approach is to use the fExecute function instead to execute
the editor's shell function.

DoSpawn begins by using strcpy and strcat to create the following string:

  arg "command" shell

where "command" is the text pointed to by the szCmd parameter passed to
DoSpawn. By then passing this string to fExecute, the editor shell
function causes the command to be executed by the operating system.


Conclusion

We've taken a close look at the three types of programmability that
the Microsoft Editor provides: keystroke assignments, which permit
editor functions to be tied to keystrokes and executed when the
keystrokes are pressed; macros, which enable editor functions to be
combined in complex sequences and executed as a single function; and
extensions, which create entirely new editing functions. Each is an
important and useful tool for customizing and maximizing the power and
efficiency of text editing with the Microsoft Editor.


────────────────────────────────────────────────────────────────────────────
C 5.2 Library Routines and Editor Extensions
────────────────────────────────────────────────────────────────────────────

Because of the mechanics of building Microsoft Editor extensions, there
are certain restrictions on the use of C run-time library routines. These
restrictions stem from two issues.

Extensions must be compiled with SS != DS, that is, when executing,
theextension function will operate with the same stack as the editor
itself but will have access to its own local data segment, which is not
the same as the stack segment or the editor's own data segment. This
restricts C run-time library routines to the compact model set of
libraries.

Extensions do not have C start-up support. The C start-up is responsible
for initializing a fair number of data items and the C run-time
environment. This restricts the use of C run-time library functions to
those that do not rely on this initialization.

The following list summarizes functions available to extensions from the
standard compact model library. This list uses the function categories
from Chapter 4 of the Run-Time Library Reference.

Category                                 Availability

Buffer Manipulation                      All
Character Classification and Conversion  All
Data Conversion                          All except strtod
Directory Control                        All except getcwd
File Handling                            All
Graphics                                 None
Input and Output: Stream                 None
Input and Output: Low-level              All except write in binary mode
Input and Output: Console and Port       All except cgets, cprintf, cscanf
Math                                     None
Memory Allocation                        None
Process Control                          None
Searching and Sorting                    All except qsort
String Manipulation                      All except strdup
System Calls: BIOS                       All
System Calls: MS-DOS                     All except int86, int86x
Time                                     All except ctime, gmtime,
                                         localtime, and utime
Miscellaneous                            All except assert, getenv,
                                         perror, putenv,_searchenv


Figure 1:  Microsoft Editor 1.01 Functions

╓┌───────────────────┌───────────────────────────────────────────────────────╖
Name                Function Performed

arg                 Introduce argument to subsequent function
assign              Define keystroke or define macro
backtab             Move to previous tab position
begline             Move to beginning of line
cancel              Cancel operation or argument
cdelete             Character delete
Name                Function Performed
cdelete             Character delete
compile             Execute compiler and/or browse results
copy                Copy selected text to clipboard
down                Move down one line
emacscdel           Delete character (joins lines)
emacsnewl           Move to new line (breaks lines)
endline             Move to end of line
execute             Execute other editor functions by name
exit                Exit the editor or the current file
graphic             Place character into file being edited
home                Go to top or top left position on screen
information         Display file history
initialize          Initialize the editor and read portions of TOOLS.INI
insertmode          Toggle insert/typeover mode
lasttext            Redisplay previous text arg for editing
ldelete             Delete lines or boxes
left                Move cursor left
linsert             Insert lines
mark                Place or go to a bookmark
meta                Modify action of subsequent function
Name                Function Performed
meta                Modify action of subsequent function
mlines              Move backward lines
mpage               Move backward pages
mpara               Move backward paragraphs
msearch             Search backward
mword               Move backward words
newline             Move to next line
paste               Copy contents of clipboard to current cursor location
pbal                Balance unmatched parentheses, braces, or brackets
plines              Move forward lines
ppage               Move forward pages
ppara               Move forward paragraphs
psearch             Search forward
pword               Move forward words
qreplace            Query search and replace
quote               Treat character assigned to other function as text
refresh             Reread or discard current file
replace             Search and replace
restcur             Restore cursor to saved location
right               Move cursor right
Name                Function Performed
right               Move cursor right
savecur             Save cursor location
sdelete             Delete stream of characters
setfile             Move to new file and/or save current file
setwindow           Redisplay or reorient text in window
shell               Execute operating system command
sinsert             Insert stream
tab                 Move cursor to next tab stop
unassigned          Print error message
undo                Reverse effects of previous edits
up                  Move cursor up
window              Manipulate multiple windows


Figure 2:  Arguments Used in the Print Macro

noarg        Print current line
nullarg      Print cursor to end of current line
textarg      Print the text argument
boxarg       Print the selected box
linearg      Print the selected lines
numarg       Print the next num lines
markarg      Print text between cursor and mark


Figure 3:  Microsoft Editor Filter Extension

/***********************************************************************
*
* Purpose:
*  Provides a new editing function, filter, which replaces its
*  argument with the argument run through an arbitrary operating
*  system filter program.
*
*************************************************************************/
#define ID    " filter ver 1.00 "##__DATE__##" "##__TIME__

#include <stdlib.h>            /* min macro definition */
#include <string.h>            /* prototypes for string fcns */
#include "ext.h"

/*************************************************************************
*
* Internal function forward declarations
*/
flagType pascal      DoSpawn    (char *);
void     pascal      id        (char *);
void     pascal EXTERNAL SetFilter  (char far *);

/*************************************************************************
*
* global data
*/
PFILE    pFileFilt      = 0;               /* handle for filter-file */
char    *szNameFilt     = "<filter-file>"; /* name of filter-file    */
char    *szTemp1        = "filter1.tmp";   /* name of 1st temp file  */
char    *szTemp2        = "filter2.tmp";   /* name of 2nd temp file  */
char    filtcmd[BUFLEN] = "";              /* filter command itself  */



/*** filter - Editor filter extension function
*
* Purpose:
*  Replace selected text with that text run through an arbitrary
*  filter.
*
*  NOARG       - Filter entire current line
*  NULLARG     - Filter current line, from cursor to end of line
*  LINEARG     - Filter range of lines
*  BOXARG      - Filter characters with the selected box
*
*  NUMARG      - Converted to LINEARG before extension is called.
*  MARKARG     - Converted to Appropriate ARG form above before
*                extension is called.
*
*  STREAMARG   - Treated as BOXARG
*
*  TEXTARG     - Set new filter command
*
* Input:
*  Editor Standard Function Parameters
*
* Output:
*  Returns TRUE on success, file updated, else FALSE.
*
*************************************************************************/
flagType pascal EXTERNAL filter (
unsigned int argData,            /* keystroke invoked with     */
ARG far *pArg,                   /* argument data              */
flagType fMeta                   /* indicates preceded by meta */
) {
char    buf[BUFLEN];             /* buffer for lines             */
int    cbLineMax;                /* max line length in filtered  */
LINE    cLines;                  /* count of lines in file       */
LINE    iLineCur;                /* line being read              */
PFILE    pFile;                  /* file handle of current file  */
*/
** Initialize: identify ourselves, get handle to current file, and
** discard the contents of the filter-file.
*/
id ("");
pFile = FileNameToHandle ("", "");
DelFile (pFileFilt);
*/
** Step 1, based on the argument type, copy the selected region into the
** (upper leftmost position of) filter-file.
** Note that TEXTARG is a special case that allows the user to change
** the name of the filter command to be used.
*/
switch (pArg->argType) {
  case NOARG:             /* filter entire line */
    CopyLine (pFile,
          pFileFilt,
          pArg->arg.noarg.y,
          pArg->arg.noarg.y,
          (LINE) 0);
    break;

  case NULLARG:            /* filter to EOL */
    CopyStream (pFile,
            pFileFilt,
            pArg->arg.nullarg.x,
            pArg->arg.nullarg.y,
            255,
            pArg->arg.nullarg.y,
            (COL) 0,
            (LINE) 0);
    break;

  case LINEARG:            /* filter line range */
    CopyLine (pFile,
          pFileFilt,
          pArg->arg.linearg.yStart,
          pArg->arg.linearg.yEnd,
          (LINE) 0);
    break;

  case BOXARG:            /* filter box */
    CopyBox (pFile,
         pFileFilt,
         pArg->arg.boxarg.xLeft,
         pArg->arg.boxarg.yTop,
         pArg->arg.boxarg.xRight,
         pArg->arg.boxarg.yBottom,
         (COL) 0,
         (LINE) 0);
    break;

    case TEXTARG:
    SetFilter (pArg->arg.textarg.pText);
    return 1;
    }
*/
** Step 2, write the selected text to disk.
*/
if (!FileWrite (szTemp1, pFileFilt)) {
    id ("** Error writing temporary file **");
    return 0;
    }
*/
** Step 3, create the command to be executed:
** user specified filter command + " " + tempname 1 + " >" +
** tempname 2
** Then perform the filter operation on that file, creating a second
** temp file.
*/
strcpy (buf,filtcmd);
strcat (buf," ");
strcat (buf,szTemp1);
strcat (buf," >");
strcat (buf,szTemp2);
if (!DoSpawn (buf)) {
    id ("** Error executing filter **");
    return 0;
    }

*/
** Step 4, delete the contents of the filter-file, and replace it by
** reading in the contents of that second temp file.
*/
DelFile (pFileFilt);
if (!FileRead (szTemp2, pFileFilt)) {
    id ("** Error reading temporary file **");
    return 0;
    }
*/
** Step 5, calculate the maximum width of the data we got back from
** the filter. Then, based again on the type of region selected by
** the user, DISCARD the users select region, and copy in the
** contents of the filter-file in an equivalent type.
*/
cLines = FileLength (pFileFilt);
cbLineMax = 0;
for (iLineCur = 0; iLineCur < cLines; iLineCur++)
    cbLineMax = max (cbLineMax, GetLine (iLineCur, buf, pFileFilt));

switch (pArg->argType) {
  case NOARG:             /* filter entire line  */
    DelLine  (pFile,
          pArg->arg.noarg.y,
          pArg->arg.noarg.y);
    CopyLine (pFileFilt,
          pFile,
          (LINE) 0,
          (LINE) 0,
          pArg->arg.noarg.y);
    break;

  case NULLARG:            /* filter to EOL  */
    DelStream  (pFile,
            pArg->arg.nullarg.x,
            pArg->arg.nullarg.y,
            255,
            pArg->arg.nullarg.y);
    CopyStream (pFileFilt,
            pFile,
            (COL) 0,
            (LINE) 0,
            cbLineMax,
            (LINE) 0,
            pArg->arg.nullarg.x,
            pArg->arg.nullarg.y);
    break;

  case LINEARG:            /* filter line range */
    DelLine  (pFile,
          pArg->arg.linearg.yStart,
          pArg->arg.linearg.yEnd);
    CopyLine (pFileFilt,
          pFile,
          (LINE) 0,
          cLines-1,
          pArg->arg.linearg.yStart);
    break;

  case BOXARG:            /* filter box */
    DelBox    (pFile,
         pArg->arg.boxarg.xLeft,
         pArg->arg.boxarg.yTop,
         pArg->arg.boxarg.xRight,
         pArg->arg.boxarg.yBottom);
    CopyBox (pFileFilt,
         pFile,
         (COL) 0,
         (LINE) 0,
         cbLineMax-1,
         cLines-1,
         pArg->arg.boxarg.xLeft,
         pArg->arg.boxarg.yTop);
    break;
    }
*/
** Clean-up: delete the temporary files we've created
*/
strcpy (buf, "DEL ");
strcat (buf, szTemp1);
DoSpawn (buf);
strcpy (buf+4, szTemp2);
DoSpawn (buf);

return 1;
/* end filter /*}




/*** DoSpawn - Execute an OS/2 or DOS command
*
* Purpose:
*  Send the passed string to OS/2 or DOS for execution.
*
* Input:
*  szCmd    = Command to be executed
*
* Output:
*  Returns TRUE if successful, else FALSE.
*
*************************************************************************/
flagType pascal DoSpawn (
char    *szCmd
) {
char   cmd[BUFLEN];

strcpy (cmd, "arg \"");
strcat (cmd, szCmd);
strcat (cmd, "\" shell");
return fExecute (cmd);

/* end DoSpawn /*}

/*** SetFilter - Set filter command to be used
*
* Purpose:
*  Save the passed string parameter as the filter command to be used
*  by the filter function. Called either because the "filtcmd:"
*  switch has been set or because the filter command received a
*  TEXTARG.
*
* Input:
*  szCmd    = Pointer to asciiz filter command
*
* Output:
*  Returns nothing. Command saved.
*
*************************************************************************/
void pascal EXTERNAL SetFilter (
char far *szCmd
) {
strcpy (filtcmd,szCmd);
/* end SetFilter /*}

/*** WhenLoaded - Extension Initialization
*
* Purpose:
* Executed when extension gets loaded. Identify self, create
* <filter-file>, and assign default keystroke.
*
* Input:
*  none
*
* Output:
*  Returns nothing. Initializes various data.
*
*************************************************************************/
void WhenLoaded (void) {

pFileFilt = FileNameToHandle (szNameFilt,szNameFilt);
if (!pFileFilt) {
    pFileFilt = AddFile (szNameFilt);
    FileRead (szNameFilt, pFileFilt);
    }
SetKey ("filter", "alt+f");
id ("text filter extension:");

/* end WhenLoaded /*}

/*** id - identify extension
*
* Purpose:
*  identify ourselves, along with any passed informative message.
*
* Input:
*  pszMsg    = Pointer to asciiz message, to which the extension name
*              and version are appended prior to display.
*
* Output:
*  Returns nothing. Message displayed.
*
*************************************************************************/
void pascal id (
char *pszFcn                    /* function name */
) {
char    buf[BUFLEN];                /* message buffer */

strcpy (buf,pszFcn);
strcat (buf,ID);
DoMessage (buf);
/* end id /*}

/*************************************************************************
**
** Switch communication table to the editor.
*/
struct swiDesc         swiTable[] = {
    {"filtcmd",     (PIF)(long)(void far *)SetFilter,    SWI_SPECIAL},
    {0, 0, 0}
    };

/*************************************************************************
**
** Command communication table to the editor.
** Defines the name, location, and acceptable argument types.
*/
struct cmdDesc    cmdTable[] = {
    {"filter",filter,0, KEEPMETA | NOARG | BOXARG | NULLARG |
    LINEARG | MARKARG | NUMARG | TEXTARG | MODIFIES},
    {0, 0, 0}
    };


Figure 4:   Make File for the Filter Extensions

#
# makefile for the filter extensions
#
filter.obj: filter.c
     cl /c /Gs /Asfu filter.c

filter.mxt: filter.obj
     cl /Lr /AC /Fefilter.mxt exthdr.obj filter.obj

filter.dll: filter.obj
     cl /Lp /AC /Fefilter.dll exthdrp.obj filter.obj skel.def


Figure 5:  Microsoft Editor User Arguments

╓┌────────────────┌──────────────────────────────────────────────────────────╖
User Argument    Argument Meaning

no arg           No argument specified. Default action of the function is
                 executed.

nullarg          A null argument. Often used to modify the action of a
                 function or to specify the range over which the function
                 occurs as "cursor to end of line."

textarg          Typed-in textual argument. Used to specify textual
                 information, such as search strings.

numarg           Typed in numeric argument. Often used to specify a
                 specific line number or a number of lines over which a
                 function is to be applied.

markarg          Typed-in argument that references a named mark elsewhere
                 in the current file. Besides moving to a marker position,
                 this is often used to specify the range over which a
                 function is to be executed (current position to marker
User Argument    Argument Meaning
                 function is to be executed (current position to marker
                 position).

streamarg        Arg created by moving the cursor, consisting of a range
                 of characters on a single line or a range of characters
                 that spans multiple lines but includes all characters
                 between the beginning and end of the range, regardless of
                 position.

boxarg           Arg created by moving the cursor, consisting of a box of
                 characters spanning multiple lines. Only those characters
                 within the box are included. (The distinction between
                 boxarg and multiline streamarg is made by the function
                 invoked.)

linearg          Arg created by moving the cursor, consisting of a range
                 of lines, in their entirety.

meta             The meta command prefix is not an argument but is used to
                 modify an editing function. In general it is used to
User Argument    Argument Meaning
                 modify an editing function. In general it is used to
                 either take a command to some extreme (home goes to first
                 nonblank character on a line, while meta home goes to
                 column one, regardless), or to cause a function to lose
                 some information (exit saves the current file, if
                 changed, and exits the editor, while meta exit
                 immediately exits the editor without saving any changes).


Figure 6:  User Arguments as Passed to Extension Functions

ARG Received    Possible User Argument Specified

NOARG           No argument was specified.
NULLARG         A null argument was specified.
TEXTARG         A textual argument specified. May be the result of:
                ∙ having been typed in explicitly,
                ∙ a one-line boxarg, if the BOXSTR flag is set in
                  the cmdDesc table
                ∙ a nullarg, if NULEOL flag is set (text is cursor to
                  end of line)
                ∙ a nullarg, if NULEOW flag is set (text is cursor to
                  end of word)
STREAMARG       An arg was created by moving the cursor. Can also be
                the result of a mark arg.
BOXARG          An arg was created by moving the cursor, consisting of
                a box of characters. Can also be the result of a mark arg.
LINEARG         Arg created by moving the cursor, consisting of a
                range of lines, in their entirety. Can also be the
                result of a num arg or mark arg.


Figure 7:  Microsoft Editor Internal Functions

╓┌──────────────────┌────────────────────────────────────────────────────────╖
Name                Function

AddFile             Add file to editor's file list
BadArg              Display "bad argument" message
CopyBox             Copy BOX from one file to another
CopyLine            Copy LINEs from one file to another
Name                Function
CopyLine            Copy LINEs from one file to another
CopyStream          Copy STREAM from one file to another
DelBox              Delete a BOX of characters from a file
DelFile             Delete entire contents of a file
DelLine             Delete a range of LINEs from a file
DelStream           Delete a STREAM of characters from a file
DoMessage           Display message on dialog line
Display             Cause display to be redrawn
fExecute            Execute macro string
FileLength          Return length of requested file
FileNameToHandle    Return handle to current or named file
FileRead            Read disk file into file being edited
FileWrite           Write disk file from file being edited
GetCursor           Return current cursor position
GetLine             Return line of text in file
KbHook              Restore editor's control of keyboard
KbUnHook            Release editor's control of keyboard
MoveCur             Move cursor to new position in current file
pFileToTop          Make specified file the "current" file being displayed
PutLine             Place line of text into file
Name                Function
PutLine             Place line of text into file
RemoveFile          Remove file from editor's list.
ReadChar            Read character from keyboard
ReadCmd             Read editing command interpreted from keyboard
Replace             Replace character in file
SetKey              Make keystroke assignment


Figure 8:  Arguments Used in the Filter Extension

noarg         Filter the current line
nullarg       Filter text from cursor to end of line
boxarg        Filter the selected box
linearg       Filter the selected lines
numarg        Filter a number of lines
markarg       Filter between cursor and a mark
textarg       Specify new filter command

████████████████████████████████████████████████████████████████████████████

Dynamically Creating Dialog Boxes Using New Windows 2.x Functions

Don Hasson

Creating dialog boxes or input forms dynamically, or "on the fly," was not
possible in Windows Version 1.x. The only way to define an input form or
dialog box was to predefine it in your resource file. Microsoft(R) Windows
Version 2.x, however, includes two new functions that let programmers
dynamically create dialog boxes or forms such as those found in Microsoft
Excel's sample macro sheets.


DialogBoxIndirect and CreateDialogIndirect

The two functions that are new to Microsoft Windows 2.x are
DialogBoxIndirect and CreateDialogIndirect. Close inspection shows that
the only difference between the standard DialogBox/CreateDialog and the
DialogBoxIndirect/CreateDialogIndirect functions is that the latter two
take a handle as their second parameter instead of a long pointer. By
adding these routines to an application, the program or the user has the
ability to create or modify dialog boxes as needed.

A dialog box with about 20 different items or controls is shown in Figure
1☼ The unusual part of this dialog is that it was created dynamically by
calling either the DialogBoxIndirect or CreateDialogIndirect function and
did not require extra lines in the resource file or a call to the
CreateWindow function. A table for the DialogBoxIndirect or
CreateDialogIndirect function is the only necessary component.


Microsoft Excel's Dialogs

One of the many powerful features of Microsoft Excel is that it allows
users to define their own custom dialog boxes. This is done by defining a
table of the desired controls in a macro sheet and making a call to the
table. The same type of table is necessary for windows applications;
however, this table can take any form or shape you want for your
application. It can be a text file that is read in or predefined in your
program, or just a parameter list that is created by prompting the user to
enter values in a form. Either way, your application can now dynamically
create and change dialog boxes.


The DLGBOX Program

The DLGBOX program consists of three modules, called DLGBOX.H, DLGBOX.C
and DLGTEMP.C (see Figures 2, 3, and 4). DLGBOX is the main part of the
program that makes a call to the DLGTEMP module for each dialog item to be
placed inside the dialog box. DLGTEMP builds these items into a table that
is later passed back to the DLGBOX program. The DLGTEMP module was created
as a standalone module that you could include with your application. Now
let's look at the different parts of this program. The make file (see
Figure 5) contains all the parts necessary to build this application.
Remember that two of these functions are only found in Windows 2.x, so you
must use the Microsoft(R) Windows Software Development Kit Version 2.x and
the Microsoft C 5.0 compiler or higher.

The Resource file shown in Figure 6 contains our icon and two menus. The
first menu allows you to select whether the dialog box is to be created as
a modal or modeless dialog box. The differences will be explained later
on. The second menu defined will be used in the dynamic dialog box as an
example of how to make it part of the dialog box.


New Data Structures

Using the DialogBoxIndirect and CreateDialogIndirect functions, you only
need two data structures to build a dialog box. The first is the
DialogHeader structure (see Figure 7). It is used as a header to the
table that is being built. The second data structure is DialogItem (see
Figure 8). The DialogItem structure is used for each control that is to be
placed inside the dialog box. For ease of reading in this example, the
prefix DH for DialogHeader fields and DI for DialogItem fields have been
added.

Only three functions need to be called in the DLGTEMP module in order to
perform all the work to create a dialog structure in global memory. They
are the CreateDialogHeader, CreateDialogItem, and EndDialogHeader
functions.


CreateDialogHeader

The CreateDialogHeader function passes parameters to fill in the different
parts of the DialogHeader structure, as shown in Figure 7. After the
structure is completed, a piece of global memory is allocated to store the
DialogHeader information for later use.

Looking at the DialogHeader structure, you will notice that it consists
of most of the parameters needed for the CREATESTRUCT structure used by
the CreateWindow function, and this is indeed what will happen when all
the fields are filled in. The cDHItemCount field is the only new field and
defines the number of items or controls inside the dialog box. However,
this field can only be filled in when the number of items is known. The
first field, lDHStyle, will contain the window style attributes for the
dialog box. These are the same values that are normally OR'd together for
the dwStyle field (see Table 3.4 in the Microsoft(R) Windows Software
Development Kit Programmer's Reference Version 2.x) for the CreateWindow
function.

The iDHX and iDHY fields are used to place the upper left corner of the
dialog box in the parent's client area. The iDHCX and iDHCY fields are
then used to define the width and height of the dialog box. To determine
how wide the dialog box should be, multiply the number of characters by
four and add about 10 to create an extra margin on both sides. For the
depth, multiply the number of lines by eight, adding an extra 10 to
provide a deeper margin for the top and bottom. As a shortcut, the width
and height values from the Dialog Editor, DIALOG.EXE, can be used
directly without conversion.

The next part of the structure consists of three variable-length fields.
They are called variable length because the field can range in size from 1
to many bytes. Since we don't know ahead of time how long these fields are
going to be, we cannot allocate space for them. We could have defined them
to be the longest string that could be handled, but we chose not to. In
order to use these fields, we follow a simple rule: if it is not being
used, the first byte is 00H.

The first byte after the DialogHeader structure is for the resource
name, szDHResourceName, and defines the name of the menu resource in the
application's resource file. If the first byte is 00H, then no menu is
used with the dialog box. If you want to use the ordinal number of the
menu resource, then the first byte must be set to FFH, followed by 3 bytes
for the ordinal number (in ASCII). If the first byte is not 00H or FFH,
then a null-terminated string is expected.

The szDHClassName field defines the window class for the dialog box. When
the first byte is 00H, the default class of DialogClass is used. If you
want to define your own window class, provide a null-terminated string for
szDHClassName. This is the same value that you use as lpClassName for the
CreateWindow function.

The szDHCaptionText field defines the string used for the caption bar of
the dialog box if we choose to OR the WS_CAPTION attribute into the window
lDHStyle field above. If the first byte is 00H, then nothing is displayed
in the caption bar; otherwise, a null-terminated string is required. This
string is the same value used in the lpWindowName field of the
CreateWindow function.

Now that we are finished filling in the DialogHeader structure, you can
see that we have all the necessary fields to call the CreateWindow
function. In our example, we have simplified this by passing the
parameters in the CreateDialogHeader function. We are now ready to fill
in the individual items to be placed inside the dialog box.


CreateDialogItem

The next function in our program is the CreateDialogItem function. It
will pass all the information necessary to fill in the different parts of
the DialogItem structure, as shown in Figure 8. After the structure is
filled, the current global memory block is expanded with the
GlobalRealloc function to the required size, and the information
copied into the newly expanded memory block.

Another benefit of the CreateDialogItem function is that you already
have most of the predefined window controls and their styles set so that
you can reference them with a single number. This saves you from having to
pass all the different attributes for a predefined control like Microsoft
Excel.

The DialogItem structure consists of the same fields as the DialogHeader
structure since these values must be used to call the CreateWindow
function. The iDIX, iDIY, iDICX, and iDICY fields are used to define the
upper left corner and the width and height of the control. The iDIID field
defines the control's ID (for example, IDOK). This value is the same one
used for the hMenu field in the CreateWindow function.

The behavior for the control is defined by the lDIStyle field. This can be
any of the predefined window styles and corresponds to the same value used
for the hStyle field in the CreateWindow function. In addition, you are
allowed to OR in all of the applicable window style and control style
attributes that you are accustomed to. (See Table 3.4 and 3.5 in the
Microsoft(R) Windows Software Development Kit Programmer's Reference
Version 2.x for a complete listing.)

We then have cbDIControlClass to define the specific control class of
the item. Figure 9 is a list of the predefined control Classes Windows
offers. (See Table 3.3 in the Microsoft(R) Windows Software Development Kit
Programmer's Reference Version 2.x for a more complete description of
each control class.)

The szDIText field is used to hold the initial text to be displayed in the
control. If the first byte is 00H, then no text will be displayed. If the
first byte is not 00H, then a null-terminated string is required.

The cbDIExtra field is just like the last field in the WNDCLASS
structure, cbWndExtra. It allows us to tack on extra bytes to the WNDCLASS
structure. We can then store and retrieve additional information from the
window by using the same GetWindowWord, GetWindowLong,
SetWindowWord, and SetWindowLong functions. Once we have all the
information, we are ready to pass these as parameters in the
CreateDialogItem function. If everything proceeds correctly, the global
memory is expanded, the new additional information is copied to the end,
and the item count, iItem, is bumped up by one. This way, when we are
finished putting in the last control item in the dialog, we do not have
to count how many items are in the dialog box.


EndDialogHeader

The last function used in our sample application is the EndDialogHeader
function. Here we need to go in and directly modify the DialogHeader
cDHItemCount field that is now in global memory. We do this first by
locking down the memory, then moving the pointer over the correct number
of bytes. Then we change the memory value with the correct number of items
stored in iItems that we kept track of in the CreateDialogItem function.
We then unlock the memory. If the EndDialogHeader function is
successful in changing the memory, it will return a handle to the global
memory that contains our newly created dialog table.

Now that the dialog structure is built, we are ready to pass the dialog
table to one of our dialog box functions. If we want a modal dialog box,
then all we have to do is call the DialogBoxIndirect function with the
handle to global memory. However, if we want to create a modeless dialog
box, one additional step is necessary──we have to lock down the global
memory so that we can pass a long pointer to the memory as the second
parameter of the CreateDialogIndirect function. Other than having to lock
down the global memory, the main difference between the DialogBoxIndirect
and CreateDialogIndirect functions is that DialogBoxIndirect also
makes a call to EnableWindow to disable the window handle that has been
passed as the third parameter. If this handle is not the handle to the
parent, then the DialogBoxIndirect function will find the parent and then
disable it.


Conclusion

Dynamic dialog boxes are not significantly different from their static
counterparts. However, the ability to create them on an as-needed basis is
a very desirable feature that provides extra flexibility. After learning
to create the dialog table that the two new functions require and
understanding what the functions provided in DLGTEMP.C do, you have all
the tools necessary to begin building applications that include dialog
boxes built on the fly.


Figure 2:  Header File

/* DLGBOX.H-Header File */

#define IDM_MODAL      100
#define IDM_MODELESS   110
#define IDM_ONE        120

#define IDDEDIT        3025
#define IDCHECK        3026

/* Control class codes */

#define BUTTONCLASS    0x80 /* 128 */
#define EDITCLASS      0x81 /* 129 */
#define STATICCLASS    0x82 /* 130 */
#define LISTBOXCLASS   0x83 /* 131 */
#define SCROLLBARCLASS 0x84 /* 132 */

typedef struct DialogHeader{
   long  dtStyle;
   BYTE  dtItemCount;
   int   dtX;
   int   dtY;
   int   dtCX;
   int   dtCY;
} DLGHDR;

/* The following 3 header items are variable strings so not part
of DLGHDR */

/*   char dtResourceName[]; */
/*   char dtClassName[];    */
/*   char dtCaptionText[];  */

typedef struct DialogItem {
   int   dtilX;
   int   dtilY;
   int   dtilCX;
   int   dtilCY;
   int   dtilID;
   long  dtilStyle;
   BYTE  dtilControlClass;
} DLGITEM;

/* The following control items are variable so not part of DLGITEM */

/* char dtilText[]; */
/* BYTE dtilInfo;   */

#define  DI0   0L
#define  DI1   1L
#define  DI2   2L
#define  DI3   3L
#define  DI4   4L
#define  DI5   5L
#define  DI6   6L
#define  DI7   7L
#define  DI8   8L
#define  DI9   9L
#define  DI10  10L
#define  DI11  11L
#define  DI12  12L
#define  DI13  13L
#define  DI14  14L
#define  DI15  15L
#define  DI16  16L
#define  DI17  17L
#define  DI18  18L
#define  DI19  19L
#define  DI20  20L
#define  DI21  21L


Figure 3:  DLGBOX.C Code Fragments

/* DLGBOX.C ─ version 1.00 Demonstrates how to use the DialogBoxIndirect
              and CreateDialogIndirect functions.
*/

#include <windows.h>
#include "dlgbox.h"

/* List of functions used in this module. */
BOOL FAR PASCAL InitDiabox (HANDLE , HANDLE , int);
LONG FAR PASCAL DlgboxWndProc  (HANDLE, unsigned, WORD, LONG);
BOOL FAR PASCAL DialogBoxWindowProc (HANDLE, unsigned, WORD, LONG);
BOOL     BuildDialog(HWND);

/* The following functions are found in the DLGTEMP module.
   They are used to build the dialog structure that will be
   passed to the DialogBoxIndirect or CreateDialogIndirect function.
 */

                               ∙
                               ∙
                               ∙

/* ────── parent's window procedure ────── */

LONG FAR PASCAL DlgboxWndProc( hWnd, message, wParam, lParam )
HWND     hWnd;
unsigned message;
WORD     wParam;
LONG     lParam;

{

PAINTSTRUCT ps;
int   iResult;

switch (message) {
   case WM_COMMAND:
      switch (wParam) {

        case IDM_MODAL:

           if ( hDlgTest == NULL ) {/* allow only one dialog up at a time */
             nModeless = FALSE;
             if ( BuildDialog(hWnd) == TRUE) {
               lpDlgTest = MakeProcInstance(
                   (FARPROC)DialogBoxWindowProc,hInst);
               if (lpDlgTest == NULL) {
                 MessageBox(GetFocus(),
                    "MakeProcInstance failed!", "ERROR", MB_OK );
                 GlobalFree( hDlgTemp );
                 return FALSE;
               }
               iResult = DialogBoxIndirect(hInst,hDlgTemp,hWnd,lpDlgTest);
             }
           }
           break;

        case IDM_MODELESS:

           if (hDlgTest == NULL) {/* allow only one dialog up at a time */
             nModeless = TRUE;
             if ( BuildDialog(hWnd) == TRUE) {
               lpDTemplate = GlobalLock( hDlgTemp );
               if (lpDTemplate == NULL) {
                 MessageBox( GetFocus(), "Could not lock memory.",
                    "Diabox", MB_OK );
                 GlobalFree( hDlgTemp );
                 return FALSE;
               }
               lpDlgTest = MakeProcInstance( (FARPROC) DialogBoxWindowProc, h

               if (lpDlgTest == NULL) {
                 MessageBox( GetFocus(), "MakeProcInstance failed!",
                    "ERROR", MB_OK );
                 GlobalFree( hDlgTemp );
                 return FALSE;
               }
               hDlgTest = CreateDialogIndirect( hInst, lpDTemplate, hWnd, lpD
             }  /* exit if BuildDialog didn't work */
           }
           break;

        default:
           return DefWindowProc(hWnd, message, wParam, lParam);
           break;
      }

   case WM_PAINT:
      BeginPaint(hWnd, (LPPAINTSTRUCT)&ps);
      EndPaint  (hWnd, (LPPAINTSTRUCT)&ps);
      break;

   case WM_DESTROY:
      FreeProcInstance ((FARPROC)lpDlgTest);
      PostQuitMessage(0);
      break;

   default:
      return(DefWindowProc(hWnd, message, wParam, lParam));
}

return(0L);

}

/* ────── Build dialog routine ───────────── */
/*
 *  This routine passes all the necessary information to the
 *  dialog functions in DLGTEMP.C that will build the dialog table.
 */

BOOL BuildDialog(hWnd)
HWND  hWnd;

{

BOOL    bResult;
HCURSOR hOldCursor;      /* Handle to old mouse cursor  */

/* Build the Dialog header */

bResult = CreateDialogHeader(
  WS_BORDER  | WS_CAPTION | WS_DLGFRAME | WS_VSCROLL |
  WS_HSCROLL | WS_SYSMENU | WS_GROUP    | WS_TABSTOP  |
  WS_SIZEBOX | WS_VISIBLE | WS_POPUP, /* window style  */
  (BYTE)12, 24, 12, 180, 160,         /* coordinates of Dialog box.*/
  "dlgtemp",                          /* menu          */
  "",                                 /* class name    */
  "A test Dialog Box" );              /* Caption bar   */

if (bResult == FALSE) {
  MessageBox( GetFocus(), "Not enough memory.","Diabox", MB_OK );
  return FALSE;
}

hOldCursor = SetCursor( LoadCursor(NULL, IDC_WAIT) );

/* Each call to CreateDialogItem is an item to be placed inside the
   dialog box */
/* This next comment line is the value that is being passed       */
/*     id,  style, class, x,   y, cx, cy,      text, extrabytes   */

CreateDialogItem( IDCHECK, DI0,   0,  60,  90, 45, 12,  "&Ck box",0x00 );/* c
CreateDialogItem( 3001,    DI1,   0,  44, 107,  0,  0,  "dlgicon",0x00 );/* i
CreateDialogItem( 3002,    DI2,   0,  73, 107, 16, 17,         "",0x00 );/* b
CreateDialogItem( 3003,    DI3,   0,   6, 107, 25, 15,         "",0x00 );/* r
CreateDialogItem( 3004,    DI4,   0,   6, 132, 40, 10,"L Justify",0x00 );/* l
CreateDialogItem( 3005,    DI5,   0, 122,  64, 40, 38,   "M Edit",0x00 );/* m
CreateDialogItem( 3006,    DI6,   0,  60,   8, 54, 32,       "lb",0x00 );/* l
CreateDialogItem( 3007,    DI7,   0,  43,   8,  8, 34,      "v s",0x00 );/* v
CreateDialogItem( 3008,    DI8,   0,   6,  47, 47,  8,      "h s",0x00 );/* h
CreateDialogItem( 3009,    DI9,   0,   6,   8, 32, 36,    "group",0x00 );/* g
CreateDialogItem( IDCANCEL,DI10,  0, 102, 112, 24, 14,    "&Push",0x00 );/* p
CreateDialogItem( 3011,    DI11,  0,   6,  61, 45, 12,   "&radio",0x00 );/* r
CreateDialogItem( IDOK,    DI12,  0, 136, 112, 24, 14,      "&Ok",0x00 );/* d
CreateDialogItem( 3013,    DI13,  0,   6,  90, 44, 12,"&L Ck box",0x00 );/* l
CreateDialogItem( 3014,    DI14,  0,   6,  76, 30, 12,   "&3auto",0x00 );/* 3
CreateDialogItem( 3015,    DI15,  0, 122,  27, 40, 12,   "C Edit",0x00 );/* e
CreateDialogItem( 3016,    DI16,  0, 122,  44, 40, 12,   "R Edit",0x00 );/* r
CreateDialogItem( IDDEDIT, DI17,  0, 122,   8, 40, 12,   "L Edit",0x00 );/* L
CreateDialogItem( 3018,    DI18,  0,  60,  45, 54, 32,  "no sort",0x00 );/* l
CreateDialogItem( 3019,    DI19,  0,  66, 132, 50, 10,   "Center",0x00 );/* C
CreateDialogItem( 3020,    DI20,  0, 122, 132, 46, 10,"R Justify",0x00 );/* R

SetCursor( hOldCursor );

/* Get the handle to the new dialog table */

hDlgTemp = EndDialogHeader();  /* end dialog template, return hDlgTemp. */
if (hDlgTemp == NULL) {
  GlobalFree(hDlgTemp);
  MessageBox( GetFocus(), "End dialog routine.", "Error!", MB_OK );
  return FALSE;
}
return TRUE;
}

                          ∙
                          ∙
                          ∙


Figure 4:  DLGTEMP.C Code Fragments

/* DLGTEMP.C ─ version 1.00
              This module supplies the necessary functions
              used to create a dialog box template in global
              memory and to pass the handle on to either the
              DialogBoxIndirect or CreateDialogIndirect functions.
 */

#include <windows.h>
#include "dlgbox.h"

/*  forward references */
WORD   lstrlen( LPSTR );
void   SetStyleClass(int);

HANDLE   hDlgTemplate; /* Handle to current dialog template memory   */
WORD     wOffset;      /* Current memory offset (updated by CDH & CDI) */
BYTE     iItems;       /* number of items in dialog */
DLGITEM  DlgItem;      /* Dialog item structure */

/* ────── Create Dialog Header ─────────────*/
*/
 *  This routine allocates a piece of global memory
 *  and then fills in the dialog header structure and saves the
 *  information in global memory.
 */

BOOL FAR PASCAL CreateDialogHeader( Style, ItemCount, X, Y, cX, cY,
                                    Resource, Class, Caption)
LONG  Style;     /* Dialog box Style              */
BYTE  ItemCount; /* Control count for dialog box  */
int   X;         /* Dialog box top left column    */
int   Y;         /* Dialog box top row            */
int   cX;        /* Dialog box width              */
int   cY;        /* Dialog box height             */
LPSTR Resource;  /* Dialog box resource string    */
LPSTR Class;     /* Dialog box class string       */
LPSTR Caption;   /* Dialog box caption            */

                        ∙
                        ∙
                        ∙

* ────── Create Dialog Item ─────────────*/
*/
 *  This routine fills in the dialog item structure and
 *  saves the information in global memory after resizing it.
 */

BOOL FAR PASCAL CreateDialogItem( iCtrlID, lStyle, Class,
                                  X, Y, cX, cY, Text, ExtraBytes )
int    iCtrlID;      /* Control ID                */
LONG   lStyle;       /* Control style             */
BYTE   Class;        /* Control class             */
int    X;            /* Control top left column   */
int    Y;            /* Control top row           */
int    cX;           /* Control width             */
int    cY;           /* Control height            */
LPSTR  Text;         /* Control text              */
BYTE   ExtraBytes;   /* Control extra bytes count */

                        ∙
                        ∙
                        ∙

/* ────── End Dialog Header ─────────────── */
*/
 *  This routine changes the number of items in the
 *  dialog header and then returns a handle to the global memory.
 */

HANDLE FAR PASCAL EndDialogHeader()  /* end dialog header function */

                         ∙
                         ∙
                         ∙

/* ────── List of predefined dialog items ───────── */
*/
 *  This routine defines a list of predefined dialog items.
 *  This allows the user to just pass a number to get the
 *  attributes of a pre-defined control.
 *  You can add to this list or modify this to best fit your needs.
 *
 *  Warning: If you change one of the styles, it could affect
 *           other controls that expect the old behavior.
 */

void SetStyleClass(iStyle)
int  iStyle;
{
/*
   if an iStyle is given then dtilControlClass will take on the default
   value.
*/

switch ( iStyle ) {
   case DI0:     /* check box */
          DlgItem.dtilControlClass = BUTTONCLASS ;
          DlgItem.dtilStyle = BS_CHECKBOX | WS_TABSTOP |WS_CHILD |
                              WS_VISIBLE ; break;
   case DI1:     /* icon */
          DlgItem.dtilControlClass = STATICCLASS ;
          DlgItem.dtilStyle = SS_ICON | WS_BORDER |WS_CHILD |WS_VISIBLE ;
          break;
   case DI2:     /* black box */
          DlgItem.dtilControlClass = STATICCLASS ;
          DlgItem.dtilStyle = SS_BLACKRECT |WS_CHILD |WS_VISIBLE ; break;
   case DI3:     /* rectangle */
          DlgItem.dtilControlClass = STATICCLASS ;
          DlgItem.dtilStyle = SS_BLACKFRAME |WS_CHILD |WS_VISIBLE ; break;
   case DI4:     /* left static text */
          DlgItem.dtilControlClass = STATICCLASS ;
          DlgItem.dtilStyle = SS_LEFT |WS_CHILD |WS_VISIBLE ; break;
   case DI5:     /* multiline edit box */
          DlgItem.dtilControlClass = EDITCLASS ;
          DlgItem.dtilStyle = ES_LEFT | ES_MULTILINE | ES_NOHIDESEL |
                              ES_AUTOVSCROLL | ES_AUTOHSCROLL |
                              WS_VSCROLL | WS_HSCROLL | WS_BORDER |
                              WS_TABSTOP |WS_CHILD |WS_VISIBLE ; break;
   case DI6:     /* list box - sorted */
          DlgItem.dtilControlClass = LISTBOXCLASS ;
          DlgItem.dtilStyle = LBS_STANDARD |WS_CHILD |WS_VISIBLE ; break;
   case DI7:     /* vertical scrollbar */
          DlgItem.dtilControlClass = SCROLLBARCLASS ;
          DlgItem.dtilStyle = SBS_VERT |WS_CHILD |WS_VISIBLE ; break;
   case DI8:     /* horizontal scrollbar */
          DlgItem.dtilControlClass = SCROLLBARCLASS ;
          DlgItem.dtilStyle = SBS_HORZ | WS_CHILD |WS_VISIBLE ; break;
   case DI9:     /* group box */
          DlgItem.dtilControlClass = BUTTONCLASS ;
          DlgItem.dtilStyle = BS_GROUPBOX | WS_TABSTOP |WS_CHILD |
                              WS_VISIBLE ; break;
   case DI10:    /* Push button */
          DlgItem.dtilControlClass = BUTTONCLASS ;
          DlgItem.dtilStyle = BS_PUSHBUTTON | WS_TABSTOP |WS_CHILD |
                              WS_VISIBLE ; break;
   case DI11:    /* radio button */
          DlgItem.dtilControlClass = BUTTONCLASS ;
          DlgItem.dtilStyle = BS_RADIOBUTTON | WS_TABSTOP |WS_CHILD |
                              WS_VISIBLE ; break;
   case DI12:    /* default push button */
          DlgItem.dtilControlClass = BUTTONCLASS ;
          DlgItem.dtilStyle = BS_DEFPUSHBUTTON |WS_TABSTOP |WS_CHILD |
                              WS_VISIBLE ; break;
   case DI13:    /* left check box */
          DlgItem.dtilControlClass = BUTTONCLASS ;
          DlgItem.dtilStyle = BS_LEFTTEXT |BS_CHECKBOX |WS_TABSTOP |
                              WS_CHILD |WS_VISIBLE; break;
   case DI14:    /* 3 auto state button */
          DlgItem.dtilControlClass = BUTTONCLASS ;
          DlgItem.dtilStyle = BS_AUTO3STATE |WS_TABSTOP |WS_CHILD |
                              WS_VISIBLE ; break;
   case DI15:    /* centered edit control */
          DlgItem.dtilControlClass = EDITCLASS ;
          DlgItem.dtilStyle = ES_CENTER | ES_MULTILINE | WS_BORDER |
                              WS_TABSTOP | WS_CHILD | WS_VISIBLE ; break;
   case DI16:    /* right edit control */
          DlgItem.dtilControlClass = EDITCLASS ;
          DlgItem.dtilStyle = ES_RIGHT | ES_MULTILINE | WS_BORDER |
                              WS_TABSTOP | WS_CHILD | WS_VISIBLE ; break;
   case DI17:    /* left edit control */
          DlgItem.dtilControlClass = EDITCLASS ;
          DlgItem.dtilStyle = ES_LEFT  | WS_BORDER | WS_TABSTOP |
                              WS_CHILD | WS_VISIBLE ; break;
   case DI18:    /* listbox w/out sort */
          DlgItem.dtilControlClass = LISTBOXCLASS ;
          DlgItem.dtilStyle = LBS_NOTIFY | WS_BORDER | WS_VSCROLL |
                              WS_CHILD   | WS_VISIBLE ; break;
   case DI19:    /* center static text */
          DlgItem.dtilControlClass = STATICCLASS ;
          DlgItem.dtilStyle = SS_CENTER |WS_BORDER |WS_CHILD |WS_VISIBLE ;
          break;
   case DI20:    /* right static text */
          DlgItem.dtilControlClass = STATICCLASS ;
          DlgItem.dtilStyle = SS_RIGHT | WS_CHILD | WS_VISIBLE ; break;
   default:      /* left edit control */
          DlgItem.dtilControlClass = STATICCLASS ;
          DlgItem.dtilStyle = SS_LEFT | WS_CHILD | WS_VISIBLE ; break;
} /* end switch */

}


WORD  lstrlen( lpszString )
LPSTR lpszString; /* String to check  */
{
 WORD Length; /* Length of string */

 for ( Length = 0; *lpszString++ != '\0'; Length++ );

 return( Length );
}


Figure 5:  DLGBOX.MAK is the Make Script for DLGBOX

dlgtemp.obj: dlgtemp.c  dlgbox.h
     cl  -W2 -d  -c  -AM  -Gsw  -Os -Zpe dlgtemp.c
dlgbox.obj: dlgbox.c  dlgbox.h
     cl -W2 -d  -c  -AM  -Gsw  -Os -Zpe dlgbox.c
dlgbox.res: dlgbox.rc dlgbox.h
     rc -r dlgbox.rc
dlgbox.exe:  dlgbox.obj dlgbox.def dlgbox.res dlgtemp.obj
     link4 dlgbox
dlgtemp,dlgbox/align:16,dlgbox,mlibw,dlgbox.def
     rc dlgbox.res


Figure 6:  The Resource File for DLGBOX

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

dlgicon ICON dlgbox.ico

dlgbox MENU
BEGIN
  POPUP "&DialogBox"
  BEGIN
    MENUITEM "&Modal",    IDM_MODAL
    MENUITEM "M&odeless", IDM_MODELESS
  END
END

dlgtemp MENU
BEGIN
  POPUP "&Top Menu"
  BEGIN
    MENUITEM "&One", IDM_ONE
  END
END


Figure 7:  The DialogHeader Structure

struct DialogHeader
{
    LONG       lDHStyle;
    BYTE       cDHItemCount;
    int        iDHX;
    int        iDHY;
    int        iDHCX;
    int        iDHCY;

/* The Variable part of the record starts here. */

    char        szDHResourceName[];
    char        szDHClassName[];
    char        szDHCaptionText[];
};


Figure 8:  The DialogItem Structure

struct DialogItem
{
    int       iDIX;
    int       iDIY;
    int       iDICX;
    int       iDICY;
    int       iDIID;
    LONG      lDIStyle;
    BYTE      cbDIControlClass;

/* The Variable part of the record starts here. */

    char       szDIText[];
    BYTE       cbDIExtra[];
};


Figure 9:  Predefined Control Classes

BUTTONCLASS       0x80
EDITCLASS         0x81
STATICCLASS       0x82
LISTBOXCLASS      0x83
SCROLLBARCLASS    0x84


════════════════════════════════════════════════════════════════════════════


Vol. 3 No. 6 Table of Contents

Building a Device-Independant Video Display I/O Library in Microsoft(R) C

Though memory-mapped video is an important part of PC programming, the
standard C run-time I/O library contains no specific support for the video
subsystems of the IBM PC. This article offers a generalized approach to
managing screen I/O.


Developing SQL Server Database Applications Through DB-Library

SQL Server's DB-Library provides programmers with a set of functions that
can be used to issue SQL-based commands to access local or remote databases.
This article demonstrates how to develop true distributed processing
client/server applications for SQL Server.


OS/2 Protected-Mode Programming with Forth, LISP, Modula-2, and BASIC

A variety of interpreters and compilers are now available for OS/2
protected-mode programming. We examine how several of these languages handle
function calls to the OS/2 API, data type conversion, and especially the use
of dynamic-link libraries.


The High Memory Area: Addressing 64Kb More Memory In Real Mode

It is commonly believed that in real mode, the Intel 80286 and 80386
processors can only address 1Mb of memory, but it is possible to address
almost 64Kb more. We explore the High Memory Area and its use with the
Microsoft-Intel -Lotus Extended Memory Specification.


Developing and Debugging Embedded Systems Applications

Embedded systems are ROMable computer applications that perform dedicated
control tasks, typically without an operating system. This article offers a
high-level overview of the special techniques and tools needed to build
embedded systems applications.


C Scope and Linkage: The Keys to Understanding Identifier Accessibility

Taking full advantage of the C language requires comprehending the semantics
and usage of identifiers and the attributes that can be given to them. This
article discusses scope, the visibility of an identifier in code, and
linkage, the ability to resolve identifiers regardless of scope.


Extending the Functions of the Windows Clipboard with Scrapbook+

The Windows clipboard, used to exchange data between applications, is
restricted to one item at a time with no way to save its contents.
ClickArt(R) Scrapbook+ acts as an agent for the clipboard and lets users
store, retrieve, and examine data in a variety of formats.


EDITOR'S NOTE

Nearly every book, technical document, and article written about OS/2 to
date uses C and Assembly language to provide programming examples, creating
the illusion that these are the only viable languages for OS/2. Actually,
in addition to C and Macro Assembler, Microsoft currently offers Pascal,
COBOL, and FORTRAN for OS/2.

It is interesting to note that rather than limiting the languages one can
choose, OS/2's multitasking capabilities and extensibility via dynamic-link
libraries (DLLs) are enhanced by the availability of additional languages
represented by the compilers, interpreters, and integrated programming
environments that are being ported to OS/2.

In this issue, we examine four of these languages. Modula-2, built around
the idea of the standalone module and multiple process, is a perfect match
for OS/2 and interfaces very nicely with C. Access to the OS/2 Application
Program Interface (API) fits naturally into UR/FORTH, a multitasking
threaded-code interpreter and a stack oriented language. The interactive
nature of LISP not only lets you type in OS/2 calls and immediately see
their results, but LISP also knows about dynamic linking; access to the OS/2
API is coordinated through a few functions rather than scattered across
hundreds of new ones. BASIC, traditionally requiring a run-time environment,
fits right into OS/2, where the run-time environment is simply a DDL.

The very nature of OS/2, coupled with these languages, challenges
programmers to carry out ad hoc experiments to find the right tool for
the job at hand or to create dynamic new applications.

The article about OS/2 compilers proves that we don't only talk about C.
But for our constituency of C aficionados we offer three articles in this
issue──"Building a Device-Independent VIdeo Display I/O Library in
Microsoft C;" "C Scopes and Linkage: The Keys to Understanding Identifier
Accessibility;" and "Developing and Debugging Embedded Systems
Applications."──Ed.


Masthead

JONATHAN D. LAZARUS
Editor and Publisher

EDITORIAL

TONY RIZZO
Technical Editor

KAREN STRAUSS
Assistant Editor

JOANNE STEINHART
Production Editor

GERALD CARNEY
Staff Editor

MATTHEW LIGHT
Copy Editor

KIM HOROWITZ
Editorial Assistant

ART

MICHAEL LONGACRE
Art Director

VALERIE MYERS
Associate Art Director

CARLOS BOLL
Designer

CIRCULATION

STEVEN PIPPIN
Circulation Director

WILLIAM B. GRANBERG
Circulation Manager

L. PERRIN TOMICH
Assistant to the Publisher

JAANA NIEUWBOER
Administrative Assistant

Copyright(C) 1989 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, the Microsoft logo, CodeView, MS, MS-DOS, and GW-BASIC are
registered trademarks of Microsoft Corporation. IBM, PC/AT, and PS/2 are
registered trademardk of International Business Machines Corporation.
PC/XT is a trademark of International Business Machines Corporation. Apple
and Macintosh are registered trademarks of Apple Computer, Inc. MultiFinder
is a trademark of Apple Computer, Inc. PostScript is a registered trademark
of Adobe Systems, Inc. Aldus and PageMaker are registered trademarks of
Aldus Corporation. UNIX is a registered trademark of American Telephone &
Telegraph Company. COMPAQ is a registered trademark of COMPAQ Computer
Technology. Intel is a registered trademark of Intel Corporation. Epsilon
is a trademark of Lugaru Software, Inc. Micrografx and Micrografx Designer
are registered trademarks of Micrografx, Inc. The Norton Utilities is a
trademark of Peter Norton Computing. ClickArt is a registered trademark of
T/Maker Company.

████████████████████████████████████████████████████████████████████████████

Building a Device-Independent Video Display I/O Library in Microsoft C

Jeff Prosise☼

Nearly every experienced C programmer who is developing applications for
DOS has, at some time, had to write his own set of character-mode video
functions. C's standard I/O library contains no specific support for the
memory-mapped video subsystems of the IBM(R) PC. This generic quality of C
preserves its portability across different operating systems and
different machines.

However, memory-mapped video is an inseparable component of PC
programming. The outdated teletype interface has given way to carefully
crafted color screens that distinguish microcomputer user interfaces from
those of their mainframe cousins and are a mark of well-designed
application programs.

The professional C developer must have at his disposal a suite of high-
performance video I/O routines. OS/2 provides these services through its
VIO class of functions. For DOS developers, the Microsoft(R) C run-time
library offers a large-model graphics package containing a powerful set of
functions for character-mode video output, but it is neither as robust
nor as concise in its syntax as its OS/2 counterpart. Generalized
programming libraries rarely include everything you need for the task
at hand, but a customized library can be tailored to meet specific needs,
especially when core routines can be developed in assembly language.

This article will lay the foundations for a high-performance, device-
independent video I/O library and present a demonstration program that
illustrates its use. The development platform is DOS and the Microsoft C
Compiler Versions 5.0 and 5.1. When possible, functions are written in C;
others are coded in assembly language using the Microsoft Macro Assembler
(MASM) Versions 5.0 and 5.1.


Design Criteria

Performance and device independence are the two most important criteria
in the design of video functions. Performance is the raw measure of the
functions' speed of execution; device-independence is the measure of
their ability to insulate the programmer from the video hardware.

Speed is critical in the operation of video functions. Video output
using BIOS routines does not compare with the instantaneous screen
updates obtained when characters and attributes are written directly to
the video buffer. The video BIOS writes directly to the buffer, but
limits its access to the relatively infrequent intervals when the
scanning electron beam is not displaying data. As a result, high
performance can't be achieved with its inefficient character I/O
services.

But performance doesn't come without a price. When a programmer forsakes
the BIOS, he also abandons the layer of firmware that shields him from the
nuances of individual video adapters. Although most adapters can be
treated the same in character mode once the color boards are sorted from
the monochrome, certain architectures require special consideration.
The design of IBM's Color Graphics Adapter (CGA), which does nothing to
arbitrate requests for access to video RAM arriving simultaneously from
the CPU and character-generating hardware, is a striking example.

In the context of an all-around video function library, device
independence means more than simply running on any adapter; it also means
adapting to the different environments that the video adapters offer.
Programs could once count on 25 lines of text per screen, but modern
adapters permit more. A program must be able to assess its environment in
terms of video mode, number of displayed rows and columns, monochrome or
color, and other modals. Developing a common set of routines to provide
this service eliminates the needless duplication of code when new
applications are put together.


Device Independence

When IBM introduced the PC in 1981, it offered buyers a choice of two
video adapters: the Monochrome Display Adapter (MDA) and the Color
Graphics Adapter (CGA). The MDA featured sharp, readable text with
characters formed from a 9-by-14-character matrix, but lacked graphics
capability. The CGA offered three dot-addressable color graphics modes
but could only display text by using a fuzzy 8-by-8-character matrix. As a
result, the majority of users who dealt primarily with text opted for a
monochrome display and adapter.

Both the MDA and the CGA were built around the Motorola 6845 CRT
Controller (CRTC). The 6845 is a highly programmable device designed to
drive raster scan displays in a variety of modes and configurations. Since
the two video adapters shared a common CRTC, their architectures were very
similar. Both organized character-mode video memory as an interleaved
sequence of ASCII characters and attribute bytes. IBM located the MDA
video buffer at segment address B000H and the CGA buffer at B800H so that
one system could accommodate both adapters simultaneously. Each was
assigned unique and nonconflicting I/O addresses; however, only one video
subsystem could be active at once.

Recognizing that the middle ground between the CGA and MDA was wide open,
in 1982 Hercules introduced the Hercules(R) Graphics Card (HGC). Based on
the same CRTC as its IBM predecessors, the HGC offered MDA compatibility
plus dot-addressable graphics on a monochrome display. For all practical
purposes, an HGC running in text mode was identical to an MDA.
Applications written especially for the HGC, however, enjoyed graphics
capabilities previously available only on more expensive color
systems.

IBM countered with the Enhanced Graphics Adapter (EGA) in 1985. The EGA
redefined the state of the art in video adapters by blending a crisp text
mode with extensive color graphics support and the ability to produce
graphics on a monochrome display. Although its designers gave up the 6845
in favor of a custom CRTC chip, the EGA retained a high degree of
compatibility with the CGA and MDA.

A distinguishing feature of the EGA was its ability to hardware-relocate
its video buffer and I/O port addresses to emulate a CGA or an MDA. An
EGA connected to a monochrome display located its video buffer at B000H.
The same adapter paired with a color display mapped its video buffer to
segment B800H. A properly configured EGA could be placed into a PC
alongside an MDA to drive the color half of a dual monitor system or next
to a CGA to drive the monochrome half.

Another feature of the EGA was its ability to display more than 25 lines
of text on a single screen. Both the CGA and MDA were limited to 25, but
the EGA could be reprogrammed to display almost any number of rows
permitted by its 350 scan lines of resolution. Programs were suddenly
confronted with a new variable in the video environment. Because the EGA
BIOS offered no direct support in helping a program determine the display
density, a new level of hardware dependence was established.


MCGA and VGA

The most recent advance in mainstream video hardware came from IBM in the
form of the PS/2(R) Multi-Color Graphics Array (MCGA) and Video Graphics
Array (VGA). Both are functionally similar to the EGA, but unlike earlier
IBM adapters, the MCGA and VGA generate analog RGB signals capable of
driving analog color and monochrome monitors. Since an analog monochrome
monitor can display up to 64 shades of gray using essentially the same
signal as a color monitor, the MCGA and VGA are capable of driving both
at once. What appears on one screen in color will appear in black-and-
white on the other.

On an EGA, the video modes available depended on the type of display
attached. Modes 7 and 15 were reserved for monochrome. The VGA is capable
of switching to any supported mode, monochrome or color, regardless of the
monitor. Both the MCGA and the VGA employ a gray-scale summing algorithm
that enables black-and-white displays to be treated as color and vice
versa. A VGA running in mode 7 or 15 will locate its video buffer and I/O
ports in monochrome address space, while a VGA in any other mode will
configure itself as a color adapter.

From the standpoint of a program wanting to access the video buffer for
high-speed screen updates, all of the adapters obey a common set of rules.
Monochrome video memory is always found at B000H, color at B800H.
Determining whether an adapter is configured for color or monochrome
is as simple as checking the word at 0040:0063 in the BIOS Data Area,
which holds the I/O port address of the CRTC. A value of 3B4H indicates
the system is monochrome──either an MDA, an HGC, an EGA connected to a
monochrome display, or a VGA running in mode 7 or 15-while 3D4H identifies
a color system. If more than one adapter is installed, the CRTC address
reflects the mode the system is currently running in.

If it weren't for the CGA, a program's concerns for device independence
would begin and end with the location of the video buffer. However, the
CGA poses an additional problem: when a program tries to read from or
write to the video buffer in text mode, the CPU must contend with the
adapter's display refresh circuitry for access to video RAM. Since the CPU
is given priority, an ugly, snowlike interference results.

A program can sidestep this interference problem in one of three ways: by
blanking the screen before accessing video memory, by limiting memory
accesses to the vertical blanking interval, or by limiting them to the
horizontal blanking interval. Blanking intervals are the brief periods of
time when the electron beam scanning the CRT screen is inactive while
moving from the end of one display row to the beginning of the next or
from the bottom of the screen to the top. These movements are called
retraces, and blanking intervals are often synonymously referred to as
retrace intervals.

Each method is best suited for a particular situation. The video BIOS
disables video output entirely-effectively blanking the display──when it
scrolls the screen, producing a brief but perceptible flicker. It uses
horizontal blanking intervals to move single character/attribute pairs
to or from the video buffer. Using horizontal retraces is preferable to
using vertical retraces because CGA horizontals occur 200 times per
refresh frame, while verticals occur only once.

To synchronize video memory accesses with vertical or horizontal
retraces, an application uses the CRT Status Register, located at I/O
address 3DAH on a CGA. Two bits in the CRT Status Register reflect the
status of the moving electron beam: bit 0 for the horizontal retrace and
bit 3 for the vertical. A high bit means a retrace is in progress. By
monitoring the desired bit and looping until its polarity changes from low
to high, a program can detect the beginning of a blanking interval and
update the video buffer at a time when the CPU will have uncontested
access to video RAM.

Video routines that manipulate the contents of the video buffer without
the help of the BIOS must be sensitive to the environment they're running
in. Video RAM accesses must be timed to occur during nondisplay periods
in the refresh cycle on adapters that lack dual-ported RAM or arbitration
circuitry. Reads and writes must be directed to the proper video buffer
address. All of this must be made transparent through a set of function
protocols that eliminates the need for the calling program to have any
intimate contact with the video hardware.


Performance

If the CGA were the only adapter whose BIOS synchronized video RAM
accesses with the horizontal retrace, C video functions might simply rely
on the BIOS for character I/O. But in the design of the original PC, the
video BIOS was part of the larger ROM BIOS that provided a variety of
system-related services. The same performance penalty imposed on systems
equipped with a CGA was imposed on monochrome systems as well. Even the
EGA, which packed its own on-board video BIOS, needlessly restricted
video buffer activity to the horizontal blanking interval.

The BIOS' blind insistence on using the horizontal retrace isn't all that
slows it down. The original PC BIOS offered I/O routines for individual
characters and attributes only, and nothing for strings. When a string is
output a character at a time, the video buffer address must be generated
separately for each individual character. An efficient string output
routine eliminates redundancy by calculating the address corresponding
to the first character in the string, then offsetting that address for
succeeding characters. Also, with character-oriented operations, the
cursor must be repositioned before every I/O call. Overhead is increased
while performance is markedly decreased.

A true high-performance, device-independent video library must bypass the
video BIOS. Its functions must also distinguish between CGA and non-CGA
adapters and treat them differently. The best strategy is to move
character data into and out of the video buffer during the horizontal
blanking interval on a CGA and to read and write in an uninhibited manner
to all other adapters.

The adapters are easily enough identified; the trick lies in dealing
properly with the CRT Status Register when a CGA is detected. Because of
the stack overhead and compiler inefficiency involved in executing and
returning from a C function call, a C program cannot act in the short
amount of time after the Status Register's horizontal retrace bit goes
high but before the horizontal retrace is completed.

A CGA horizontal retrace lasts for approximately 7 microseconds. On a
4.77-MHz PC, that's enough time to move one 16-bit word to or from the
video buffer. Figure 1 shows the assembly code needed to write a word to
the buffer. It assumes that, on entry, AX holds the word to be written.

In actuality, only every other horizontal retrace is used. The first loop
waits for the CRT Status Register's rightmost bit to go low to eliminate
the possibility of entering the thread at the exact moment when a
retrace is in progress but almost over. In most instances, the first
comparison will fall right through. The second loop waits for the bit
to go high, indicating that a horizontal retrace has begun. The
instruction STOSW transfers the data. Interrupts are disabled to prevent
an external event from interrupting this time-critical sequence.

The instruction used to retrieve the word that was originally stored in
AX from CX just before the move:

  XCHG AX,CX

was not chosen arbitrarily. As Richard Wilton points out in his book,
Programmer's Guide to PC and PS/2 Video Systems (Microsoft Press, 1987),
using a MOV instruction in the same place on a 4.77-MHz 8088-based
machine will produce snow. The reason: the 1-byte XCHG and STOSW
instructions will fit together with the instruction immediately
preceding them in the 8088's 4-byte instruction prefetch queue; MOV and
STOSW will not. The delay resulting from an intervening RAM access is
just enough to cause the code to overshoot the horizontal blanking
interval.

Were it not for the severe time constraints placed on this routine, the
same end could be achieved with the snippet of C code shown in Figure 2,
but implementing this on a CGA-equipped system will produce snow. Why?
Because there are so many machine code instructions executed after the
Status Register is read, the horizontal retrace has long since ended when
the word is finally moved.

Figure 3 shows the unoptimized code Microsoft C 5.0 generates for the
line "while ((inp(0x3DA) & 0x01) == 0);", with the code for the inp
function inserted and labels renamed for clarity. After the Status
Register is read with an IN instruction, eight more instructions
consuming 65 clock cycles on an 8088 (equivalent to almost 14 microseconds
at 4.77 MHz) are executed before the code that transfers the data is
reached. Even if we're lucky enough to have caught the very beginning of
the blanking interval, we have no chance of getting anything done before
it's over.

The solution is to resort to assembly language when speed is of the
essence. The majority of library functions can be implemented in C if a
small kernel of fast and efficient assembler routines is available for
time-critical operations.


Source Modules

The video functions presented here are divided between two source files,
CVIDEO.C and ASMVIDEO.ASM. Figure 4 shows CVIDEO.C, which is a collection
of C functions. ASMVIDEO.ASM, shown in Figure 5, supplies a set of
assembly language functions compatible with small-model C programs. Both
are designed to be compiled or assembled into independent OBJ files and
linked with a primary C program module. ASMVIDEO.ASM requires the use of
MASM 5.0 or later, as it takes advantage of Version 5.0 and 5.1's
simplified segment directives for interfacing MASM-generated code with
high-level languages.

The source code for DEMO.C, a demonstration program, appears in Figure 6.
DEMO.C uses functions from the video library to clear the screen, fill it
with text, and open and close a series of windows. DEMO runs equally well
on any type of video hardware. If you are using a monochrome monitor, the
program will automatically adjust its display attributes to accommodate
it.

Instructions for compiling CVIDEO.C and DEMO.C, assembling ASMVIDEO.ASM,
and linking the three together appear in Figure 7v3n6a1f7.

MASM is invoked by using the /ML command line switch to conform to C
conventions where names are case-sensitive. For the same reason, LINK is
run with the parameter /NOI.


Video Functions

A summary of the nine video functions and their calling conventions
appears in Figure 8. The character and string I/O functions distinctly
resemble OS/2's VIO functions. DispString, for instance, is similar to
OS/2's VioWrtCharStrAtt. Both eliminate the need to position a logical
cursor by taking a row and column address as arguments, both accept a
string address and attribute byte (or the address of an attribute byte),
and both require an auxiliary parameter fundamental to their operation.
With VioWrtCharStrAtt, it's a Vio handle; with DispString, it's a pointer
to a structure. As you'll see in a moment, this structure plays a vital
role in video function performance.

Three functions are provided to manipulate a specific region of the screen
without disturbing what surrounds it: ClrRegion, SaveRegion, and
RestRegion. ClrRegion clears a region, SaveRegion copies the contents of a
region to a buffer in word-sized chunks, and RestRegion complements
SaveRegion by restoring a region whose contents were previously saved
with SaveRegion. A related function, ClrScr, clears the entire viewing
area, regardless of the number of rows and columns displayed. TextBox is
thrown in to draw borders surrounding screen regions, which is handy if
you're using SaveRegion and RestRegion for windowing support.

The Region functions take the parameters row1, col1, row2, and col2 as
input. Row1 and col1 represent the row and column of the upper left corner
of the region, while row2 and col2 give the screen coordinates of the
lower right corner. SaveRegion and RestRegion also accept the address of
an integer array where screen contents are stored. VideoAttr is a
standard attribute byte, where the upper nibble represents background
color and the lower nibble represents foreground color. Screen coordinates
are zero-based in all functions that are passed row and column
parameters.

Two functions are provided for the display of individual
character/attribute pairs and strings. DispCharAttr and DispString are
implemented in assembly language for performance reasons; both write
directly to the video buffer for fast display speeds.

GetCharAttr returns an integer representing the character and attribute
at a specified row and column address. GetCharAttr is analogous to the
OS/2 VioReadCellStr function with a buffer length of 1. Like DispCharAttr
and DispString, GetCharAttr is written in assembly language and accesses
the video buffer directly. On a CGA, GetCharAttr synchronizes itself with
the horizontal retrace.

GetVideoParms must be called before any other video function. It is passed
a pointer to a structure of type VideoInfo, defined in the header file
VIDEO.H as follows:

  struct VideoInfo {
    unsigned char mode;
    unsigned char rows;
    unsigned char columns;
    unsigned char ColorFlag;
    unsigned char SyncFlag;
    unsigned int BufferStart;
    unsigned int SegAddr;
  };

The byte-sized variable mode represents the current video mode. Rows holds
the number of displayed screen rows, and columns holds the number of
characters per row. ColorFlag is set to 0 on a monochrome system or 1 on
a color system. SyncFlag returns 0 for all adapters except a CGA. SegAddr
holds the current video buffer segment address, and BufferStart holds the
offset into the video buffer segment of the active video page.

Any of these parameters can be used directly by the calling program to
make critical determinations about the video environment. If a
program finds it's running in a 43-line mode, for example, it may choose
to take advantage of all 43 rows rather than limiting itself to the first
25. Also it may use ColorFlag to adjust video attributes for monochrome
or color displays. Inserting a piece of code at the beginning of a program
similar to the one below will make its display functions independent of
display type:

  GetVideoParms(&video);
  if (video.ColorFlag) {
    color1 = 0x1F;
    color2 = 0x1E;
  }
  else {
    color1 = 0x07;
    color2 = 0x70;
  }

If the video environment is color, the variables color1 and color2 are
assigned color attributes; if monochrome, they're assigned the more
generic values 0x07 and 0x70. However, GetVideoParms and the VideoInfo
structure have an even greater significance: all of the remaining eight
functions except one, ClrRegion, are passed a pointer to the structure
initialized by GetVideoParms.

ClrScr has to know how many rows and columns are displayed to clear the
screen; VideoInfo supplies those parameters. DispCharAttr, DispString,
and GetCharAttr, the three functions that move data between conventional
RAM and video RAM, use SyncFlag to determine whether or not to
synchronize buffer accesses with the horizontal blanking interval.
SegAddr and BufferStart are used to determine the segment:offset address
of the active video page, and columns is used to calculate addresses
within a page. In short, the VideoInfo structure offers a mechanism for
supplying commonly needed information to a variety of otherwise
independent routines.

GetVideoParms normally needs to be called only once in the lifetime of a
program. The one possible exception to this rule is when it is used in a
TSR environment. Since most memory-resident programs can be triggered by
an asynchronous external event like a keystroke or timer interrupt, the
video environments they inherit may be different each time. The solution
is to call GetVideoParms to reinitialize the VideoInfo structure each
time your TSR is brought to the foreground.


Demonstration Program

DEMO is a short demonstration program that is designed to run in any 80-
column text mode that illustrates the proper techniques for using the
video I/O routines presented in this article. It includes the header file
VIDEO.H, which contains the VideoInfo structure definition and function
prototypes for all library routines. The listing for VIDEO.H is shown in
Figure 9.

DEMO begins by declaring the structure video to be of type VideoInfo and
passing GetVideoParms a pointer to it. The structure is initialized, and
video.ColorFlag is checked to determine whether the current video
environment is monochrome or color. The attributes WinColor1, WinColor2,
and WinColor3 are set accordingly.

The screen is cleared and filled from top to bottom with text. Rather than
assuming 25 rows are displayed, the program uses video.rows to determine
how many times DispString should be called. Then three windows are opened,
one after another, with a brief pause between each one. SaveRegion copies
the underlying contents of each region to a buffer, ClrRegion clears the
region, TextBox borders it, and DispString fills it. The windows are
closed in the reverse order in which they were opened with RestRegion.
Finally, the screen is recleared and control handed back to DOS.

Once GetVideoParms is invoked, DEMO intrinsically has all the information
that it needs to handle its assorted video tasks. It doesn't need to know
what sort of video adapter it's running on, how many adapters might be
installed, or whether the adapter needs the special kind of treatment
accorded a CGA. It doesn't have to hunt around to find out whether it's
running on a monochrome or color display, and the dimensions of the
screen are immediately available to it. The programmer is left free to
concentrate on the program's design and functionality and isn't
burdened by the fact that his code may be used on any of a number of
different pieces of video hardware.


Video Parameters

The process GetVideoParms must go through to determine what type of video
hardware it's running on and establish resultant video parameters merits
a closer look. It begins by calling Function 12H of Interrupt 10H with BL
set to 10H. If an EGA, VGA, or MCGA is installed, the BL register will
return with a value other than the 10H it held on entry. If BL returns
unchanged, GetVideoParms assumes either a CGA, MDA, or HGC is present and
active and tests for a CGA by checking the CRTC address held in the BIOS
Data Area. If the CRTC address corresponds to that of a color adapter,
GetVideoParms concludes it has just found a CGA and sets SyncFlag to 1.

If the Function 10H test indicates an EGA or equivalent is installed,
GetVideoParms tests bit 3 of the byte at 0040:0087 to determine if it's
the adapter currently active, just in case more than one is installed. If
the EGA is the active adapter, SyncFlag is set to 0; if it's not and the
CRTC address indicates the video environment is monochrome, SyncFlag is
still 0; if the EGA isn't active and the CRTC address corresponds to
color, then SyncFlag is set to 1. The latter case is symptomatic of a CGA
sharing a system with an EGA configured for monochrome use.

If an EGA, VGA, or MCGA is installed and active, the number of displayed
rows is pulled from the BIOS Data Area at address 0040:0084. The number of
characters per row, the current video mode, and the offset address in
the video segment of the active video page are likewise taken from the
BIOS Data Area. ColorFlag is simply set to 0 if the CRTC address is 3B4H,
or set to 1 if it's 3D4H.

All these methods will probably remain compatible with future video
hardware. There will, we hope, be no more video adapters with the quirky
characteristics of the CGA, and since so many programs rely on the
BIOS Data Area, particularly the video portion of it, IBM has promised
to leave it intact.


MASM Functions

The three MASM functions DispCharAttr, DispString, and GetCharAttr are
designed to be used with small-model C programs but are easily adapted to
other memory models. MASM's new MODEL directive makes the changeover
relatively simple in terms of keeping naming conventions consistent with
those used by the C compiler. The only other consideration is adjusting
references to parameters passed on the stack when a memory model with far
return addresses is used.

Both DispCharAttr and DispString employ a subroutine named WriteWithWait
to write a word to the CGA video buffer. WriteWithWait is nearly identical
to the code in Figure 1. Writes are only performed during the horizontal
blanking interval. A second MASM subroutine, AddressOf, is called by all
three functions to calculate the video buffer address that corresponds to
a given row and column on the active page. Neither WriteWithWait nor
AddressOf is accessed directly by C functions, so neither is declared
public.

All three functions are passed a pointer to the VideoInfo structure. To
establish addressability, the BX register is loaded with the pointer and
all references to data within the structure are made relative to BX using
based mode addressing. Since BX and BP are used to address data, neither
is used for any other purpose in these functions. Once the register
values are set on entry, they are preserved until exit.

Figure 10 shows the layout of the VideoInfo structure in the program's
data segment and how its individual elements are referenced with BX. Note
that the compiler places an empty byte between SyncFlag and BufferStart to
align the latter on a word boundary.


Building a Video Library

The functions presented here can serve as the groundwork for a more
extensive video I/O library. OS/2's VioScrollDn and VioScrollUp functions
are relatively easy to duplicate in the DOS environment using straight
BIOS calls. Another useful function would be a counterpart to OS/2's
VioWrtNAttr, which permits a number of consecutive cells to be painted
with a specified attribute. The function could use VideoInfo as its
nucleus and AddressOf to calculate video addresses. VideoInfo itself
could even be expanded to provide more information such as CRTC addresses,
number of scan lines per character, or the amount of on-board display
memory.

DOS invites creative and time-saving solutions such as these; OS/2
eliminates them. Programmers should welcome the arrival of an operating
system that provides device independence and high performance in its
kernel routines and wholly insulates them from the vagaries of hardware
programming.

Nevertheless, DOS will continue to be around for the forseeable future,
and tools such as those presented here will remain a required part of
every programmer's toolkit.


Figure 1:  MASM code to transfer a word to the video buffer during the
           horizontal blanking interval.

            mov       cx,ax       ; store word value in CX
            mov       dx,3DAH     ; CRT Status Register
            cli                   ; interrupts off
  wait1:    in        al,dx       ; wait for non-retrace
            test      al,1        ;   period
            jnz       wait1
  wait2:    in        al,dx       ; wait for start of
            test      al,1        ;   next horizontal
            jz        wait2       ;   retrace
            xchg      ax,cx       ; retrieve word
            stosw                 ; then write it
            sti                   ; interrupts on


Figure 2:  C code to transfer a word to the video buffer during the
           horizontal blanking interval.

  while ((inp(0x3DA) & 0x01) != 0);
  while ((inp(0x3DA) & 0x01) == 0);
  *WordData1 = WordData2;


Figure 3:  Code generated by the Microsoft C Compiler 5.0 for
           "while ((inp(0x3DA) & 0x01) == 0);".

  _main1:   MOV    AX,03DAH
            PUSH   AX
            CALL   _inp

            PUSH   BP
            MOV    BP,SP
            MOV    DX,Word Ptr [BP+04]
            IN     AL,DX
            XOR    AH,AH
            MOV    SP,BP
            POP    BP
            RET

            ADD    SP,+02
            TEST   AL,01
            JZ     _main2
            JMP    _exit
  _main2:   JMP    _main1


Figure 4:  Source Code for CVIDEO.C

/*=================================================================
     CVIDEO.C - C video modules.
================================================================== */

#include <dos.h>

struct VideoInfo {
   unsigned char mode;
   unsigned char rows;
   unsigned char columns;
   unsigned char ColorFlag;
   unsigned char SyncFlag;
   unsigned int BufferStart;
   unsigned int SegAddr;
};

void GetVideoParms(struct VideoInfo *);
void ClrScr(char, struct VideoInfo *);
void ClrRegion(char, char, char, char, char);
void TextBox(char, char, char, char, char, struct VideoInfo *);
void SaveRegion(char, char, char, char, int *, struct VideoInfo *);
void RestRegion(char, char, char, char, int *, struct VideoInfo *);

extern int GetCharAttr(char, char, struct VideoInfo *);
extern void DispCharAttr(char, char, char, char, struct VideoInfo *);

struct VideoInfo video;
union REGS inregs, outregs;

/* ======================================================================
   GetVideoParms() fills a video structure of type VideoInfo with data.
======================================================================= */
void GetVideoParms(struct VideoInfo *sptr)
{
   char far *CrtMode    = (char far *) 0x00400049;
   char far *CrtCols    = (char far *) 0x0040004A;
   int  far *CrtStart   = (int far *) 0x0040004E;
   int  far *CrtAddr    = (int far *) 0x00400063;
   char far *CrtRows    = (char far *) 0x00400084;
   char far *CrtEgaInfo = (char far *) 0x00400087;

   /* --- Determine whether video writes must be synchronized --- */
   inregs.h.ah = 0x12;
   inregs.h.bl = 0x10;
   int86(0x10, &inregs, &outregs);
   if (outregs.h.bl == 0x10) {
       if (*CrtAddr == 0x3D4)
           sptr->SyncFlag = 1;
       else
           sptr->SyncFlag = 0;
       sptr->rows = 25;
   }
   else {
       if ((*CrtEgaInfo & 0x08) && (*CrtAddr == 0x3D4))
           sptr->SyncFlag = 1;
       else
           sptr->SyncFlag = 0;
       sptr->rows = *CrtRows + 1;
   }

   /* ---- Determine whether video is color or monochrome ----*/
   if (*CrtAddr == 0x3D4) {
       sptr->ColorFlag = 1;
       sptr->SegAddr = 0xB800;
   }
   else {
       sptr->ColorFlag = 0;
       sptr->SegAddr = 0xB000;
   }

   /* ----- Copy remaining parameters from BIOS ----- */
   sptr->mode = *CrtMode;
   sptr->columns = *CrtCols;
   sptr->BufferStart = *CrtStart;
}

/* ==================================================================
     ClrScr ()  clears the entire viewing area.
=================================================================== */
void ClrScr (char VideoAttr, struct VideoInfo *sptr)
{
     inregs.x.ax = 0x0600;
     inregs.h.bh = VideoAttr;
     inregs.x.cx = 0x0000;
     inregs.h.dh = sptr->rows - 1;
     inregs.h.dl = sptr->columns - 1;
     int86(0x10, &inregs, &outregs);
}

/* ==================================================================
     ClrRegion ()  clears the specified screen region.
=================================================================== */
void ClrRegion (char row1, char col1, char row2, char col2,
                char VideoAttr)
{
     inregs.x.ax = 0x0600;
     inregs.h.bh = VideoAttr;
     inregs.x.ch = row1;
     inregs.h.cl = col1;
     inregs.h.dh = row2;
     inregs.h.dl = col2;
     int86(0x10, &inregs, &outregs);
}

/* ==================================================================
     TextBox ()  draws a box around the specified region.
=================================================================== */
void TextBox (char row1, char col1, char row2, char col2,
              char VideoAttr, struct VideoInfo *sptr)
{
     char i;

     DispCharAttr (0xDA, row1, col1, VideoAttr, sptr);
     for (i=col1+1; i<col2; i++)
          DispCharAttr (0xC4, row1, i, VideoAttr, sptr);
     DispCharAttr (0xBF, row1, col2, VideoAttr, sptr);
     for (i=fow1+1; i<row2; i++)
          DispCharAttr (0xB3, i, col2, VideoAttr, sptr);
     DispCharAttr (0xD9, row2, col2, VideoAttr, sptr);
     for (i=cold2-1; i>col1; i--)
          DispCharAttr (0xC4, row2, i, VideoAttr, sptr);
     DispCharAttr (0xC0, row2, col1, VideoAttr, sptr);
     for (i=row2-1; i>row1; i--)
          DispCharAttr (0xB3, i, col1, VideoAttr, sptr);
}

/* ==================================================================
     SaveRegion ()  saves the contents of a specified screen region.
=================================================================== */
void SaveRegion (char row1, char col1, char row2, char col2,
                 int *ScreenBuffer, struct VideoInfo *sptr)
{
     char i, j;

     for (i=row1; i<=row2; i++)
          for (j=col1; j<=col2; j++)
               *ScreenBuffer++ = GetCharAttr (i, j, sprt);
}

/* ======================================================================
     RestRegion ()  restores the contents of a specified screen region.
========================================================================= */
void RestRegion (char row1, char col1, char row2, char col2,
                 int *ScreenBuffer, struct VideoInfo *sptr)
{
     char i,j;

     for (i=row1; i<=row2; i++)
          for (j=col1; j<=col2; j++)
               DispCharAttr ((char)  (ScreenBuffer++ & 0x00FF), i, j,
                             (char)  (ScreenBuffer >> 8), sptr);
}


Figure 5:  Source Code for ASMVIDEO.ASM

;====================================================================
; ASMVIDEO.ASM - Macro Assembler video modules for
;                small model C programs.
;====================================================================

              .MODEL SMALL
              .CODE
              PUBLIC _DispString, _DispCharAttr, _GetCharAttr

;--------------------------------------------------------------------
; _DispString writes a string to the active video buffer.
;--------------------------------------------------------------------
_DispString   PROC
              push   bp                ; save registers and establish
              mov    bp,sp             ;   stack frame addressability
              push   si
              push   di
              mov    bx,[bp+12]        ; get pointer to VideoInfo

              mov    si,[bp+4]         ; get pointer to string
              mov    dh,[bp+6]         ; calculate video address
              mov    dl,[bp+8]
              call   AddressOf
              mov    es,[bx+8]         ; point ES to video buffer
              mov    ah,[bp+10]        ; get video attribute in AH
              cld                      ; clear DF

dstr1:        lodsb                    ; get next character
              or     al,al             ; terminate on null byte
              jz     dstr3
              test   byte ptr [bx+4],1 ; branch if SyncFlag is clear
              jz     dstr2
              call   WriteWithWait     ; write during retrace
              jmp    dstr1
dstr2:        stosw                    ; write one character/attribute
              jmp    dstr1             ; loop back for more

dstr3:        pop    di                ; restore registers and exit
              pop    si
              pop    bp
              ret
_DispString   ENDP

;--------------------------------------------------------------------
; _DispCharAttr      writes a character and attribute to the active
;                    video buffer.
;--------------------------------------------------------------------
_DispCharAttr PROC
              push   bp                ; save registers and establish
              mov    bp,sp             ;   stack frame addressability
              push   di
              mov    bx,[bp+12]        ; get pointer to VideoInfo

              mov    dh,[bp+6]         ; calculate video address
              mov    dl,[bp+8]
              call   AddressOf
              mov    es,[bx+8]         ; point ES to video buffer
              mov    al,[bp+4]         ; get character/attribute
              mov    ah,[bp+10]        ;   in AX

              test   byte ptr [bx+4],1 ; branch if SyncFlag is clear
              jz     dca1
              call   WriteWithWait     ; write during retrace
              jmp    short dca2        ; then exit
dca1:         stosw                    ; write one character/attribute

dca2:         pop    di                ; restore registers and exit
              pop    bp
              ret
_DispCharAttr ENDP

;--------------------------------------------------------------------
; _GetCharAttr       returns the character and attribute at the
;                    specified address.
;--------------------------------------------------------------------
_GetCharAttr  PROC
              push   bp                ; save registers and establish
              mov    bp,sp             ;   stack frame addressability
              push   di
              mov    bx,[bp+8]         ; get pointer to VideoInfo

              mov    dh,[bp+4]         ; calculate video address
              mov    dl,[bp+6]
              call   AddressOf         ; offset in DI
              mov    es,[bx+8]         ; point ES to video buffer
              test   byte ptr [bx+4],1 ; branch if SyncFlag is clear
              jz     gca3

              mov    dx,3DAh           ; point DX to CRTC status reg
gca1:         in     al,dx             ; wait for non-retrace period
              test   al,1
              jnz    gca1
gca2:         in     al,dx             ; wait for next horizontal
              test   al,1              ;   retrace
              jz     gca2

gca3:         mov    ax,es:[di]        ; get character and attribute
                     pop   di          ; restore registers and exit
                     pop   bp
                     ret
_GetCharAttr  ENDP

;--------------------------------------------------------------------
; AddressOf          returns the video buffer address that
;                    corresponds to the specified row and column.
;
;   Entry:  DS:BX - address of VideoInfo structure
;           DH,DL - row and column
;   Exit:   DI    - offset address
;--------------------------------------------------------------------
AddressOf     PROC   NEAR
                     mov   al,[bx+2]   ; get columns per row
                     mul   dh          ; multiply by row number
                     xor   dh,dh       ; add column number
                     add   ax,dx
                     shl   ax,1        ; multiply by 2
                     add   ax,[bx+6]   ; add offset of active page
                     mov   di,ax       ; transfer result to DI
                     ret               ;   and exit
AddressOf     ENDP

;--------------------------------------------------------------------
; WriteWithWait      writes a character and attribute to a CGA during
;                    the horizontal blanking interval.
;
;   Entry:  ES:DI - video buffer address
;           AH,AL - character/attribute
;--------------------------------------------------------------------
WriteWithWait PROC   NEAR
                     mov   cx,ax       ; save character/attribute
                     mov   dx,3DAh     ; point DX to Status Register
                     cli               ; disable interrupts
www1:                in    al,dx       ; wait for non-retrace period
                     test  al,1
                     jnz   www1
www2:                in    al,dx       ; wait for next horizontal
                     test  al,1        ;   retrace
                     jz    www2
                     xchg  ax,cx       ; retrieve character/attribute
                     stosw             ; write them to video memory
                     sti               ; enable interrupts
                     ret
WriteWithWait ENDP

                     END


Figure 6:  Source Code for DEMO.C

/* ==================================================================
              DEMO.C - Program demonstrating the use of
              video functions.

              Copyright(C) 1988 Microsoft Systems Journal

              Compile with:  cl /c demo.c
                             link /noi demo+cvideo+asmvideo;
================================================================== */

#include "video.h"
#include <time.h>

struct VideoInfo video;
int buffer[3][510];

main()
{
   char TextColor = 0x07;
   char BorderColor = 0x0F;
   char WinColor1, WinColor2, WinColor3;
   int i, j;

   /* ----- Initialize video structure -----*/
   GetVideoParms(&video);

   /* ----- Set attributes for monochrome or color -----*/
   if (video.ColorFlag) {
       WinColor1 = 0x1F;
       WinColor2 = 0x4F;
       WinColor3 = 0x6F;
   }
   else {
       WinColor1 = 0x0F;
       WinColor2 = 0x70;
       WinColor3 = 0x0F;
   }

   /* ----- Clear the screen and fill it with text -----*/
   ClrScr(TextColor, &video);
   for (i=0; i<video.rows; i++)
      for (j=0; j<=52; j+=26)
         DispString("Microsoft Systems Journal", i, j, TextColor, &video);
   delay(2.0);

   /* ----- Open a window -----*/
   SaveRegion(5, 2, 14, 34, buffer[0], &video);
   ClrRegion(6, 3, 13, 33, WinColor1);
   TextBox(5, 2, 14, 34, BorderColor, &video);
   for (i=6; i<14; i++)
      DispString("Open the first window here...", i, 4, WinColor1, &video);
   delay(2.0);

   /* ----- Open a second window -----*/
   SaveRegion(2, 48, 12, 74, buffer[1], &video);
   ClrRegion(3, 49, 11, 73, WinColor2);
   TextBox(2, 48, 12, 74, BorderColor, &video);
   for (i=3; i<12; i++)
      DispString("Then the second here...", i, 50, WinColor2, &video);
   delay(2.0);

   /* ----- Open a third window overlapping the first two -----*/
   SaveRegion(9, 25, 22, 60, buffer[2], &video);
   ClrRegion(10, 26, 21, 59, WinColor3);
   TextBox(9, 25, 22, 60, BorderColor, &video);
   for (i=10; i<22; i++)
      DispString("And finally a third window here.", i, 27, WinColor3,
                  &video);
   delay(4.0);

   /* ----- Close all windows and exit -----*/
   RestRegion(9, 25, 22, 60, buffer[2], &video);
   delay(1.0);
   RestRegion(2, 48, 12, 74, buffer[1], &video);
   delay(1.0);
   RestRegion(5, 2, 14, 34, buffer[0], &video);
   delay(1.0);
   ClrScr(TextColor, &video);
}
/* ----- Timed delay function -----*/
delay(double sec)
{
   time_t StartTime, EndTime;

   time(&StartTime);
   time(&EndTime);
   while (difftime(EndTime, StartTime) < sec)
       time(&EndTime);
}


Figure 7:  Instructions to compile and link the library modules and
           demonstration program.

  ASMVIDEO.ASM       masm /ml asmvideo;

  CVIDEO.C           cl /c cvideo.c

  DEMO.C             cl /c demo.c link /noi demo+cvideo+asmvideo;


Figure 8:  Video Function Summary

void GetVideoParms(struct VideoInfo *sptr)
  Fills a structure of type VideoInfo with information reflecting the
  current video mode, the number of displayed rows and columns, whether
  video is color or monochrome, whether or not video buffer accesses should
  be synchronized with blanking intervals, and the video buffer address.

void ClrScr(char VideoAttr, struct VideoInfo *sptr)
  Clears the screen using the specified video attribute.

void ClrRegion(char row1, char col1, char row2, char col2, char VideoAttr)
  Clears a region using the specified video attribute.

void TextBox(char row1, char col1, char row2, char col2, char VideoAttr,
             struct VideoInfo *sptr)
  Draws a box around a region using single-line graphics characters and the
  specified video attribute.

void SaveRegion(char row1, char col1, char row2, char col2, int *buffer,
                struct VideoInfo *sptr)
  Saves the contents of a screen region to a buffer.

void RestRegion(char row1, char col1, char row2, char col2, int *buffer,
                struct VideoInfo *sptr)
  Copies the contents of a buffer to a screen region.

void DispCharAttr(char asciicode, char row, char col, char VideoAttr,
                  struct VideoInfo *sptr)
  Displays a single character at the specified row and column address using
  the specified video attribute.

void DispString(char *sptr, char row, char col, char VideoAttr,
                struct VideoInfo *sptr)
  Displays a string at the specified row and column address using the
  specified video attribute.

int GetCharAttr(char row, char col, struct VideoInfo *sptr)
  Returns the character and attribute at the specified row and column
  address.


Figure 9:  Source Code for VIDEO.H

/*
       VIDEO.H header file
*/

struct VideoInfo {
              unsigned char mode;
              unsigned char rows;
              unsigned char columns;
              unsigned char ColorFlag;
              unsigned char SyncFlag;
              unsigned int  BufferStart;
              unsigned int  SegAddr;
};

/* ----- C function prototypes -----*/

extern void GetVideoParms(struct VideoInfo *);
extern void ClrScr    (char, struct VideoInfo *);
extern void ClrRegion (char, char, char, char, char);
extern void TextBox   (char, char, char, char, char, struct VideoInfo *);
extern void SaveRegion(char, char, char, char, int *, struct VideoInfo *);
extern void RestRegion(char, char, char, char, int *, struct VideoInfo *);

/* ----- Macro Assembler function prototypes -----*/

extern int GetCharAttr(char, char, struct VideoInfo *);
extern void DispCharAttr(char, char, char, char, struct VideoInfo *);
extern void DispString(char *, char, char, char, struct VideoInfo *);


Figure 10:  Layout of the VideoInfo structure in the data segment.

                             ┌─────────────┐
                           2 │   SegAddr   │ BX+8
                             ├─────────────┤
                           2 │ BufferStart │ BX+6
                             ├─────────────┤
                           1 │  SyncFlag   │ BX+4
                             ├─────────────┤
                           1 │  ColorFlag  │ BX+3
                             ├─────────────┤
                           1 │   columns   │ BX+2
                             ├─────────────┤
                           1 │    rows     │ BX+1
                             ├─────────────┤
                           1 │    mode     │ BX+0
                             └─────────────┘

████████████████████████████████████████████████████████████████████████████

Developing SQL Server Database Applications Through DB-Library

Marc Adler☼

SQL Server is one of the latest applications in the growing set of
Microsoft(R) systems software products for the new generation of personal
computers and OS/2 systems. In "SQL Server Brings Distributed DBMS
Technology to OS/2 Via LAN Manager" (MSJ, Vol. 3 No. 4), we discussed the
concepts and features found in SQL Server and the ramifications it has for
the future of database processing. In this issue, we will demonstrate how
to access SQL Server from within C programs by examining SQL Server's
Application Program Interface (API), DB-Library, which is part of the SQL
Server Software Developers Pack.

DB-Library provides a set of functions that can be used in C programs to
access databases, issue SQL-based commands and queries, and extract data.
Unlike database APIs offered by some manufacturers, there is no "embedded
SQL" or SQL precompiler to worry about.

With an embedded SQL interface, the programmer codes actual SQL commands
into a C program by writing those commands and prefixing them with a
certain token, in much the same way that the C preprocessor recognizes
directives that are prefixed with the pound sign (#). This means that the
C program containing the SQL statements must be preprocessed with a
manufacturer's embedded SQL preprocessor before the program is actually
compiled; the preprocessor will generate a series of function calls with
the manufacturer's API. DB-Library eliminates this step, allowing you to
send any Transact:SQL command to SQL Server by issuing a simple function
call.

The functionality of DB-Library, combined with the APIs for Microsoft's
LAN Manager, Presentation Manager, and OS/2, will enable programmers
to design new applications that were previously incapable of running on
personal computers. With these products, true distributed database
processing along with sophisticated graphical interfaces will be
available to every PC user.


Main DB-Library Structures

Almost every function in DB-Library uses a DBPROCESS structure. This
structure contains information about your SQL Server connection, the
current state of pending commands, data resulting from the commands, and
error and information messages. To communicate with SQL Server, you
must have an active DBPROCESS structure that is returned when a dbopen
function call is made. The DBPROCESS structure is shown in Figure 1.

You can have more than one DBPROCESS structure active in the same program.
Two DBPROCESS structures can be connected to the same server or to
different servers. Each DBPROCESS structure functions much like a cursor;
just as a cursor holds information about the current position on the
screen or within a file, each DBPROCESS structure contains information
about the current processing state on each server.

For example, if you want to transfer data from one server to another, you
can issue a SELECT statement on one server and an INSERT command on the
other. As you fetch a row from the first server, an INSERT command could
be issued on the other server. Having multiple cursors on different
servers will help to provide true distributed database processing to the
SQL Server user.


Two-phase Commit

The first major step in achieving a true distributed processing
environment involves the concept of two-phase commit, a service that
essentially guarantees that either all databases on a set of
participating servers that are affected by a given set of transactions are
updated or that none of them are. Such a two-phase commit service is
already fully supported in DB-Library.

Briefly, all transactions, some or all of which might be on different
servers, are treated by the service as one transaction. One SQL Server is
used as a "commit server," a sort of central record-keeper that an
application uses to determine whether or not transactions should be
committed or rolled back (in case of failure). Here is a summary of the
process:

  ■  An application establishes a session with one or more servers and
     issues a BEGIN TRANSACTION along with information that identifies the
     application, the transaction, and the commit server.

  ■  The application issues Transact-SQL commands through DB-Library
     routines.

  ■  A PREPARE TRANSACTION command is sent indicating that the server is
     ready to commit.

  ■  The two-phase commit begins. In the first phase all servers agree
     that they are ready to commit. In the second phase they all  commit
     via a COMMIT TRANSACTION that is issued to all participating
     servers.

  ■  All servers coordinate with the commit server to determine if the
     transaction should be committed or rolled back.

There is a good deal more to this procedure but the basic two-phase commit
service described here is the key to true distributed processing.


LAN Manager/SQL Server

To connect to a SQL Server, you need to know which servers you are
connected to on the network and which of those servers is a SQL Server.
The Microsoft LAN Manager API provides a function called NetServerEnum to
do just this. NetServerEnum will fill a buffer with information about
all of the servers you are connected to.

Several LAN Manager API functions will allow you to specify an
information level; the higher the information level, the more
information will be returned about the requested item. For instance,
with the NetServerEnum function, specifying level 0 will return only
the name of each server, while level 1 returns not only the name, but the
type (workstation, server, or SQL server), version, and comments
associated with each server as well. Level 2 will return even more
information. See the LAN Manager API Reference for more details.

We are going to issue the NetServerEnum call with information level 1.
We can only log on to servers that have the SV_TYPE_SQLSERVER bit set in
their "type" field.


Logging On

The first step in establishing a connection to SQL Server is to allocate a
login structure and to fill it with such information as your user ID and
your password, as follows:

  LOGINREC *login;
  if ((login = dblogin())== NULL)
   {
   /* out of memory ... */
   }
  DBSETLUSER(login,
       "MARC");
  DBSETLPWD(login,"thunk");
  DBSETLAPP(login,"AdlerApp");

SQL Server must validate your credentials before permitting you access to
any database. You use the dblogin function to create a login record and
then use DBSETLAPP, DBSETLHOST, DBSETLPWD, and DBSETLUSER to fill in the
various fields of the record.

You must fill in your user name and password as SQL Server requires these.
The application name, AdlerApp, will be used by SQL Server in the
sysprocesses table to identify your application.

After the login structure has been filled, you use it in the dbopen call
to pass the login information to SQL Server, as follows:

  DBPROCESS *DBProc;
  if ((DBProc = dbopen(login, "SQLSERV")) == NULL)
   {
   <error processing>
   }

You also need to pass the name of the server to dbopen, or a NULL pointer
if SQL Server is being used on a standalone workstation. If everything
goes well, a DBPROCESS structure will be returned. Once you have obtained
a DBPROCESS structure, you can communicate with SQL Server.


Building a Query

You can issue queries and commands directly to SQL Server by inserting
the text of the command into the command buffer pointed to by the
DBPROCESS structure. Two functions, dbcmd and dbfcmd, let you build up a
command string for subsequent execution (see Figure 2). The text that
you pass to these functions is added to the end of DBPROCESS's command
buffer.

With dbcmd, you can build up an arbitrarily long command string. You can
even build a command string that is comprised of two or more separate SQL
commands. After the command is executed, the command buffer will be
cleared so that the next invocation of dbcmd will build a completely new
command string. You are responsible for inserting blanks properly when
calling dbcmd more than once; notice the blank preceding "where" in the
second call to dbcmd. The only difference between dbcmd and dbfcmd is
that dbfcmd can accept a printf-style format string and an arbitrary
number of arguments (see Figure 3).


Executing a Query

Once the DBPROCESS command buffer has a command string, you can then send
the command to SQL Server for execution. SQL Server provides functions to
ship a command to the server that the DBPROCESS structure is connected to
and to wait for a response from that server.

The dbsqlsend function will send the command to the appropriate SQL
Server, but will not wait for a response from the server. If you are
managing multiple I/O streams or want to do other processing before the
server responds, you can perform this processing directly after the call
to dbsqlsend.

The dbsqlok function waits for the response from the server, and returns a
code that tells the user whether the query succeeded or failed.

The dbsqlexec function is equivalent to a call to dbsqlsend followed by a
call to dbsqlok:

  if (dbsqlexec(
   DBProc) == FAIL)
   {
   <error processing>
   }
  else
   {
   <process results>
   }

If you don't need to do any processing between the time the command is
sent to the server and the time the server responds, you can use dbsqlexec.


Obtaining Results

At this point in the process, you have logged on to a SQL Server, built a
command, sent the command to the server, and received a reply that the
command was executed successfully. You are now ready to receive the
results from SQL Server and process them.

For each command that was in the DBPROCESS command buffer, you must call
dbresults. This function prepares the DBPROCESS structure to receive the
output of the next command that is in the command batch. If you do not
know how many commands were sent in the command batch, you will call
dbresults in a loop until it returns a code of NO_MORE_RESULTS. An example
of a dbresults loop is shown below:

  /* Loop to process each command that was sent in the command buffer */

  while ((rc = dbresults(DBProc))!= NO_MORE_RESULTS && rc != FAIL)
   {

   <Fetch each row>

             ∙
             ∙
             .
   }

Once the DBPROCESS structure has been prepared to receive the results,
you can access the row data in a number of ways. The most convenient method
to access the data is to use a combination of dbbind and dbnextrow.

Dbnextrow will retrieve the next data row from SQL Server. If a command
didn't return any rows, or if all the rows have been read, then dbnextrow
returns a result of NO_MORE_ROWS. The data will be read into internal
buffers that the DBPROCESS structure maintains, and you can use various
DB-Library functions to extract and convert the data to a format that you
can use.

You can use the dbbind function to ask DB-Library to extract and convert
the data automatically every time dbnextrow is called:

  DBINT num;
             ∙
             ∙
             ∙
  dbbind(DBProc, 1, INTBIND, 0, &num);

Before the dbnextrow loop, you would call dbbind to bind each column that
you were interested in to a variable within your C program. For each call
to dbbind, you specify the column number that you want bound, the address
of the C variable that receives the converted result, the length of the
variable, and the type of conversion you want.

For instance, executing the call made by the code shown above would tell
DB-Library that the data associated with column 1 should be converted to
type DBINT and should be transferred to the variable num. The fourth
parameter is the length of the variable; if it is 0, then you tell DB-
Library that you are positive that there is enough room to accommodate the
entire converted result.

We now have all the necessary tools to build and execute a simple query
such as that shown in Figure 4. The completed query will fetch the names
and department numbers of all of the members of the employees table.


Row Buffering

The DBPROCESS structure will normally keep the data associated only with
the current row. If you want to be able to access previously read rows,
then you must use "row buffering." The row buffering option must be
enabled and the number of rows to be buffered must be set by calling:

  dbsetopt(DBProc, DBBUFFER, "500");

This call will enable buffering and will cause DB-Library to allocate
buffer space to accommodate 500 rows.

Once you begin reading results with dbnextrow, you can access a previous
row by using the dbgetrow function. For example, if you are currently
reading row 100 and you want to access row 50, you would issue the call:

  dbgetrow(DBProc, 50);

Dbgetrow will change the "current row pointer" associated with the
DBPROCESS structure. For instance, when dbnextrow is next called, it will
retrieve rows starting at row number 51. You can clear row buffering by
making the call:

  dbclropt(DBProc, DBBUFFER, "0");


Other Functions

DB-Library has other functions that give you information about the
columns that result from a SELECT statement. These functions give you the
name of a column (dbcolname), the maximum length of a column (dbcollen),
the type of a column (dbcoltype), the number of columns (dbnumcols), the
actual size of the data in a column (dbdatlen), and a pointer to the
actual data (dbdata). This information is useful if you need to format the
results in a certain way. In the programming example later in this
article, we will illustrate the use of some of these functions.


Exiting

After you have finished your session with SQL Server, you need to log off
and clean up. You use dbclose to deallocate a DBPROCESS structure and to
sever the corresponding connection with SQL Server, as follows:

  dbclose(DBProc);

If you want to close several DBPROCESS structures, you must use dbexit.


Error Handling

DB-Library keeps track of system or DB-Library errors; the error code can
be retrieved by using dberrno. DB-Library will also return a printable
string that corresponds to the error number by using the dberrstr and
dboserrstr functions.

You can also install your own error handler that DB-Library will call
whenever an error is detected. The dberrhandle function passes the address
of your error handler pointer to DB-Library. When an error is detected,
DB-Library calls your routine, passing it the DBPROCESS structure, the
severity code, the DB-Library error code, and the operating system error
code:

  int myhandler();
             ∙
             ∙
             ∙
  dberrhandle(myhandler);

             ∙
             ∙
             ∙
  int myhandler(dbproc, severity, errno, oserr)

  DBPROCESS *dbproc;
  int   severity,
   errno,
   oserr;
   {
   <myhandler code>
   }

Your error handler must return one of three possible return codes shown in
Figure 5.


A SQL Server Program

We will use the techniques and DB-Library functions that have been
discussed along with some features of OS/2 to create a SQL Server program,
SQLPOP (see Figure 6 for the source code). This simple program is a
memory-resident ad hoc query facility that the user can invoke at the
press of a key. To accomplish this we will need to use an OS/2 facility
known as a "device monitor."

SQLPOP will be executed as a detached process, that is, will run in the
background and will not be activated until a certain event takes place.
The event that SQLPOP will watch for is a press of the Alt-S key
combination. When Alt-S is detected, SQLPOP will take over the screen
from the current foreground process and will present the user with a one-
line edit field. The user can issue any SQL Server command, as long as the
command is under 80 characters, and examine the results. After the results
are shown, SQLPOP relinquishes control to the foreground process and sits
quietly in the background waiting for another Alt-S combination.

The first step in the program will be to log on to SQL Server. We call the
function InitDB to handle the initialization. We first install our own
error handler as the default error handler for DB-Library. We have to
find out which SQL Servers are available, so we use the LAN Manager's
NetServerEnum function in order to find any servers that have the
SV_TYPE_SQLSERVER flag set. For the sake of simplicity, we will log on to
the first SQL Server that we find.

Next we allocate a login structure by using dblogin, fill in the user
name, password, and application name, and call dbopen to establish a
connection. For a production program, you would most likely prompt the
user for the user ID and password. FALSE is returned if the connection
could not be established, and the program is exited without any further
processing.

Back in the main routine, we know that there is a valid connection
established to SQL Server, so we can safely install our device monitor.
Before we proceed, let's take a quick look at terminate-and-stay-resident
(TSR) programs.

Under DOS, the process of writing a TSR program is confusing at best.
Esoteric and undocumented DOS calls are necessary to find out if the TSR
can be activated safely without interfering with the operation of DOS, the
hardware, or the program currently running. Usually the TSR is activated
when the user presses a certain key combination; this entails writing
an interrupt routine that puts itself in front of Interrupt 16H or
Interrupt 9H, depending on how early in the processing chain you want to
examine the keystroke. Even then you are not assured of getting first
dibs on the keystroke; other TSR programs may have decided to install
themselves before you are in the processing chain. This is messy business
all around.

OS/2 lets the programmer implement the concept of TSRs in a more friendly
manner. A device monitor lets you examine the information that comes from
a character device driver before it is returned to the application
program. A detached process can install a monitor on the keyboard device
driver, examine the keystroke information packets from the keyboard, and
choose to activate itself when a certain keyboard event is detected.
However, what do we do about a foreground process that is currently
running and has control of the screen?

Fortunately, OS/2 provides the VIOPOPUP function. (For a detailed
description of the OS/2 VIO subsystem, see "Character-Oriented Display
Services Using OS/2's VIO Subsystem," MSJ Vol. 2, No. 4). When a program
calls VIOPOPUP, it will preempt the foreground process and will take over
the input focus (keyboard, mouse, and screen). The detached process can
now interact with the user, unfettered by any other processes running in
that screen group. When the detached process is done, it returns the input
focus and control to the old foreground process by issuing a VIOENDPOPUP
call.

We will use these techniques to monitor the keyboard device driver in
SQLPOP. The DOSMONOPEN call will provide a handle (hMonitor) into the
keyboard device, and the DOSMONREG call will register the monitor with
the device driver. After the monitor is installed, we will just sit in an
infinite loop, reading each keyboard packet that comes in by using
DOSMONREAD. We will call DoSQLPopup if a key has been pressed and not
released, the Alt shift state is on, and the scan code of the key is an S.
If the keyboard packet does not indicate that this event has occurred, we
will then pass it back to the device driver with DOSMONWRITE.

DoSQLPopup will use the VIOPOPUP function to take over the screen group.
We draw a small box with room for a single line of input, position the
cursor at the first column in the box, and use KBDSTRINGIN to get the
user's input. If the user enters the command "quit" or "exit", then the
detached process will be terminated, along with the corresponding device
monitor.

The user's SQL command is passed to DB-Library with dbcmd and then sent to
the SQL Server that we are connected to by using dbsqlexec. Dbresults will
then prepare the results that SQL Server sends back; since the user might
have issued more than one SQL command, dbresults must be called before the
results for each command are processed.

We find out the number of columns in the result by calling dbnumcols and
set up the address in our results buffer where the data from each column
will be placed with dbbind. By using the CHARBIND argument in dbbind, we
will convert the data into a readable ASCII character format. Finally, we
use dbnextrow to read each row of data from SQL Server and print out the
results below the command box.

After all the results are read, we use the VIOENDPOPUP call in order to
return control to the old foreground process. VIOENDPOPUP will
automatically restore the contents of the foreground process' screen and
the rest of the information that constitutes the input focus.

───────────────────────────────────────────────────────────────────────────
How SQLPOP functions

┌──────────────────────┐
│     Load SQLPOP      │                ┌──Local OS/2 System──┐
├──────────▼───────────┤                │  ┌────────────────┐ │
│SQLPOP logon to server│                │  │    Kernel      │ │
├──────────▼───────────┤                │  ├────────────────┤ │
│ Register KBD monitor │     ┌──────────┼──►   SQL Server   │ │
└──────────────────────┘     │          │  ├────────────────┤ │
                             │          │  │   LAN Manager  │ │
┌──────────────────────┐     │          │  └────────────────┘ │
│   Alt S keystroke    │     │          └──┬──────────────────┘
│    encountered       │    Via            │
├──────────▼───────────┤    DBCMD          │
│    Call VIOPOPUP     │    DBSQL          │         ┌─────────────────────┐
├──────────▼───────────┤    EXEC           │         │Local OS/2 or DOS 3.x│
│    Get user input    │     │             │         │     Workstations    │
├──────────▼───────────┤     │             │         └─────────┬───────────┘
│  Send command to SQL ◄─────┘             │                   │
│  Server and display  │          ◄───┬────┴──Network──────────┴──────────►
│       results        │              │
├──────────▼───────────┤            ┌─┴────────────────────────┐
│      VIOENDPOPUP     │            │  Other OS/2 or DOS 3.x   │
│      and return      │            │Systems and/or SQL Servers│
└──────────────────────┘            └──────────────────────────┘
───────────────────────────────────────────────────────────────────────────

Compile, Link, Run

To compile SQLPOP.C, you issue the following compiler command (all on
one line, of course):

  cl /c /AM /Zp
  /W1 /Lp sqlpop.c

and to link the program, issue the following linker command (again, all on
one line):

  link sqlpop,sqlpop,,
  mdblib+mlibcep+doscalls;

To run the program, simply issue the following command from the OS/2 CMD
prompt:

  detach sqlpop

Now, whenever you press Alt-S in that screen group, SQLPOP will be
activated. You must ensure that you have started a SQL Server before you
run SQLPOP.


What's To Come

The simple programming example shown here only begins to touch on the
capabilities of DB-Library and SQL Server, and we really haven't explored
the SQL end of things. However, you should at least have a good sense of
the possibilities becoming available as all the pieces of the MS(R) OS/2
environment come together.

DB-Library offers a consistent set of functions that permit the C
programmer to access a SQL Server database running locally or on another
computer. Using the Microsoft LAN Manager, the user can write programs
that provide for distributed processing. Because the user does not have
to worry about the database engine robbing the program of precious
memory, the application programmer can concentrate on developing advanced
features that go beyond the present state of the art.


Figure 1:  The DBPROCESS Structure

struct dbprocess
{
struct       *dbfile;            /* dataserver connection */
servbuf

DBUSMALLINT  dbstatus;           /* status field for dbprocess */

BYTE         dbtoken;            /* current dataserver token */

DBSTRING     *dbcmdbuf;          /* command buffer */

int          dbcurcmd;           /* number of the current cmd results */

DBINT        dbprocid;           /* procid, if any, of current cmd */

DBCOLINFO    *dbcols;            /* linked list of column info */

DBALTHEAD    *dbalts;            /* linked list of alt column info */

DBROW        *dbfirstdata;       /* doubly linked list of returned row
                                    data */

DBROW        *dbcurdata;         /* current row in dbfirstdata */

DBROW        *dblastdata;        /* last row in dbfirstdata, usually
                                    dbcurdata */

DBOFF        *dboffsets;         /* list of offsets and controls in
                                    dbcmdbuf */

int          dboffadjust;        /* adjustment factor for offsets */

DBSMALLINT   dbcuroffset;        /* active offset for results */

DBOPTION     *dbopts;

DBSTRING     *dboptcmd;          /* option string to send to server */

DBINFO       *dbmsgs;            /* linked list of info and error messages */

DBINFO       *dbcurmsg;          /* last message read by dbgetmsg() */

DBDONE       dbdone;             /* done information */

char         dbcurdb[MAXNAME+1]; /* the name of the current database */

INTFUNCPTR   (*dbbusy)();        /* Called when waiting on dataserver */

int          (*dbidle)();        /* Called when waiting on dataserver */

int          (*dbchkintr)();     /* user's function to call to check
                                    for queued interrupts */

int          (*dbhndlintr)();    /* user's interrupt handler */

int          dbbufsize;          /* the size of the row buffer, if enabled */

NULLBIND     dbnullbind;         /* what to bind for nulls */

int          dbsticky;           /* sticky flags like attn */

int          dbnumorders;        /* number of columns in the query's
                                    "order by" clause */

int          *dbordercols;       /* array of the column numbers
                                    found in query's "order by" clause */

BOOL         dbavail;            /* is this dbproc available for general
                                    use? */

int          dbftosnum;          /* this id is used when recording
                                    frontend-to-Server SQL traffic of this
                                    DBPROCESS */

BOOL         dbdead;             /* TRUE if this DBPROCESS has become
                                    useless, usually due to a fatal Server
                                    error, or a communications failure */

BOOL         dbenabled;          /* TRUE if this DBPROCESS is allowed to be
                                    used in DB-LIBRARY functions. The user
                                    may set this flag FALSE, possibly
                                    within an error handler, if execution
                                    of further commands would just cause
                                    further errors. DB-LIBRARY initially
                                    sets this flag TRUE. The user may set
                                    and re-set this flag at will */

BOOL         dbloginfailed;      /* TRUE if the Server has rejected
                                    our user name and password */

BOOL         dbsqlsent;          /* TRUE if the SQL in the command buffer
                                    has already been sent to the Data-
                                    Server */

struct       *dbnext;            /* DBPROCESSes are kept track of in a big
dbprocess                           linked-list */

};

typedef struct dbprocess DBPROCESS;


Figure 2:  Using dbcmd()

  dbcmd(DBProc, "select * from employees");
  dbcmd(DBProc, " where dept = 9999");


Figure 3:  Example Using dbfcmd()

  char *tblname;
  int  limit;
                                    ∙
                                    ∙
                                    ∙
  /*  An example of building two commands in one command buffer - Note the
      printf-like feature of dbfcmd()
   */

  dbfcmd(DBProc, "select * from %s where dept > %d", tblname, limit);
  dbcmd (DBProc, "select * from wages");


Figure 4:  A Complete Example of a Database Query

char name[80];
DBINT dept;

dbcmd(DBProc, "select name, dept from employees");

if (dbsqlexec(DBProc) == FAIL)
      {
          fprintf(stderr, "The query failed.\n");
      }
else
      {
      while (dbresults(DBProc) != NO_MORE_RESULTS)
          {
               dbbind(DBProc, 1, CHARBIND, sizeof(name), name);
               dbbind(DBProc, 2, INTBIND,   0, &dept);

               while (dbnextrow(DBProc) != NO_MORE_ROWS)
                   {
                        printf("%-30s\t%ld", name, dept);
                   }
          }
      }


Figure 5:  Error Handler Return Codes

  INT_EXIT      Print an error message and abort the program

  INT_CANCEL    Return FAIL from the DB-Library routine that caused the
                error

  INT_CONTINUE  For time-out errors, wait another time-out period; at
                the end of that period, call the error handler again


Figure 6:  Source Code for SQLPOP.C

/**********************************************************************
 * SQLPOP - allows the user to hot-key into an SQL Server query window
 *
 * Created by Marc Adler
 *********************************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <memory.h>
#include <string.h>
#include <sqlfront.h>
#include <sqldb.h>
#include <netcons.h>
#include <server.h>
#include <os2def.h>
#include <doscalls.h>
#include <subcalls.h>
#include <infoseg.h>

typedef unsigned char BOOL;
typedef unsigned char BYTE;
typedef unsigned int  WORD;
typedef unsigned long DWORD;

/* Structure of the keyboard packet which the device monitor receives */

typedef struct kbdpacket
{
WORD        monitor_flags;
BYTE        ascii_code;
BYTE        scan_code;
WORD        nls;
WORD        shift_code;
DWORD       time;
WORD        devdrv_flags;
} KBDPACKET;

#define KEY_RELEASE    0x40
#define ALT            0x08

#define IDNO           FALSE
#define IDYES          TRUE
#define MB_YESNO       1

#define ERR_DBSQLEXEC_FAILED    -100
#define ERR_DBRESULTS_FAILED    -101

DBPROCESS *DBProc = NULL;            /* Our connection with the DataServer*/

WORD        hMonitor;                /* Monitor handle                    */
BOOL        bIsResident = FALSE;     /* TRUE once we become resident      */

struct server_info_1 ServerInfo[65]; /* server_info_1 buffer area */

/***********************************************************
 * Main()
 *
 * Logs into SQL Server, installs the monitor, and calls
 * DoSQLPopup() when the user presses the ALT-S key.
 **********************************************************/

main()
{
    KBDPACKET   packet;
    int         packet_len;
    char        inbuf[128], outbuf[128];

    /* Try to login */
    if (!InitDB())
        exit(1);

    bIsResident = TRUE;

    /* Open a keyboard monitor and register it */

    DOSMONOPEN((char far *) "KBD$", (unsigned far *) &hMonitor);
    inbuf[0]  = sizeof(inbuf);   inbuf[1]  = 0;
    outbuf[0] = sizeof(outbuf);  outbuf[1] = 0;
    DOSMONREG(hMonitor, (char far *) inbuf, (char far *)
              outbuf, 0, GetCurrScreenGroup());

    /* Monitor loop - watches for the ALT+S keystroke */
    for (;;)
    {
        packet_len = sizeof(packet);
        DOSMONREAD((char far *) inbuf, 0, (char far *) & packet,
                   (unsigned far *) &packet_len);

    if (!(packet.devdrv_flags & KEY_RELEASE))
    {
        /* Check for an ALT-S keystroke. */

        if ((packet.shift_code & ALT) &&
            packet.ascii_code == 0 && packet.scan_code == 31)
        {
            DoSQLPopup();
            continue;
        }
    }

    /* Pass all other keys through */
    DOSMONWRITE((char far *) outbuf, (char far *) &packet, packet_len);
    }
}

/**********************************************************
 * TerminateMonitor()
 * Logs off the server and terminates the monitor.
 *********************************************************/

TerminateMonitor()
{
    Logout();
    DOSMONCLOSE(hMonitor);
    DOSEXIT(1, 0);
}

/***********************************************************
 * DoSQLPopup()
 * Puts up a rudimentary one-line query box, gets the user's
 * query, sends it to SQL Server, and prints the results.
 * This is done when the user presses the proper hot-key
 * sequence.
 **********************************************************/

char *BoxStr[] =
{
"IMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM
":
:",
"HMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM
};

DoSQLPopup()
{
    unsigned   i;
    int        row;
    char       buf[82];
    struct     KbdStringInLength ksl;
    unsigned   waitflag;
    unsigned   attr = 0x07;
    int        nc, col, len;
    char       *more = "Press a key for more...";

    /* Take over the screen group for a while */
    waitflag = 0;

    if (VIOPOPUP((unsigned far *) &waitflag, 0))
            return;

    /* Draw the query box */
    for (i = 0;  i <= 2;  i++)
    {
        VIOWRTCHARSTRATT((char far *) BoxStr[i],
                    strlen(BoxStr[i]), i, 0,
                    (char far *) &attr, 0);
    }

    VIOSETCURPOS(1, 1, 0);

    /* Get the user input. Get rid of trailing carriage returns. */
    ksl.Length = sizeof(buf);
    KBDSTRINGIN((char far *) buf, (struct KbdStringInLength far *)
                 &ksl, 0, 0);
    if (ksl.LengthB)
    {
        if (buf[ksl.LengthB] == '\r')
              buf[ksl.LengthB] = '\0';
    }

    /* If the user wants to exit the program, logout and
       deinstall the monitor */

    if (!strcmpi(buf, "quit") || !strcmpi(buf, "exit"))
    {
        VIOENDPOPUP(0);
        TerminateMonitor();
    }

    /* Ship the query to SQL Server */

    dbcmd(DBProc, buf);
    if (dbsqlexec(DBProc) != FAIL)
    {
        while (dbresults(DBProc) != NO_MORE_RESULTS)
        {
            char results_buf[256], *rb;

            /* Find out how many columns each row has */
            nc = dbnumcols(DBProc);
            /* Insert the row data one column at a time into the buffer */
            for (rb = results_buf, col = 1;  col <= nc;  col++)
            {
                dbbind(DBProc, col, CHARBIND, 0, rb);
                len = dbcollen(DBProc, col);
                rb += len;
            }
            *rb = '\0';

            /* Print out the results */
            for (row = 3;  dbnextrow(DBProc) !=
                NO_MORE_ROWS;  row++)
            {
                VIOWRTCHARSTRATT((char far *)
                    results_buf, strlen(results_buf),
                    row, 0, (char far *) &attr, 0);
                if (row > 22)
                {
                    VIOWRTCHARSTRATT((char far *) more,
                        strlen(more), row, 0,
                        (char far *) &attr, 0);
                    getch();
                    row = 3;
                }
            }
        }
    }

    VIOWRTCHARSTRATT((char far *) more, strlen(more), 24,
                      0, (char far *) &attr, 0);
    getch();

    VIOENDPOPUP(0);
}

/**********************************************************
 * InitDB()
 * Installs the error handler and tries to login to SQL
 * Server.
 **********************************************************/

InitDB()
{
    int DBErrorHandler();

    /* Install our error handler */
    dberrhandle(DBErrorHandler);

    /* Try to login to SQL Server */
    if (!DoLogin())
        return FALSE;
    return TRUE;
}

/**********************************************************
 * DoLogin()
 * Executed at startup and when user chooses LOGIN
 * from the menu.
 *
 * RETURNS
 *
 * TRUE if the login is successful, FALSE if not.
 **********************************************************/

DoLogin()
{
    LOGINREC    *login;    /* Our login information */
    int         iServer;

    /* Find all servers we are connected to who are running SQL SERVER */
    if ((iServer = LocateSQLServer()) == FALSE)
        return FALSE;

    /* Get a login structure and set the app name, the login id, and the
       pwd. */

    login = dblogin();
    DBSETLAPP(login, "Adler");
    DBSETLUSER(login, "sa");
    DBSETLPWD(login, "");

    /* Open a new DBProc structure with the new login info.
       If we fail, then DB_LIB will print out a message
       through the error handler. */

    if ((DBProc = dbopen(login,
        ServerInfo[iServer].sv1_name)) == NULL)
        return FALSE;
    return TRUE;    /* Successful login ! */
}

/**********************************************************
 * Logout()
 * Closes the DBProc upon exit.  We call this when we end
 * our keyboard monitor.
 **********************************************************/

Logout()
{
    if(DBProc)
    {
        dbclose(DBProc);
        DBProc = NULL;
    }
}

/**********************************************************
 * DBErrorHandler()
 * Handles error conditions from SQL Server. Puts up a
 * message box and asks the user for an action.
 **********************************************************/

int DBErrorHandler(dbproc, severity, errno, oserr)
DBPROCESS    *dbproc;
int        severity, errno, oserr;

{
/* Print the SQL Server error message */

if (MessageBox("SQL Server Error!!!",
                dberrstr(errno),
               "Continue the program?",
                MB_YESNO) == IDNO)
{
    TerminateMonitor();
    return INT_EXIT;
}

/* See if we have an operating system error */
if (oserr != DBNOERR)
    if (MessageBox("OS/2 Error!!!",
            dboserrstr(oserr),
            "Continue the program?",
            MB_YESNO) == IDNO)
    {
        TerminateMonitor();
        return INT_EXIT;
    }

/* If for some reason the DBProc structure is dead, exit immediately */

if (DBDEAD(dbproc))
{
    TerminateMonitor();
    return INT_EXIT;
}

/* Tell DB-LIB to cancel the command and continue. */

return INT_CANCEL;
}

/**********************************************************
 * printmsgs()
 * Fetches any pending messages from SQL Server and
 * inserts them into the results buffer.
 *
 * RETURNS - nothing
 **********************************************************/

void printmsgs(dbproc)
DBPROCESS *dbproc;

{
    char *msg;

    while ((msg = dbgetmsg(dbproc)) != NULL)
        fprintf(stderr, "%s\n", msg);
}

MessageBox(s1, s2, s3, action)
char *s1, *s2, *s3;
unsigned  action;

{
    int    c;

    if(s1)
    {
        fprintf(stderr, "%s", s1);
        if(s2)
        {
            fprintf(stderr, "\n%s", s2);
            if(s3)
            {
                fprintf(stderr, "\n%s", s3);
            }
        }
    }

    switch(action)
    {
        case MB_YESNO :
            if ((c = getch()) == 'Y' || c == 'y')
                return IDYES;
            return IDNO;
    }
}

/**********************************************************
 * GetCurrScreenGroup()
 *
 * Returns the id of the current screen group.
 **********************************************************/

#define    MAKEPGINFOSEG(sel)    (MAKEP(sel, 0))

GetCurrScreenGroup()
{
    unsigned ldt_seg, gdt_seg;
    struct InfoSegGDT far *pGdt;

    DOSGETINFOSEG((unsigned far *) &gdt_seg,
                  (unsigned far *) &ldt_seg);
    pGdt = MAKEPGINFOSEG(gdt_seg);    /* Get a pointer to the info */
    return pGdt->CurScrnGrp;          /* return the screen group   */
}

LocateSQLServer()
{
    unsigned entriesread;    /* number of entries read by NetServerEnum */
    unsigned totalentries;   /* total number of entries available       */
    int i, rc;

    rc = NetServerEnum((char far *) NULL, 1,
                (char far *) ServerInfo,
                sizeof(ServerInfo),
                (unsigned far *) &entriesread,
                (unsigned far *) &totalentries);
    if (!rc)
    {
        for (i = 0;  i < entriesread;  i++)
        if (ServerInfo[i].sv1_type & SV_TYPE_SQLSERVER)
            return i + 1;    /* use offset of 1 so we don't return a '0' */
    }

    fprintf(stderr, "Could not find an SQL Server\n");
    return FALSE;
}

████████████████████████████████████████████████████████████████████████████

OS/2 Protected-Mode Programming with Forth, LISP, Modula-2, and BASIC

Andrew Schulman☼

If you were going to be stranded on a desert island, what programming
language would you make sure you had with you?

Well yes, if forced into such a position I would undoubtedly take C too.
Fortunately we do not have to make such a choice. OS/2 programmers are
anything but stranded on a desert island and are definitely not limited to
working with one language. A wide selection of compilers and interpreters
have become available for OS/2's protected mode, and this article will
examine four of them──Forth, LISP, Modula-2, and BASIC──each with its own
unique features and each one able to do things that cannot be done directly
in C.

UR/FORTH, from Laboratory Microsystems, Inc. (LMI), is available for
OS/2 protected mode, 386 machines, MS-DOS(R) real mode, and 68000 UNIX(R). Th
Modula-2 development system for OS/2 used in this article is available
from Stony Brook Software, Inc., which also makes a version for real-mode
DOS. Microsoft(R) BASIC Version 6.0 compiles code for MS-DOS and OS/2.
Finally, the LISP interpreter discussed here is an extended version of
David Betz's freely-distributed XLISP, which I ported to OS/2 protected
mode, and can be found on several electronic bulletin boards. There are
other languages available as well; COBOL and FORTRAN come immediately
to mind.

The attitude taken in this article reflects that of The Tao of
Programming, an ancient text discovered and translated by Geoffrey
James:

"The assembler gave birth to the compiler. Now there are ten thousand
languages. Each language has its purpose, however humble. Each language
expresses the yin and yang of software. Each language has its place
within the Tao. But do not program in COBOL if you can avoid
it." [Geoffrey James, The Tao of Programming, InfoBooks, 1987, p. 7.]

We will focus on several aspects of each product:

   ■  How does it let the user make function calls to the OS/2 Application
      Programming Interface (API)? How well are OS/2 calls integrated into
      the rest of the language?

   ■  What conversions are required between OS/2 data types and the data
      types intrinsic to the language? How easy is it to create OS/2 data
      structures?

   ■  Unlike C, which was never designed for concurrent programming, some
      languages have built-in multitasking capabilities. When these
      languages are ported to OS/2, threads should be used to implement
      the languages' process abstraction. What is OS/2 programming like
      when using a language that has inherent multitasking
      capabilities?

   ■  How different is the product from its MS-DOS (or 386) equivalent?
      What OS/2 features does the product take advantage of?

Along the way, we will see that some of these languages offer useful
services, such as an in-line assembler and other forms of mixed-language
programming, OS/2 graphics (without Presentation Manager), access to
ports, and built-in debugging facilities.


An OS/2 Rosetta Stone

In 196 BC, during the reign of Ptolemy V, a clerk transcribed a petty
decree in three scripts: Greek, Egyptian demotic, and hieroglyphics. The
discovery of this slab, known as the Rosetta Stone, led to the
decipherment of Egyptian hieroglyphics in the early nineteenth century.

In the same way, the code shown in Figure 1 presents an otherwise
unimportant OS/2 program transcribed in C, Forth, LISP, Modula-2, and
BASIC. The idea is to help the C programmer decipher the possibly
cryptic notations used by the other languages.

The program simply loops through all possible module handles, printing out
the names of all currently loaded executables and dynamic-link libraries
in all sessions. The program makes 65,535 calls to DosGetModName, and
takes a long time to run, no matter which language is used (DosGetModName
could be OS/2's slowest function).

Figure 1 also shows sample output from the program, from which you can see
that I boot OS/2 off floppy drive A, that I use a command line editor
(ALIAS.DLL), Lugaru Software's Epsilon(TM) text editor, and that I was
running UR/FORTH and OS2XLISP.

Each of these products has its own way of providing OS/2 function calls.
Calling OS/2 from Microsoft(R) C involves using an include file containing
function prototypes, and a comparable technique is used in Modula-2 and
BASIC. The way to declare external functions differs, however. Because C
does not use the Pascal calling convention, it requires the APIENTRY (far
pascal) modifier for OS/2 functions. Both Modula-2 and BASIC already use
the Pascal function calling convention, and therefore require no special
handling for OS/2 functions.

UR/FORTH and OS2XLISP are both interactive programming environments,
rather than batch-mode compilers, and neither includes header files. In
UR/FORTH every OS/2 API call, except for a few that Forth cannot use
effectively (such as KbdRegister and VioRegister), has a header built into
the OS/2 version. OS2XLISP uses run-time dynamic linking: we get a handle
to the DOSCALLS module with the (loadmodule) function, link to the
function with (getprocaddr), and invoke the function with (call). Neither
UR/FORTH nor OS2XLISP checks what you pass to the OS/2 function,
though in both cases users can supply their own parameter checking.

Another issue that comes up in this small program is passing OS/2 a far
pointer to a buffer. In C, the function prototype casts buf to PCHAR (a
far pointer to a char), while in Modula-2, the procedure ADR takes a
variable or procedure and returns its address. OS2XLISP's (call)
function automatically treats string parameters as far pointers. In
Forth, offspring of CREATE (like BUF) automatically return their address.
However, the address is the offset of BUF within the Forth dictionary, and
in order to get a full far pointer, we also need the segment/selector of
the Forth data segment, which the UR/FORTH word DS0 supplies.

Like Forth, BASIC handles the segment and offset separately. The
declaration for DosGetModName% treats the far pointer as two separate
integer parameters; VARSEG retrieves the segment and SADD retrieves the
offset of a variable-length string. The percent sign at the end of the
BASIC name indicates that this function returns an integer.

DosGetModName moves an ASCIIZ string into the buffer, but ASCIIZ is not
the intrinsic string type of every programming language. In UR/FORTH,
the expression BUF -ASCIIZ COUNT TYPE converts BUF from ASCIIZ to a Forth
"counted string" and then prints it. In BASIC, we can use the PRINT
command, but we have to RTRIM$ the string and refill the buffer with
spaces before using it again. With Modula-2, strings are similar to Pascal
strings, where each string has a definite length, but Modula-2 also
recognizes null terminators, which are designated 0C. Finally, OS2XLISP
is written in C, and uses the C ASCIIZ string format.


UR/FORTH

UR/FORTH is a multitasking threaded-code interpreter. In contrast to DOS's
register- and interrupt-based system services, parameters to the OS/2 APIs
are placed on the stack, and a far CALL invokes any given service.
Because it is stack-based, access to the OS/2 APIs fits naturally into
Forth, a stack-oriented language.

To call OS/2 functions from UR/FORTH, parameters are placed on the stack,
followed by the name of the OS/2 function which will use them. This is
just like any other version of Forth. Here's the real treat:

  880 100 + . 980 ok
  880 100 DOSBEEP . 0 ok

Note that just as 880 100 + can be keyed in interactively by using Forth
like an RPN calculator, so can OS/2 function calls.

OS/2 functions can be called from UR/FORTH without having to write a
"program," which makes it an ideal tool for experimenting with and
learning OS/2. Just as + uses the top two numbers on the stack, replacing
them with their sum, the DOSBEEP operator uses the top two numbers on the
stack, replacing them with an OS/2 error code. In this example, it also
happens to have the side effect of playing the first note of a Bach
toccata. The dot is not idle punctuation, but the Forth "word" which
prints the top number on the stack. After Forth has responded to a
command, it prints "ok" and waits for your next command.

What exactly is Forth? Essentially it is an interactive interpreter,
compiler, and assembler rolled into one. Forth maintains a dictionary of
words, in which "." and "+" and "DOSBEEP" are all words. Any new words you
create, such as ENUMDLL in Figure 1, consist either of previously defined
words strung or "threaded" together, or of native object code from the
Forth assembler.

An outer interpreter reads input from the keyboard or from a file, a
compiler strings together instructions, and an inner interpreter chugs
along the thread of instructions carrying them out. The user can directly
access any of these components (e.g., incorporating the outer
interpreter in your own programs).

UR/FORTH for OS/2 is an OS/2 protected-mode task, not its own operating
system like some other Forths. UR/FORTH is designed for multitasking and
protected-memory environments. In addition to providing direct access
to the OS/2 APIs, UR/FORTH has a multilevel machine-independent
operating system interface so that you can also use MALLOC instead of
DOSALLOCSEG, or FOPEN instead of DOSOPEN.


Factoring Forth Words

The examples in Figure 1 are as alike as possible. In Forth, one might
break up even this miniprogram into several words. Everything except a
number in Forth is a word, including control structures. This means you
can create new control structures and user programmable syntax. Forth
words are defined with the word : (a colon), and ; (a semicolon)
terminates the definition. In Figure 1, we defined ENUMDLL, which
contained a DO LOOP, and executed it by typing ENUMDLL.

We can include ENUMDLL inside other words, but it does too much to be
generally useful. A more realistic Forth version of Figure 1 is shown in
Figure 2. One of our new words, MODULE?, expects a number on the stack; it
tests this with DOSGETMODNAME and leaves a boolean flag on the stack. This
(before-after) stack effect is documented inside the parenthetical
comment, which is a Forth convention for commenting words.

The new word .ASCIIZ prints out an ASCIIZ string whose address is on the
top of the stack. Since the Forth word . (dot) prints, a dot often
precedes the name of a word which prints anything. The word .MODULE then
prints out a module name and number. It calls our newly defined word
.ASCIIZ. Finally, the new version of ENUMDLL uses the new words MODULE?
and .MODULE.

The structure of IF in Forth takes a little getting used to at first, but
it is consistent with Forth's postfix notation. Note that THEN means
afterward, and has no connection to the if...then construct found in other
programming languages.


Interacting with Threads

There isn't anything particularly remarkable about this code; after
all, as Figure 1 demonstrates, it can also be written in C, LISP,
Modula-2, and BASIC. Figure 3 presents a more interesting bit of Forth
code for OS/2, a tiny interactive laboratory for experimenting with
multiple threads of execution.

If you are familiar with Forth, it might surprise you that Figure 3 can be
run from UR/FORTH simply by typing INCLUDE THREADS.4TH. In addition to the
old-style 1024-byte screen blocks used in most Forths (for which UR/FORTH
provides a very good editor), UR/FORTH also offers INCLUDE to compile
Forth source code from normal text files.

In THREADS.4TH several threads of execution are launched, each one
executing through the same piece of code, with each thread displaying its
thread ID number on a portion of the screen. An area of the screen is left
free for you to enter commands, and a HELP message is displayed. If
threads 5, 6, and 7 are executing, for instance, you can type SUSPEND 6
and, sure enough, thread 6 will halt. Type RESUME 6 and it starts again.
If you type SET-IDLE 6, it appears to halt again; but if you SUSPEND 5 and
SUSPEND 7, then thread 6, which is executing at idle priority, begins to
display on the screen again because it's getting more timeslices.

You should note that nowhere is there a call to DOSCREATETHREAD. Yet
there are threads here. Like many other versions of Forth, UR/FORTH has a
multitasking facility that allows creation of multiple background tasks.
In versions of UR/FORTH for MS-DOS and the 386, Forth task control blocks
and semaphores are set up and maintained entirely by Forth.

In the most recent version of UR/FORTH for OS/2, however, LMI has
implemented Forth tasks as OS/2 threads and Forth semaphores as OS/2
semaphores (UR/FORTH comes with a huge amount of source code, including
code for the tasker, so you can see how it's done). Forth tasks under OS/2
are therefore truly timesliced, whereas in other versions a switch depends
on someone calling PAUSE, much more like nonpreemptive multitasking under
either Microsoft Windows or Apple(R) MultiFinder(TM). Aside from the inclusio
of the API calls in the OS/2 version, this is probably the major
difference between the OS/2 and non-OS/2 versions of UR/FORTH.

We can employ the standard UR/FORTH multitasking words (such as TCB to
create a task control block, SLEEP to suspend a task, or SGET to acquire
a semaphore), and know that we are actually using the underlying
multitasking services of OS/2. Likewise, we can use OS/2 function calls
like DOSSUSPENDTHREAD in order to manipulate Forth task control blocks.

Each task should execute PAUSE somewhere in its main loop, as PAUSE is
responsible for killing the thread when a task is terminated by STOP.
Since OS/2 lacks a DosKillThread call, STOP has to do some funny business
to convince a thread to execute a DOSEXIT.

Even though UR/FORTH is an interpreter, it comes with an assembler and
native-code compiler. It is therefore capable of creating object code
whose address can be passed to DOSCREATETHREAD. Note that all the threads
execute through the same code. Each thread can get its own ID number,
because each thread has its own view of the local information segment.

THREADS.4TH creates a tiny command language for playing with
threads/tasks. In Forth, it is a trivial matter to create such "little
languages." Rather than force the user to conform to Forth's postfix
notation and enter commands such as 6 SUSPEND, in this example we define
GETNUM, which takes a number, not from the stack but from the input
stream, so instead you can type SUSPEND 6. This also shows how easy it is
for Forth programmers to use the language parser in their own programs-
something you can't do with a C compiler.


Graphics, IOPL, and Ports

UR/FORTH for OS/2 has a good set of video and graphics primitives, and
provides a good environment in which to do non-Presentation Manager-based
graphics programming. Its graphics can be used inside UR/FORTH tasks, so
you can create multithreaded graphics programs.

One reason graphics programming under OS/2 is difficult is IOPL
protection, which can make direct port access under OS/2 tricky. However,
UR/FORTH makes this easy, as the small program in Figure 4, XY.4TH,
illustrates. XY.4TH directly programs the 6845 controller to deposit the
current X and Y cursor position on the stack. UR/FORTH comes with the
function ?XY which does the same thing by calling VIOGETCURPOS; however,
XY.4TH does help to illustrate a few points.

To send a byte to a port from UR/FORTH, use PC!, and to read a byte from a
port, use PC@. In our example, we send 0EH to port 3D4H, and then read a
byte from port 3D5H. The first time you use one of these words, the disk
light comes on as OS/2 loads in an IOPL segment. Note that each PC@ and
PC! involves a far call into the IOPL segment, so they aren't as fast as
in real-mode UR/FORTH. You need not call DOSPORTACCESS, since it is called
for all ports by UR/FORTH at initialization.

The word CUR-LOC puts the contents of registers 14 and 15 of the 6845
controller on the stack, MK-WORD forms the bytes into a word, and COL-ROW
splits out the current X and Y position of the cursor. These three words
are threaded together in the definition of CURS, and all without any
variables; all communication occurs on the stack. Because of this, Forth
words can easily return multiple values-another thing that can only be
simulated in C.

Some other "goodies" for UR/FORTH include: the file OS2.SCR, which is an
extensive demonstration of shared memory, queues, pipes, and so on; a
file with the unlikely name URPOUN.ARC, with which you can create OS/2
data structures; also, the LMI BBS has applications like SEE (a Forth
decompiler); a turtle graphics package for UR/FORTH; and URTCO (a threaded
code optimizer).

What does UR stand for? Don't ask!


OS2XLISP

In some respects, LISP is the opposite of Forth. For instance, in Forth we
said 880 100 + . whereas with the OS2XLISP interpreter, one might have the
following exchange (the LISP prompt is a simple angle bracket):

  > (+ 880 100)
  980

While many programming languages use infix like 880 + 100, and while
Forth uses postfix, functions in LISP are always at the beginning of the
list.

A LISP function call is a list, marked with LISP's infamous parentheses.
The LISP evaluator uses the first item in a list (the car) as a function
applied to the remainder of the list (the cdr). For example, + is applied
to 880 and 100. Whereas in Forth we had to explicitly ask to print out a
result, the LISP interpreter does this automatically; LISP's top level
is a read-eval-print loop.

Actually, Forth and LISP aren't completely different. Both, for example,
use the idea that, in an expression, the function or operator should
appear in some consistent place. And like UR/FORTH, OS2XLISP is
interactive. This means you can type in OS/2 calls and immediately see
their results.

LISP parentheses make it easy to nest lists inside of lists, thus making
it easy to pass the results of one function call to an enclosing function
call. In the following example, we pass the result of an addition to a
macro which creates variables:

  > (define tmp (+ 880 100))
  980

To examine an object in LISP, type its name (without parentheses):

  > tmp
  980


Educating XLISP

Following what we did in Forth, the next step is obvious. We want to see
if DOSBEEP works the same as + (this is our litmus test for ease of
interaction with the OS/2 API):

  > (dosbeep 880 100)
  error: unbound
  function - DOSBEEP

Ugh! OS2XLISP doesn't even know what dosbeep is!

In fact, OS2XLISP.EXE doesn't really know much about OS/2 API calls. But
we can teach XLISP what dosbeep is. OS2XLISP does know about DLLs, and
this is its main difference from XLISP running under other operating
systems. For now, let's see if we can tell OS2XLISP what dosbeep is "on
the fly," while the program is running:

  > (define doscalls(loadmodule "DOSCALLS"))
  1910

  > (define dosbeep (getprocaddr doscalls "DOSBEEP"))
  1216544768

What's going on here? We have created a numeric variable, dosbeep, and set
it to some strange large number. But what exactly is this number? Maybe it
will look more reasonable in hex:

  > (ultoa dosbeep 16)
  "48830000"

C programmers may recognize (ultoa) as a function from the C standard
library. By displaying dosbeep in hexadecimal, you should see that
this variable contains an address in memory──the proc address of DosBeep,
to be exact. The LISP numeric variable dosbeep is actually a function
pointer, through which we can invoke the function:

  > (call dosbeep (word 880) (word 100))
  ; speaker beeps
  0

Great, it worked, but remember that we really wanted to say (dosbeep 880
100), just as we say (+ 880 100). Having to cast LISP numbers, which are
4-byte longs, to the 2-byte words DosBeep wants is too much trouble, and
so is having to explicitly say (call).

We can write a new LISP function that does what we want:

  > (define (dosbeep freq dur)(call dosbeep(word freq)(word dur)))
   DOSBEEP

  > (dosbeep 800 100)
  ; beep!
  0

Since I've been making it sound like dosbeep is some kind of patch you
slap on at run time, I should explain that OS/2 access is integrated into
normal LISP. Let's say that a note is a list of two items: a frequency and
a duration. We can assign a list of notes to the symbol bach, and write a
function (play), which takes such a list and calls (dosbeep) with each
note:

  (define bach '((880 100)(784 100) (880 1200)))
  (define (play notes)
  (mapcar(lambda (note)(DOSBEEP(car note)(cadr note)))notes))

  > (play bach)
  ; da-da-dum
  (0 0 0)

In the function (play), (mapcar) maps over the list notes, calling another
function for each note in the list. That other function has no name, being
a temporary nameless function created with (lambda), but it takes a note,
passes its first element (car) to (dosbeep) as a frequency, and its second
element (cadr) as a duration. This is similar to a C program passing
strcmp to qsort.


Variables and Symbols

Note that we have used (define), both to create variables and to create
functions. This is borrowed from the Scheme dialect of LISP. However, if
you are familiar with Common LISP or are working from a Common LISP
textbook, you can also use (setf) or (setq) for variables, and (defun) for
functions.

It is important to note that there isn't much of a difference between data
and functions. Dosbeep is a number, but also a function; in LISP, you're
really working with symbols, not variables. Symbols can have multiple
values attached to them. For example, the symbol dosbeep has a numeric
value (the address of DosBeep) and a functional value (the code we just
defined). A symbol can also have a property list (Microsoft Windows gets
the idea of window property lists from LISP).

With all these different values hanging off the data object, it shouldn't
be difficult to imagine how such a data object would be implemented. In
the C implementation of XLISP, a LISP data object is a C union that is
tagged with the object's type and has slots for the numeric value,
function value, object name, property list, and so on. Since these symbols
are intended to be linked together into lists, the symbol also has a slot
in which to put a pointer to the next object.

In OS2XLISP, therefore, even a "simple" number like 880 is represented as
one of these complex unions. When we pass 880 to the function (dosbeep),
we are really passing a pointer to an XLISP node.


Run-time Dynamic Linking

Because LISP objects are so different from "normal" variables, if OS/2
were passed one of these, it would become completely confused. So
conversions have to be done back and forth on the LISP side.

When I started porting XLISP to OS/2, I considered creating a new LISP
function for every OS/2 API call. It quickly became clear that, due to the
conversions needed between LISP data types and OS/2, I would have ended up
with a huge OS2XLISP.EXE. Also, I am lazy and I certainly did not want to
type in several hundred new functions.

OS/2 itself came to the rescue, with run-time dynamic linking. This is
quite different from the type of dynamic linking usually used in OS/2.
With the OS/2 functions DosLoadModule and DosGetProcAddr (Windows
programmers should know these as incarnations of LoadLibrary and
GetProcAddress), a program can pass ASCIIZ strings to OS/2 and get back a
function pointer. The function can then be invoked through this pointer.
In some programming languages, this technique is called "string
invocation" (because all you need to call a function is its ASCII string
name).

By using run-time dynamic linking, OS2XLISP can provide access to any OS/2
API call simply by providing a handful of functions. The function
(loadmodule) gets the module handle of a dynamic-link library (DLL),
(getprocaddr) gets a pointer to a function in the DLL, (call) calls the
function through its pointer, (c-call) does the same for functions that
use the CDECL calling convention, and (freemodule) relinquishes access to
a DLL.

Let's look at an example:

  > (define dosgetinfoseg(getprocaddr doscalls"DOSGETINFOSEG"))
  1201340416

The function DosGetInfoSeg expects to be called with two arguments: first,
a pointer to 2 bytes (a word), in which OS/2 will place the selector of
the global info segment (GIS), and second, a pointer to a word, in which
OS/2 will place the selector of the local info seg (LIS). We also need to
create some 2-byte variables for OS/2:

  > (setf gis (word 0) lis (word 0))
  0

I used (setf) here instead of (define) because it can perform multiple
assignments in one fell swoop. XLISP numbers are generally 4-byte longs,
so we use the OS2XLISP function (word) to cast them. To call the function,
we need to pass not gis and lis, but their addresses. We use the macro ^
which is shorthand for the OS2XLISP (addr) function, and which returns a
pointer to the data inside an XLISP node:

  > (call dosgetinfoseg ^gis ^lis)
  0

The (call) function returned zero (success), because that's what
DosGetInfoSeg returned. So, how could DosGetInfoSeg fail? Now we can
examine the contents of our LISP variables:

  > gis
  96

  > lis
  15

OS/2 has changed these LISP objects, right out from under LISP's nose.

Generally, these numbers would be worthless by themselves. But since
OS2XLISP knows about segmented protected memory, we can examine not just
the contents of these segments, but the segment attributes themselves.
For instance, (lsl) corresponds to the protected-mode instruction LSL,
which gives the last legal offset within a segment (the size minus one):

  > (lsl gis) ; how big is it?
  69          ; size - 70 bytes

  > (verr gis); look at it?
  T           ; yes

  > (verw gis); can I touch it?
  NIL         ; no way!

NIL is the LISP symbol for the empty list, or false; T is the symbol for
true. The functions (verr) and (verw) correspond to other protected-mode
instructions, and will be discussed later in the article.

As with UR/FORTH, in order to look inside these info segs, we need to make
far pointers and examine what they point to. Suppose we want the process
ID of whatever process is running in the foreground. Since we're typing
away at OS2XLISP, it's a good bet that OS2XLISP is running in the
foreground. This information is kept in the 2 bytes at offset 28 of the
GIS:

  > (peek (mk-fp gis 28) 'word)
  11

(mk-fp) takes a segment and an offset and in turn creates a far pointer.
The function (peek) takes a far pointer and an optional directive which
tells it what to fetch (you can fetch bytes, words, longs, IEEE floating-
point numbers, and ASCIIZ strings). The apostrophe in front of a symbol
tells LISP that we're actually interested in the symbol itself, not in any
value it may possibly have.


Chatting with DLLs

Run-time dynamic linking turned out to be a big plus; not only is all
access to the OS/2 API bottlenecked through a few functions rather than
scattered across hundreds of new ones, but it also means that users can
access anything from a dynamic-link library, not just the APIs supplied
in DOSCALLS, VIOCALLS, and KBDCALLS. When new functions come along,
they're instantly available, without users having to wait for an OS2XLISP
update to be issued.

Figure 5 shows OS2XLISP calling CHATLIB.DLL, a program presented in
"Design Concepts and Considerations in Building an OS/2 Dynamic-Link
Library" (MSJ Vol. 3, No. 3). OS2XLISP.EXE obviously has had no prior
knowledge of CHATLIB.DLL. It makes all the connections at run time, which
is a surefire way to build an extensible interpreter. What LINK normally
does, we defer until the last possible moment. As all good programmers
know, never do today what you can put off until tomorrow.

We have been using OS2XLISP interactively, by typing expressions directly
in from the keyboard, but you can also evaluate plain text files of
expressions using the (load) function. If, for example, the code in
Figure 5 existed in a text file called CHATLIB.LSP, you would tell XLISP:

  > (load 'chatlib)


Teaching C to XLISP

OS2XLISP can also call C functions in the dynamic-link library CRTLIB.DLL,
which comes with OS2XLISP. Earlier we used ultoa, though in LISP it looked
like (ultoa):

  > (define crtlib(loadmodule "CRTLIB"))
  2520

  > (define ultoa(getprocaddr crtlib "_ultoa"))
  97984426

  > (define (ultoa val base &aux (str(make-string 32 80)))
  (string-upcase (c-call ultoa val str (word base) 'str)))
  ULTOA

  > (ultoa dosgetinfoseg 16)
  "479B0000"

Like LINK, (getprocaddr) is case sensitive. Functions that use the Pascal
calling convention (such as DosBeep or KbdStringIn) should be presented
to (getprocaddr) in uppercase, whereas functions like ultoa that use the
cdecl calling convention should appear in lowercase with a leading
underscore.

Again, the symbol ultoa has both a numeric and a functional value. We can
even pass the numeric value to the function, in order to print out the
address in memory of the C ultoa function:

  > (ultoa ultoa 16)
  "5D71FAA"

There are several ways to create local variables in LISP. Here I used
&aux; str starts out as a local string of 80 spaces. After being passed to
(c-call), which passes it to the C function ultoa, it will contain
something useful, like "5D71FAA."

Since ultoa takes three arguments, why was (c-call) passed a fourth
argument, the symbol 'str? Most OS/2 functions by convention return an
unsigned short error code, but with a function like ultoa that returns
something else, like a char far *, we must tell (call) or (c-call) about
it. The function (c-call) returns a string from ultoa; this string is then
passed to the XLISP function (string-upcase).

Even the most famous of C functions, printf-which takes a variable number
of arguments-can be called from LISP, again through the magic of run-time
dynamic linking (and some minor miracles performed by the Common LISP
defmacro facility):

  > (define printf(getprocaddr crtlib "_printf"))
  97980568

  > (defmacro printf(mask &rest args)'(c-call printf , mask ,@args))
  PRINTF

  > (printf "printf lives at %Fp\n" printf)
  printf lives at 05D7:1098
  28


The Price of Flexibility

While (getprocaddr) has clearly given us a lot of flexibility, it's a
pain to use it every time we want to call a new function. Therefore
OS2XLISP has an (enumproc) function to enumerate all the functions
exported from a DLL; there is also an (install) function, which
automatically creates procaddr variables for all the functions exported
from a DLL:

  > (install "CHATLIB" t)
  SEND_MSG    GET_MSG_CNT
  LOGIN   GET_MSG
  LOGOUT      T

The install function even works for the DOSCALLS "module," which is
actually the OS/2 kernel masquerading as a DLL. For DOSCALLS.DLL routines,
DosGetProcAddr only works for "ordinal numbers," but OS2XLISP contains a
table of function names for DOSCALLS.

Because LISP code consists simply of LISP nodes consed together into
lists, it is easy to write code that generates code. Whenever we've wanted
to produce a new LISP function based on a routine in a DLL, we've gone
through pretty much the same steps: get the procaddr, then write a
function to (call) or (c-call) this procaddr with some parameters. We can
make LISP go through these steps for us, using the short macro (make-
call), shown in Figure 6.

The function (make-call) takes an OS/2 module handle, the name of the
routine, and any arguments to be passed to the routine. Nothing needs to
be quoted. For example:

  > (make-call viocalls viogetphysbuf buf(word handle))
  VIOGETPHYSBUF

OS2XLISP has created a new function, (viogetphysbuf), which takes two
parameters, buf and handle, and which will, when invoked, call the OS/2
function VioGetPhysBuf. Note that the (getprocaddr) operation happens only
once, when (viogetphysbuf) is created, not each time that (viogetphysbuf)
is called.

So far, the DLL-based functions we've used have needed only simple
parameters. What about OS/2 data structures? Consider (viogetphysbuf),
which requires a pointer to a PhysBufStruct. The OS2XLISP function (make-
struct), written in LISP, constructs OS/2 data structures from symbolic
directives in LISP lists, and (unpack-struct) constructs LISP association
lists from OS/2 data structures:

  ; the structure template
  (define PhysBufStruct '((ptr bufaddr)(long size)(word sel)(word sel2)))
  ; an instance of the structure, initialized
  (define physbuf(make-struct PhysBufStruct'(#xb8000 #x4000 0 0)))

Now we can call the function that (make-call) generated for us:

  > (viogetphysbuf physbuf 0)
  0

  > (define sel(assoc 'sel(unpack-struct PhysBufStruct physbuf)))
  (SEL 847)

  > (cadr sel) ; get second element of list
  847

We can verify that 847 really is a selector to the physical screen
buffer:

  > (define (writechar x y char)(poke (mk-fp 847(* 2 (+ x(* 80 y))))char))
  WRITECHAR

  > (writechar 79 24 #\a)
  97

This will display the letter 'a' in the bottom right corner of a CGA
screen.


Protection Faults

How does OS2XLISP protect itself from protection faults? While we were
using (poke) just now, you might have thought, "Peek, poke, protected
mode?" As the name implies, memory access in protected mode is protected,
but functions like (peek) and (poke) let the user try any place in memory.
If an OS2XLISP user asks to (peek) somewhere they have no business
looking, OS/2 will not only want to shut down the user's incorrect (or
malicious) program; it will send OS2XLISP itself a GP (general
protection) fault.

This presents a major problem for anyone writing an interpreter for
protected mode. Protected mode is great, but a user's illegal command
should simply cause the user's incorrect program to be terminated with an
error message. Having the LISP interpreter itself shut down with a GP
fault is not acceptable.

OS2XLISP offers two solutions to this problem. By default, OS2XLISP.EXE
will run itself as a debugged process under DosPTrace (full C and ASM
source code for OS2XLISP is part of the package, soif you are really
curious you can see how it's done), which is the only way to "catch" GP
faults in OS/2:

  > (poke #xb8000000 666)
  break: Segmentation Violation

This will enter OS2XLISP's built-in debugger where you can correct the
problem or back-trace in order to figure out what went wrong (though in
this case it's pretty obvious). Another way to enter the OS2XLISP debugger
is by hitting Ctrl-C or Ctrl-Break.

There is a problem with DosPTrace. DosPTrace is essentially the Microsoft
CodeView(R) "engine," opened up for OS/2 programmers. Unfortunately, only
one instance of DosPTrace can be running at one time. This means that if
you want to run OS2XLISP under CodeView, either at the same time as
CodeView or, more likely, run multiple sessions of OS2XLISP (perhaps to
experiment with system semaphores or shared memory), you need some way to
disable DosPTrace.

OS2XLISP can be invoked with the -x command line, which disables use of
DosPTrace and kicks in OS2XLISP's backup technique for self-preservation
against GP faults committed by XLISP programs──using the 286 protected-mode
instructions LAR, LSL, VERR, and VERW to check out a user's pointers
before trying to dereference them. LAR returns a segment's access rights,
LSL returns its size, VERR tells whether you can read it, and VERW tells
whether you can write to it. If you pass an invalid segment number to all
of these instructions, you do not get a GP fault.


Heapwalking

I found these instructions so useful that I brought them up to the surface
for LISP programmers to use too.

Figure 7 is a program called SEGS.LSP, which walks through all segments
visible from OS2XLISP. After loading SEGS.LSP, you can run the function
(total-segs), which will grind away and eventually print a statement of
the number of segments that are visible from OS2XLISP and how much memory
they take. This is easily run in the background, and if you instruct
(total-segs) that OS2XLISP will run in the background, it will switch to
idle priority and put up a pop-up when it's done. (Unfortunately,
multithreaded applications can't yet be written in OS2XLISP.)

The other function that can run from SEGS.LSP is (print-segs), which
passes a callback function to the function (enumsegs). Every time the
callback gets called, it prints out a segment's size, what it is
(readable code, writable data, or whatever), and whether the segment is
present in memory (see Figure 8).

Why are the segments in this example grouped by fours? In 286 protected
mode, a segment number (the numbers we got from DosGetInfoSeg, like 96 or
15) is not just any old number, but a structure composed of a number of
fields, as shown in Figure 9.

Thus, the numbers alone say something about these memory segments. For
example, the GIS has an RPL of 0 and is, naturally enough, in the global
descriptor table (GDT):

  > (ultoa gis 2) ; binary 96
  "1100000"

The LIS has an RPL of 3 (11 in binary) and occupies the first slot in the
local descriptor table (LDT):

  > (ultoa lis 2) ; binary 15
  "1111"

Since the requested protection level is in the bottom 2 bits of this
structure, which we think of as a segment number, clearly there will be
four different segment numbers which differ only by their protection
level. For example, segment 15 is overlayed by segments 12, 13, and 14.

The other segment attributes are kept in the segment access-rights byte,
which we retrieve with (lar). SEGS.LSP defines several macros to interpret
the bits returned by (lar). For instance, is the GIS writable data? Is it
present in memory?

  > (write? (lar gis))
  NIL

  > (present? (lar gis))
  T

We can make certain this works by checking one of OS2XLISP's own code
segments. First we verify that a given segment is not present in memory
by getting the address of a function, extracting the segment from the far
pointer with (fp-seg), taking the (lar) of the segment, and passing it to
(present?). We call the function, then we check the present bit again:

  > (present? (lar (fp-seg (addr(function sin)))))
  NIL
  ; maybe even see disk light come on as segment loads in

  > (sin (/ 3.14159 2))
  1

  > (present? (lar (fp-seg (addr (function sin)))))
  T

It works! And──admit it──all those parentheses aren't so intimidating after
all, are they?


Modula-2

In many ways, Modula-2 is a perfect match for OS/2. Just as OS/2 is built
around the ideas of dynamic linking and multiple threads, Modula-2 is
built around the ideas of the module and multiple processes.

Some of the parallels between OS/2 and Modula-2 are really quite
remarkable. For instance, the Modula-2 NEWPROCESS procedure, which creates
a coroutine that maintains its own context and can execute in parallel
with other coroutines, takes the following parameters:

  PROCEDURE NEWPROCESS (
  (* a procedure without parameters *)
  Code : PROC;
  (* stack *)
  Workspace : ADDRESS;
  (* size of stack *)
  WsSize : CARDINAL;
  (* will receive handle to coroutine *)
  VAR process : ADDRESS);

Anyone familiar with OS/2 will recognize that this is almost identical
to:

  DosCreateThread(void (far *procaddr)(void), unsigned far
     *thread_id, char far *stack);

Unlike C, concurrent programming is built into the language definition
for Modula-2. Niklaus Wirth designed Modula-2 as a high-level language
that could be used to write operating systems, device drivers, and so on.
This required a wide range of low-level facilities, as well as
multitasking.

Processes are defined in Modula-2 itself, rather than in extensions (such
as Concurrent C developed at Bell Labs), making portability possible. The
Modula-2 StartProcess call would be implemented in OS/2 using
DosCreateThread, in UNIX using fork, and perhaps you could implement
processes in MS-DOS by writing your own scheduler; but if a program calls
StartProcess rather than calling the underlying facility, it has a chance
of being ported to other environments.

Wirth recognized that a language like Pascal that insists on recompiling
an entire program each time a change is made is not suitable for large
development projects. Modula-2 thus features separate compilation,
allowing systems to be broken into individual components, but
maintaining complete type checking across module boundaries.

Note that separate compilation of dependent modules is not the same as
independent compilation. Even the ability to check the compilation
dates of dependent modules is built into the language. Consider the
difference between this and the ad hoc approach to modularity found in C.
The forthcoming ANSI C standard views function prototyping as optional,
and the inclusion of prototypes in include header files is still rather a
weak convention.


The Module

As might be expected from its name, Modula-2 raises the module to
fundamental importance. Looking back at the Modula-2 example in Figure 1,
note the statement "IMPORT DosCalls" and the call to DosCalls.DosGetModName.
If that function call looks like it is accessing a field in a data
structure, it was meant to. I could have said "FROM DosCalls IMPORT
DosGetModName," and then said DosGetModName without qualifying it, but I
wanted to emphasize that in Modula-2 modules are really akin to data
structures.

A module can export procedures, variables, constants, records, and types.
Looking ahead to SEM.DEF in Figure 10, we see that the module SEM exports
two types, which are called CountingSemaphore and BinarySemaphore, and
various operations on these types, such as P, V, Request, and Clear.

Equally important, a module can choose not to export something. A module
usually hides a set of data and provides a set of operators in order to
manipulate this data. For instance, while SEM.DEF exports the new type
CountingSemaphore, this is an "opaque type" in that its internals in
SEM.MOD are not exported (SEM.MOD also takes care of memory allocation for
any objects created of this type). Information hiding is as important as
information sharing; the goal is to keep the connections between modules
as small as possible. This modularization also pays off later when trying
to break up an application into several well defined dynamic-link
libraries.

There are two important components: definition modules and
implementation modules. A definition module represents the "Ministry of
the Exterior," that is, the face that a module shows to the outside world
(you might say that hypocrisy is a language feature of Modula-2). This
.DEF file is precompiled into a symbol table (.SYM file), which is used to
do type checking across module boundaries. Because a module can only
import another module's definition part, I need only write the definition
part for it to work. Definition modules can be used as a "language" in
which to write functional specifications.

The implementation module, of course, contains the code. In addition to
what I said earlier about information hiding, this is a perfect place to
put all operating-system-dependent code. An implementation module can
contain auto-initialization code as well, comparable to the INITINSTANCE
option for an OS/2 DLL. A module can contain submodules, such as Self
within SEM.MOD (see Figure 10). Finally, an implementation module would
be the place to bury the details of handling mutual exclusion for a
limited resource like VIO or KBD.

Because it has built-in multiprogramming, the Modula-2 standard
library expects to be called from multiple threads of execution. It is
therefore quite easy to write multithreaded applications and OS/2 or
Windows dynamic-link libraries in Modula-2. By contrast, until releasing
the MTDYNA (multithread) libraries in C 5.1, Microsoft has had to
publish a list of those C standard library functions that could not be
called from multithreaded applications, Windows, or OS/2 dynamic-link
libraries.


Semaphores

Modula-2 does not offer any semaphore/signal functions that represent an
improvement over the services available in OS/2. In the Stony Brook
Modula-2 implementation, the Processes module-as described by Niklaus
Wirth in Programming in Modula-2 (Springer-Verlag, 1983)-uses OS/2 threads
and semaphores. The module Processes defines the type Signal and the
operations Send, Wait, Init, Awaited, and StartProcess. The Stony Brook
documentation recommends using signals only if you are seeking
compatibility with other versions of Modula-2, since OS/2 offers a more
effective means of synchronization.

Although it is easy enough to write multithread code, and we can get the
StartProcess procedure-a portable way to launch multiple threads of
execution-Modula-2 doesn't provide much in the way of process
synchronization. On the other hand, simply having the Send and Wait
functions in Modula-2 does encourage us to think about higher-level
synchronization than is provided by OS/2 primitives like DosSemRequest.
There is a great gap between the powerful semaphore primitives supplied by
OS/2, and the great body of classical "academic" concurrency theory.
Modula-2 helps bridge that gap.


Dining Philosophers

In Figure 10, I have used Stony Brook Modula-2 to implement a solution to
the Five Dining Philosophers problem, which is a vivid demonstration of
the problems of multiprogramming first posed by the Dutch computer
scientist E.W. Dijkstra in 1965. The following is a summary from M.
Ben-Ari's Principles of Concurrent Programming (Prentice-Hall, 1982), p.
109-110 from which our solution borrows as well:

"The problem is set in a monastery whose five monks are dedicated
philosophers. Each philosopher would be happy to engage only in thinking
were it not occasionally necessary to eat. Thus the life of a
philosopher is an endless cycle: repeat think; eat forever... In the
center of the table is a bowl of spaghetti that is endlessly replenished;
there are five plates and five forks. A philosopher wishing to eat enters
the dining room, takes a seat, eats, and then returns to his cell to
think. However, the spaghetti is so hopelessly entangled that two forks
are needed simultaneously in order to eat. The problem is to devise a
ritual (protocol) that will allow the philosophers to eat... Each
philosopher may use only the two forks adjacent to his plate. The
protocol must satisfy the usual requirements: mutual exclusion (no two
philosophers try to use the same fork simultaneously) and freedom from
deadlock and lockout (absence of starvation──literally!)."

Obviously, the five philosophers represent five processes (or, in OS/2
terms, threads). The five forks represent critical resources over which
five semaphores will ensure mutual exclusion. Since there are five
philosophers (threads) and five forks (resources), and each philosopher
needs two forks to eat, two philosophers can eat at any given time; the
forks can't be protected with a simple critical section, because then only
one philosopher could be eating at one time.

Since the five philosophers do the same thing, the five processes all
execute the same piece of code: in Figure 10, the procedure philosopher.
Because both OS/2 and Modula-2 treat concurrent processes as parameterless
procedures (and because philosophers ought to know who they are),
each thread executing through the procedure philosopher determines its
own identification by calling self. In SEM.MOD, self is implemented by
pulling the thread ID number out of the local info seg.

Life is an endless circle, therefore the philosopher procedure consists
mostly of an endless loop. However, for any given philosopher, certain
properties do not change: the philosopher's identity, and which forks he
should use. These are determined once and deposited in the philosopher's
memory (that is, left on each thread's stack).

Once inside its loop, each philosopher sits in a cell and thinks for
awhile, then tries to enter the dining room. If there are five
philosophers (the constant NUM_PHILOSOPHERS can be changed), then there
can be four philosophers in the room. The philosopher executes the code
P(room). Here room is a counting semaphore, and P is the Wait operation
(the Dutch for try is "proberen"). When the philosopher is ready to
return to a cell, he will execute the code V(room); V is the Release
operation (the Dutch for release is "vrygeven").

The implementation of counting semaphores is contained in the module
SEM.MOD. It borrows heavily from the OS/2 simulation of counting
semaphores in "Using OS/2 Semaphores to Coordinate Concurrent Threads of
Execution" (MSJ, Vol. 3, No. 3).

No OS/2-dependent code appears in DINING.MOD. All system-dependent code is
isolated within SEM.MOD (and in the library modules written by Stony
Brook Software, of course; all source code for the libraries comes with
the development package). Yet, this is simply an OS/2 program with
threads and semaphores.

Once in the dining room, a philosopher waits until he can pick up his left
fork, then waits again until the right fork is available. Deadlock is not
a problem here. The counting semaphore room keeps the number of
philosophers trying to access forks below the number of forks.
Therefore, at least one philosopher may eat at any given time. An array
of binary semaphores represent the forks. (Binary semaphores are simpler
than counting semaphores, and can be implemented as straight calls to
DosSemRequest and DosSemClear.) After eating, each philosopher releases
the forks, leaves the room, and starts over again.

To start the five philosopher threads in the initialization code in
DINING.MOD, we say:

  FOR i := 1 TO NUM_PHILOSOPHERS DO
     StartProcess(philosopher, 2048);
  END;

In order to ensure that all the philosophers start together, we
simulate the cobegin-coend construct of concurrent programming. In
SEM.MOD, these are implemented as simple calls to DosEnterCritSec and
DosExitCritSec. As long as the main process is in its critical section, it
can start processes, but they can't run; as soon as it exits its critical
section, all the processes start together.

I kept running into a problem while writing DINING.MOD. All my threads
would start properly, but for some reason the program would exit back to
OS/2 almost immediately. The problem was that the main thread, having
brought the philosophers to life, had nothing else to do and would exit.
I introduced another semaphore called fini; the main thread waits on
this semaphore, but none of the philosophers ever release it. I suppose
one could say that this signifies that philosophizing always goes on and
has no end. To finish off the otherwise endless cycle of thinking and
eating, Ctrl-C needs to be hit.

The philosophers show what they're doing by issuing little statements to
the screen:

   thinking: 4
   eating:   2
   eating:   1
   eating:   4
   thinking: 5
   thinking: 3
   thinking: 2
   thinking: 1
   eating:   3
      ∙
      ∙
      ∙

To keep these strings from interfering with each other on the screen (e.g.
"thieateateating: 4inginging: 2 : 1 : 4"), we have to use yet another
semaphore, called iosem. Since this has no bearing on the dining
philosophers problem itself, all code concerned with I/O is isolated in
an internal module within DINING.MOD.

Another problem cropped up while writing DINING.MOD: occasionally the
program would be terminated with a GP fault. After spending some time
trying to decipher the OS/2 register dump, I remembered that another
feature of Modula-2 is optional run-time error checking, so I turned on
the /CHECK compiler switch and recompiled. The next time I ran the
program, it provided a message about an out-of-bounds array access from
line 79 of DINING.MOD and, sure enough, that's where the bug was. This
alone makes Modula-2 worthy of consideration.

After getting the dining philosophers program running, the next step was
to split off SEM.MOD as an OS/2 dynamic-link library, SEM.DLL. This
required writing a DEF file for LINK-not to be confused with the Modula-2
DEF file also shown in Figure 10, along with the necessary OS/2 commands
(M2 is the Stony Brook Modula-2 compiler). In addition to running several
concurrent copies of DINING.EXE, all of which are dynamically linked to
SEM.DLL, I also called SEM.DLL from C (remember that SEM.MOD takes care
of allocating space for the counting semaphore itself):

  typedef void far *CountingSemaphore;
              ∙
              ∙
              ∙
CountingSemaphore room;

  /* pointer to pointer */
  CSInit(&room, 4);
              ∙
              ∙
              ∙
  P(room);

Stony Brook Modula-2 comes with what must be the world's largest
collection of options for interfacing with other languages. Whereas most
C compilers will give you the simple division of CDECL and Pascal calling
conventions, Stony Brook Modula-2 has the optional keywords REVERSED (push
parameters in reverse order), LEAVES (procedure leaves parameters on the
stack), ALTERS (specify which registers a procedure alters), RETURNS
(specify the registers in which a procedure returns its value), and
VARIABLE (procedure accepts a variable number of arguments).

For instance, to import the C function fprintf into Modula-2, you would
use REVERSED, LEAVES, VARIABLE, and ALTERS (AX,BX,CX,DX,ES). But the real
advantage is in being able to use, for instance, LEAVES without having to
use REVERSED as well; this would be just the ticket for writing a push
function that would intentionally leave data on the stack to be consumed
by other functions.

Stony Brook Modula-2 interfaces very nicely with C──unusual for a product
whose ads feature Marc Antony saying, "I have come to bury C, sir, not to
praise it."


BASIC

BASIC has grown up a lot since John Kemeny and Tom Kurtz invented the
language in 1964. You no longer need line numbers, there are genuine
functions and subroutines, and certainly it's a sign of maturity that
Microsoft BASIC 6.0 is a compiler (not an interactive interpreter) that
comes with a full range of inscrutable job-control-languagelike command-
line switches like /Zi, /ah, and /d/e/x.

In one way, BASIC fits right into OS/2; given the BASIC tradition of
creating programs that require a run-time environment──BRUN.COM in
earlier versions of Microsoft BASIC, for example──in OS/2 it's natural to
make this run-time environment a dynamic-link library (BRUN60EP.DLL).
Compare this to the more shaky transition that C, with its heritage of
standalone applications, is making towards DLLs; CRTLIB.DLL is a rather
under-publicized option in C 5.1. Even the run-time environment for
real-mode BASIC 6.0 seems to have been redone to closely resemble a DLL.

In any event, BASIC is still a beginner's language. But for precisely this
reason, BASIC has many powerful features. It may not occur to a beginner
that space must be allocated for strings in memory. Therefore, BASIC takes
care of this itself:

  print "hello " + "world"

This is equivalent to saying:

  puts(strcat("hello ","world"));

which you most certainly cannot say, as "world" will overwrite some
memory which does not belong to it. In contrast, BASIC takes total control
over managing string space, making BASIC strings easy to use, intuitive,
and therefore powerful.

Note also that the one line of BASIC is a complete program, whereas the
equivalent program in Modula-2 or even C takes several lines (or perhaps
100 if you're programming for Windows). Complex programs that can easily
be written in C, on the other hand, are more difficult to write in BASIC.


Flexible Syntax

The PRINT statement is another example. It has many optional parameters,
but if you just say to PRINT something you get reasonable output. Back in
Figure 1, printing out the module number and name in BASIC was more
concise than in any of the other languages.

The BASIC example in Figure 1 also shows that, for the most common cases,
variables are automatically declared "by appearance." The first
occurrence of a variable named i% means it is an integer variable; buf$
is a string variable because its name ends with a dollar sign. The symbol
at the end of a name is called a "type tag."

BASIC strings aren't ASCIIZ strings, so in those rare moments when you get
an ASCIIZ string back from OS/2, you must figure out how to print it (in
Figure 1, I used rtrim$ to remove trailing blanks). Creating an ASCIIZ
string, on the other hand, is no problem. You simply append a zero
delimiter at the end:

  asciiz$ = "hello world" _ + chr$(0)

Being able to make string assignment and concatenation this intuitively
does mean that BASIC may have to move strings around in memory. Normally
this doesn't matter, but if you need to get the address of a variable-
length string, you better use it immediately. In Figure 1, note that
taking the address of buf$ requires two function calls; first is the
segment with varseg, then we get the string address with sadd.

This is a good example of the trade-off involved in BASIC programming.
Tasks that should be simple are simple, but a more complex task can be a
real chore. It is, however, valuable to have at least one language that
makes this trade-off.


Nothing to Declare?

Advanced BASIC programs rely heavily on Microsoft BASIC 6.0's DECLARE
statement, which is the gateway to other languages such as C, assembler,
and Modula-2, to dynamic-link libraries, and the OS/2 operating systems.

Figure 1 showed a typical declaration from the header files supplied by
Microsoft. Once declared, DosGetModName can be used like any other
function. The BYVAL keyword in the declaration is very important. By
default, BASIC passes the offset (near address) of a variable, so BYVAL
instructs it to pass "by value." Another keyword, SEG, tells BASIC to
pass "by reference," but to use a full 4-byte segmented address. Once an
argument has been declared this way, you don't need to worry about taking
its address; see INFOSEG.BAS, the little program shown in Figure 11.

There are several things to note here. Since we usually ignore any error
code from DosGetInfoSeg, we can DECLARE it as a subroutine rather than a
function, and since it's a subroutine, we don't need parentheses around
its arguments when we call it. By using the SEG keyword in the
declaration, BASIC will take the far address of the variables for us. We
don't need to declare these variables-we just use them (what a novel
idea). All in all, it can lead to some very natural-looking code.

By declaring DosGetInfoSeg as a subroutine and calling it without
parentheses, we have in effect created a new BASIC keyword.

Another useful OS/2 subroutine in which we might ignore an error code is
DosBeep, an especially handy function since the SOUND and PLAY statements
have been disabled for protected-mode BASIC:

  declare sub Honk alias _
  "DOSBEEP" _
  (byval freq%, byval dur%) Honk 880, 100

Once again, we've created something that works just like a built-in BASIC
statement. Because of the ALIAS option to DECLARE, we can link to DosBeep,
but call it something else in our BASIC programs.


A Taste of "Macro BASIC"

The OPEN statement is fairly typical of BASIC, having lots of parameters
that are optional, and a reasonable default behavior if you don't use any
options. For example,

  open "foo.bar" as #1

simply opens the file for read/write random access. This is fine for
beginners and for simple applications. But you can also say something
like:

  open "foo.bar" for output _
  access write lock _
  write as #1

Furthermore, the string used for the filename can also hold parameters
that completely change what OPEN does:

  open _
  "com1:9600,n,8,1,bin" _ as #1

One of the new features for protected-mode BASIC adds another option to
the OPEN statement, OPEN "PIPE:", making it easy to use the OS/2 pipe
facility in which programs communicate via standard file I/O calls:

  open _ "pipe:exehdr brun60ep.dll _ 2>nul" as pipe

The program in Figure 12, ENUMPROC.BAS, prints out the names of all
functions exported from an OS/2 DLL. For instance,

  C>enumproc \dll\brun60ep

prints out all the functions in the BASIC run-time library (which have
names like B$DRAW and B$POKE). ENUMPROC.BAS doesn't know anything about
the structure of a DLL file, but it does know that EXEHDR.EXE knows about
these files, so it makes EXEHDR's output its input. Since the output from
EXEHDR is made into a BASIC variable, we could manipulate it in any way we
want. Thus, the OPEN "PIPE:" statement permits one program to use another
program as a subroutine.

In ENUMPROC.BAS we also use OS/2 redirection ("2>nul") to discard a few
lines of output that EXEHDR sends to stderr, as well as BASIC's important
ability to determine when EXEHDR has finished feeding us input. While this
is often difficult to determine when using OS/2 pipes (resulting in hung
programs), in BASIC all we need to say is:

  on error goto closefile

(showing that BASIC can occasionally look amazingly like psuedocode). We
get input from EXEHDR in an endless loop (do while 1), but when our
attempt to get input from EXEHDR eventually fails, BASIC raises an error.
Because we've set up an error trap, our error handler (labeled closefile)
gets called and we can close up shop.


The Three E's

One powerful feature of BASIC is its handling of "the three E's"; errors,
exceptions, and events. Perhaps derived from the similar ON statement
found in PL/I, the ON statement in Microsoft BASIC allows you to create
"daemons" that check for a wide range of run-time errors, asynchronous
events, and signals.

At no point in Figure 12 do we explicitly check for an end-of-output from
EXEHDR. BASIC does all checking behind the scenes──though not as a
background thread; instead, the compiler inserts CALLs to an error-
checking routine. This makes the code easier to read since the normal
course of events is not cluttered up with a lot of IF statements. Code
that uses an error daemon is generally a lot easier to understand than
code written using defensive programming.

Stony Brook Modula-2 has a similar facility in its Error module, and
something like this can certainly be cooked up in C using setjmp/longjmp.
However, BASIC's ON statement provides the easiest-to-use error
handling, and it uses the same technique to handle nonerror conditions,
such as a timer going off (ON TIMER), a key being struck (ON KEY), data
arriving on a COM port (ON COM), or, in protected mode, a signal being
received (ON SIGNAL).

The program listed in Figure 13, PEEKER.BAS, presents another interesting
aspect of error-checking in protected-mode BASIC. As you may remember
from the above discussion of OS2XLISP, PEEK and POKE statements present
a problem in OS/2 protected mode. PEEKER.BAS runs in an endless loop,
asking the user to input a segment and an offset. If the byte at
segment:offset can be read, its value is printed out. Otherwise, BASIC
catches the deadly GP fault, and fields it to the error handler, which
prints out "Permission denied." To represent an illegal memory access in
protected mode, BASIC uses the same error number, 70, that it uses to
represent an illegal file access. It makes sense to treat memory as a
shareable, but partially locked resource like a file.

If you run PEEKER.EXE under CodeView, you'll see that BASIC uses VERR,
VERW, and LSL before performing the PEEK so the error is being
prevented rather than caught.


Porting to OS/2

Speaking of PEEK and POKE, a lot of what's called machine-dependent code
is actually dependent on the operating system. The manual for BASIC 6.0
and Microsoft QuickBASIC 4.0 is a case in point, containing a note such as
"This program contains hardware-specific instructions. It works correctly
on IBM(R) PC/XT(R), and PC/AT(R) computers." In fact, such programs
will work correctly under MS-DOS, but not on an IBM AT under OS/2.

The manual entry for DEF SEG shows how to use DEF SEG, PEEK, and POKE to
toggle the Caps Lock key. This program, which peeks and pokes into memory
locations outside the program's purview, will not work under OS/2 (you can
verify this by keying the numbers into PEEKER.EXE). In order to toggle the
Caps Lock key in OS/2, you should use DosDevIOCtl or KbdSetStatus.

The manual entry for CALL ABSOLUTE uses INT 11H to determine if a math
coprocessor is present. The easiest way to do this under OS/2 is to use
the DosDevConfig function, which is a very close match to the equipment-
list service provided by ROM BIOS interrupt 11H.

However, since the aim of the manual example is to demonstrate how CALL
ABSOLUTE can execute a machine-language program stored in a BASIC variable,
we can do that too under OS/2.

In the code listed in Figure 14, SMSW.BAS, a machine-language subroutine,
is stuffed into a BASIC string and the string is executed. The executable
string, named code$, contains the protected mode SMSW instruction, which
retrieves the 286 machine status word. If bit 1 of the word is set, a
coprocessor is present; if bit 2 is set, there is no coprocessor and/or
someone has installed a handler for INT 7 (no numeric processor), so the
coprocessor is being emulated in software.

Before executing the string with CALL ABSOLUTE, we must remember that
protected mode prevents us from loading a data segment selector into the
CS register. Therefore, in SMSW.BAS I called the OS/2 function
DosCreateCSAlias, which takes a data segment selector and returns a
selector to an alias which is executable.

It turns out that when you are using CALL ABSOLUTE in BASIC under OS/2,
this isn't strictly necessary. Running PEEKER.EXE under CodeView, it
becomes clear that the compiler calls DosCreateCSAlias for you. Since
this isn't documented, it makes sense to call DosCreateCSAlias anyway.
If you do, the second call will fail when CALL ABSOLUTE calls it again
because you cannot get a CS alias for something that's already a code
segment.

This is probably a good a time to point out that CodeView knows all about
BASIC, which not only helps debug BASIC programs, but also helps in
snooping around the code generated by the compiler. CodeView has a built-
in BASIC expression evaluator, to make queries like:

  > ? varseg(code$) &H002f


Bringing Up BASIC

There is now a much easier way to add new capabilities to BASIC than
stuffing machine language code bytes into strings. Because the Microsoft
BASIC compiler produces standard .OBJ files, and because of the important
DECLARE statement, we can link in any code contained in standard .OBJ
files. Figure 15 shows the code for both PROTMODE.ASM and SMSW2.BAS, and
shows a better way to port the SMSW instruction to BASIC.

Notice how BASIC's DECLARE SUB lets me create the natural-looking SMSW x%,
rather than having to write x% = SMSW. Because MASM 5.1 has built-in
support for high level languages, once we give the

  .MODEL MEDIUM,BASIC

directive, MASM will take care of almost all the details of creating
BASIC-compatible code.

Knowing that we can easily use the SMSW instruction in BASIC, you won't be
surprised to hear that we can just as easily add our old friends LSL,
LAR, VERR, and VERW to BASIC's repertoire. PROTMODE.ASM also contains
code for an LSL function. Used from BASIC it looks like:

  declare function LSL& _ alias "_LSL" (byval x%)
  print "LSL for some BASIC _ data segment is"; _LSL&(varseg(x%))

Since BASIC integers are always signed, if the LSL function returned a 2-
byte integer, we would see negative numbers whenever we print the LSL of a
segment larger than 32767 bytes. Therefore the BASIC LSL function returns
a 4-byte long, as indicated by the ampersand type tag.


What's In, What's Out

A number of things were left out of the OS/2 version of BASIC. The BASIC
functions and statements that provide access to ports are missing. (We
could add these ourselves, just as we added other assembler instructions,
though we would also need an IOPL statement in an OS/2 .DEF file.) Also
omitted were the BASIC sound statements (we've already seen we can use
DosBeep), and the IOCTL statement (we can use DosDevIOctl instead).
Further, no header files for the OS/2 VIO and KBD functions are provided
even though we can dynamically link to them, because going outside
BASIC's standard I/O statements might pull the rug out from under BASIC.

Fortunately, the extremely rich collection of BASIC graphics routines has
been implemented for OS/2 protected mode, providing another way to do
graphics under OS/2 until Presentation Manager is available. The bad news
is that only CGA screen modes are available, and graphics will not be
drawn to the screen while a BASIC program is in the background. You can
forget about plotting the Mandelbrot set in the background, at least for
now.

Furthermore, while a program called GW-BASIC(R) is included with OS/2, the
famous line-numbered BASIC interpreter does not run in protected mode.
While Microsoft QuickBASIC 4.0 is included with BASIC 6.0, this
interactive programming environment also will not run in protected mode.

Finally, the current BASIC run-time library is not reentrant, which means
you can't use BASIC 6.0 to write multithreaded applications.


The Narrow View

We have taken a rather parochial view of these four products, discussing
UR/FORTH without mentioning process control, and somehow discussing LISP
without using the phrase "artificial intelligence." And I don't know how
it happened, but the word "education" didn't appear once during the
discussion of BASIC.

We did get a clue to the perennial question, "Why are there so many
languages?" Each of the languages we've examined has a number of really
superb ideas.

Forth provides user-programmable syntax, user control of the compiler
and interpreter, words that communicate via the stack, and everything is a
word.

LISP code takes the same form as lists and the language manipulates
symbols rather than variables. Dynamic linking is deferred until run time.

In Modula-2, the module itself is almost a new data type, concurrent
programming is built into the language, and there is high-level
synchronization between modules.

BASIC manages string space for you, handles errors without cluttering your
program with error-checking, and most common operations have optional
parameters with variables automatically declared.

Figure 16 presents a comparison chart of the products we have discussed.

OS/2 promotes interactive software development, even with batch-mode
compilers. You keep your protected-mode editor running in its own
session, and toggle instantly between editor, debugger, compiler, and
your running/crashing program. Because crashes are less deadly than in
real mode, you are more likely to carry out ad hoc experiments.

OS/2's multitasking capabilities, and its extensibility via DLLs, means
a whole new generation of incredible compilers, interpreters, and
integrated programming environments. We are already beginning to see
this happen. In fact, rather than being stranded on a desert island, we
may be returning from the island to civilization.


Vendor Information

UR/FOTH Version 1.1:
Laboratory Micosystems, Inc.
P.O. Box 10430
Marins Del Rey, CA 90295
(213) 306-7412

OS2XLISP (XLISP Version 2.0, OS/2 extensions Version 1.10):
Available on CompuServe Microsoft Systems Forum (GO MSSYS)
(will soon be available on BIX as well)

Stony Brook Modula-2 Development System Version 1.12:
Sony Brook Software Inc.
Forest Road, P.O. Box 219
Wilton, NH 03086
(603) 654-2525

Microsoft BASIC Compiler 6.0:
Microsoft Corporation
16011 NE 36th Way
Box 97017
Redmond, WA 98073-9717
(800) 426-9400


Figure 1:  An OS/2 Rosetta Stone

In C (using Microsoft C 5.1)

/* from bsedos.h */
USHORT APIENTRY DosGetModName(HMODULE, USHORT, PCHAR);

/* enumdll.c */
#include <stdio.h>

#define INCL_DOSMODULEMGR
#include "os2.h"

main()
{
    char buf[128];
    register unsigned int i;

    for (i=0; i<=0xFFFF; i++)
        if (! DosGetModName(i, 128, buf))
            printf("%u\t%s\n", i, buf);
}
───────────────────────────────────────────────────────────────────────────

In Forth (Laboratory Microsystems 80286 UR/FORTH 1.1 for OS/2)

\ no header files

CREATE BUF 128 ALLOT             \ create a buffer

: ENUMDLL                        \ define a new word
  65535 0 DO                     \ for i=0 to 65535 do
    I 128 DS0 BUF DOSGETMODNAME  \ DosGetModName(i,128,ds:buf)
    0= IF                        \ if no error
       CR                        \ carriage return
       I 5 U.R 3 SPACES          \ display i nicely
       BUF -ASCIIZ COUNT TYPE    \ convert ASCII string and print it
    THEN
  LOOP ;

ENUMDLL                          \ invoke the word
───────────────────────────────────────────────────────────────────────────

In LISP (OS2XLISP, Version 1.10)

; no header files

(define doscalls (loadmodule "DOSCALLS"))
                  "DOSGETMODNAME"))
(define dosgetmodname (getprocaddr doscalls
                       "DOSGETMODNAME"))
(define buf (make-string 32 128))  ; string of 128 spaces

(dotimes
    (i #xFFFF)
    (if (zerop (call dosgetmodname (word i)
        (word 128) buf))
    ; then
        (format stdout "~A\t~A\n" i buf)))
───────────────────────────────────────────────────────────────────────────

In Modula 2 (Stony Brook Software Modula-2 for OS/2):

(* from doscalls.def *)
PROCEDURE DosGetModName ['DOSGETMODNAME'] (
  ModuleHandle  : CARDINAL; (* the module handle to get name for *)
  BufferLength  : CARDINAL; (* the length of the output buffer   *)
  Buffer        : Asciiz    (* the address of output buffer      *)
    ) : CARDINAL;

(* enumdll.mod *)
MODULE ENUMDLL;

FROM SYSTEM IMPORT ADR;

FROM InOut IMPORT Write, WriteLn, WriteString, WriteInt;

IMPORT DosCalls;

VAR
  i   : CARDINAL;
  buf : ARRAY [0..128] OF CHAR;

BEGIN
  FOR i := 0 TO 65535 DO
   IF DosCalls.DosGetModName (i, 128, ADR(buf)) = 0 THEN
            WriteInt(i, 5); Write(CHAR(9)); WriteString(buf); WriteLn;
        END
    END;

END ENUMDLL.

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

In BASIC (Microsoft BASIC Compiler 6.0)

' enumdll.bas
' compile with /d so you can Ctrl-Break

' from bsedospe.bi
DECLARE FUNCTION DosGetModName%( _
        BYVAL P1  AS INTEGER,_
        BYVAL P2  AS INTEGER,_
        BYVAL P3s AS INTEGER,_
        BYVAL P3o AS INTEGER)

' include file for Program Execution Support
REM $INCLUDE: 'bsedospe.bi'

buf$ = SPACE$(128)

FOR i% = 0 TO 32765      ' only signed integers available
  Result = DosGetModName%(i%, 128, VARSEG(buf$), SADD(buf$))
  IF Result = 0 THEN
      PRINT i%, RTRIM$(buf$)
      buf$ = SPACE$(128) ' have to reset buffer,
      because
  END IF                 ' BASIC doesn't know about ASCIIZ
NEXT i%

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

Sample Output

    140     A:\HARDERR.EXE
    220     D:\OS2\DLL\BMSCALLS.DLL
    380     D:\OS2\SYS\SHELL.EXE
    630     A:\SWAPPER.EXE
    750     D:\OS2\DLL\BKSCALLS.DLL
    930     D:\OS2\DLL\ANSICALL.DLL
    1210    D:\OS2\DLL\EPS-LIB.DLL
    1230    D:\OS2\DLL\MOUCALLS.DLL
    1240    D:\OS2\DLL\QUECALLS.DLL
    1330    D:\OS2\DLL\SESMGR.DLL
    1340    D:\OS2\DLL\BVSCALLS.DLL
    1380    D:\OS2\DLL\VIOCALLS.DLL
    1390    D:\OS2\DLL\KBDCALLS.DLL
    1480    D:\OS2\DLL\DOSCALL1.DLL
    1490    D:\OS2\DLL\NLS.DLL
    1550    D:\OS2\DLL\MSG.DLL
    1590    D:\OS2\E\EPSILON.EXE
    2240    D:\OS2\DLL\MONCALLS.DLL
    2320    D:\URFOS2\FORTH.EXE
    2960    D:\OS2\DLL\CRTLIB.DLL
    2980    E:\XLISP\NEW\OS2XLISP.EXE
    3190    D:\OS2\DLL\ALIAS.DLL
    3630    D:\OS2\SYS\CMD.EXE


Figure 2:  A better Forth implementation of the Rosetta Stone program.

FORGET ENUMDLL      \ get rid of previous definition
CREATE BUF 128 ALLOT
: MODULE? ( n -- flag )   128 DS0 BUF DOSGETMODNAME 0= ;
: .ASCIIZ ( addr -- )     -ASCIIZ COUNT TYPE ;
: .MODULE ( n -- )        CR   5 U.R   3 SPACES   BUF .ASCIIZ ;
: ENUMDLL ( -- )          65535 0 DO I MODULE? IF I .MODULE THEN LOOP ;


Figure 3:  Code Listing for Threads.4th

\ threads.4th
\ Andrew Schulman 23-July-1988
\ revised 27-July: using GETNUM from input stream, not stack
\ revised 3-August: using new TASKER.BIN (08-02-88)

TASKER

\ ============================================================
\ note that each thread has its own view of the LDT
VARIABLE GIS   VARIABLE LIS
DS0 GIS DS0 LIS DOSGETINFOSEG DROP

: SELF ( -- thread# of current thread )   LIS @ 6 @L ;

: CHAR ( n -- char )   DUP 9 > IF 55 ELSE 48 THEN + ;
\ ============================================================

\ stolen from os2demo.scr
VARIABLE SEED
: (RAND)   SEED @ 259 * 3 + 32767 AND DUP SEED ! ;
: RANDOM   (RAND) 32767 */ ;

: RY ( -- y ) ?YMAX 1+ RANDOM ;
: RX ( -- x ) SELF 4 MOD 20 * 20 + RANDOM SELF 4 MOD 20 * MAX;

\ ============================================================
\ use UR/F semaphores, instead of directly calling OS/2
SEMAPHORE SEM

VARIABLE ID
VARIABLE ATTR   12 ATTR !

\ display thread ID -- multiple threads will execute through
\ this same code if the semaphore is removed, you can see
\ each thread grab wrong thread id#
: SHOW-ID ( -- )
    BEGIN
        SEM SGET
        SELF CHAR ID !
        \ use VioWrtCharStrAtt because it is properly
        \ reentrant and also does not affect the cursor
        \ position
        DS0 ID   1   RY   RX   DS0 ATTR   0 VIOWRTCHARSTRATT   DROP
        SEM SREL
        PAUSE
    AGAIN ;
\ ============================================================

\ simulate the cobegin-coend construct, so all threads start together
: COBEGIN ( -- )   DOSENTERCRITSEC DROP ;
: COEND ( -- )     DOSEXITCRITSEC DROP ;

\ ============================================================
2048 128 TCB THREAD1
2048 128 TCB THREAD2
2048 128 TCB THREAD3
2048 128 TCB THREAD4

: (START) ( tcb -- )   DUP START SHOW-ID WAKE ;

: START-IT ( -- )
    COBEGIN
        THREAD1 (START)
        THREAD2 (START)
        THREAD3 (START)
        THREAD4 (START)
    COEND ;
\ ============================================================

\ important to not stop thread while it's holding the semaphore
: (STOP) ( tcb -- )
   SEM SGET
   STOP
   SEM SREL ;

: STOP-IT ( -- )
    THREAD1 (STOP)
    THREAD2 (STOP)
    THREAD3 (STOP)
    THREAD4 (STOP) ;
\ ============================================================

\ get a number from input stream, put on stack
: GETNUM ( -- n )   BL WORD NUMBER? 2DROP ;

\ ============================================================
\ UR/F tasks are OS/2 threads, so we can manipulate a task
\ using OS/2 calls

\ important to claim semaphore, so we don't suspend a thread
\ while it's holding on to the semaphore !
: SUSPEND ( -- )   GETNUM SEM SGET DOSSUSPENDTHREAD SEM SREL DROP ;
: RESUME ( -- )    GETNUM DOSRESUMETHREAD DROP ;

\ if entered from keyboard, suspend/resume all background tasks
: CRIT ( -- )      DOSENTERCRITSEC DROP ;
: XCRIT ( -- )     DOSEXITCRITSEC DROP ;

\ if entered from keyboard, also suspend/resume all background tasks
: GETSEM ( -- )    SEM SGET ;
: RELSEM ( -- )    SEM SREL ;
\ ============================================================

VARIABLE PRTY

: GET-PRTY ( -- )   2 DS0 PRTY GETNUM DOSGETPRTY DROP PRTY @ . ;

: SET-IDLE ( -- )  2 1 0 GETNUM DOSSETPRTY DROP ;
: SET-REG ( -- )   2 2 0 GETNUM DOSSETPRTY DROP ;

\ warning:  if you set a background thread to time-critical
\ from the keyboard, you never get the keyboard back!

\ ============================================================
: HELP ( -- )
    CR ." Commands:" CR
    ." HELP" CR
    ." CRIT" CR
    ." XCRIT" CR
    ." GETSEM" CR
    ." RELSEM" CR
    ." SUSPEND <thread #>" CR
    ." RESUME <thread #>" CR
    ." GET-PRTY <thread #>" CR
    ." SET-IDLE <thread #>" CR
    ." SET-REG <thread #>" CR
    ." STOP-IT" CR ;
\ ============================================================

CLS
START-IT
THREAD1 (STOP)        \ leave room for typing in commands
PAUSE
CLS
HELP


Figure 4:  Code Listing for xy.4th

\ xy.4th for UR/Forth
\ get current cursor location, by programming the 6845 CRTC

80 CONSTANT COLUMNS

HEX
: CUR-LOC  ( -- reg14 reg15 )  E 3D4 PC!  3D5 PC@  F 3D4 PC!  3D5 PC@ ;
: MK-WORD  ( w1 w2 -- w )  SWAP FF * + ;
: COL-ROW  ( w -- x y )    DUP  COLUMNS MOD 1+  SWAP  COLUMNS / 1+ ;
: CURS  CUR-LOC  MK-WORD  COL-ROW ;
DECIMAL


Figure 5:  Dynamically Linking to a DLL from OS2XLISP

; chat.lsp
; Run-time dynamic linking to CHATLIB.DLL from OS2XLISP

(define chatlib     (loadmodule "CHATLIB"))
(define login       (getprocaddr chatlib "LOGIN"))
(define get-msg-cnt (getprocaddr chatlib "GET_MSG_CNT"))
(define my-id       (call login))
(define msg-cnt     (call get-msg-cnt (word my-id)))


Figure 6:  Code Listing for makecall.lsp

; makecall.lsp
; use LISP symbolic manipulation
; to create OS/2 API functions
; "I would rather write code to write code, than write code"
; Andrew Schulman 2-August-1988

; filter sets up formal argument list to be passed to (define)
(define (filter list &aux (lst (list nil)) elem)
    (dotimes
        ; for each element in list
        (i (length list))
        (setf elem (nth i list))
        ; if it's a list, like (word freq)
        (if (listp elem)
            ;  if it's not a retval directive, like 'word
            (if (not (eq 'QUOTE (car elem)))
                (nconc lst (list (cadr elem))))
        ; else
            (nconc lst (list elem))))
    (cdr lst))

(defmacro make-call (module func &rest args)
    `(define ,(append (list func) (filter args))
        (call
            ,(getprocaddr
                (eval module)
                (if (eq module 'CRTLIB)
                    (strcat "_" (string-downcase (symbol-name func)))
                    (symbol-name func)))
            ,@args)))

; e.g.:
;       (make-call doscalls dosbeep (word freq) (word dur))
; produces:
;       (define (dosbeep freq dur)
;           (call <procaddr> (word freq) (word dur)))
; can then be called:
;       (dosbeep 880 100)


Figure 7:  Source Code Listing for segs.lsp

; SEGS.LSP
; for OS2XLISP
; to examine OS/2 memory segments
; Andrew Schulman 2-August 1988

; requested protection level of seg
(defmacro rpl (x) `(logand ,x 3))

; in LDT or GDT?
(defmacro global? (x) `(zerop (logand ,x 4)))

; does LAR represent accessed segment?
(defmacro accessed? (x) `(= 1 (logand ,x 1)))

; does LAR represent present segment?
(defmacro present? (x) `(= 128 (logand ,x 128)))

; does LAR represent code segment?
(defmacro code? (x) `(= 8 (logand ,x 8)))

; does LAR represent call gate?
(defmacro call-gate? (x) `(= #xE4 ,x))

; does LAR represent readable code?
(defmacro read? (x)
    `(and
        (code? ,x)
        (= 2 (logand ,x 2))))

; does LAR represent writable data?
(defmacro write? (x)
    `(and
        (not (code? ,x))
        (= 2 (logand ,x 2))))

(define (enumsegs func)
    (dotimes
        ; for each possible segment....
        (i #xFFFF)
        ; if there's really a segment there....
        (if (not (zerop (lar i)))
            ; call the callback function,
            ; passing it the segment number,
            ; its access rights (LAR), and
            ; its size (LSL, remembering to
            ; add 1 because LSL is zero-based)
            (apply func (list i (lar i) (1+ (lsl i)))))))

; this is a callback function, passed to (enumsegs) by (print-segs)
; below
(define (show-seg seg seglar segsize)
    (if (< (rpl seg) 3)
        (format stdout "~A\n" seg)
    ; only show details for one of the four identical segments
        (progn
            (format stdout "~A\t~A\t~A\t"
                ; print segment/selector number
                seg
                ; print whether global (GDT) or local (LDT)
                (if (global? seg)
                    "GDT"
                    "LDT")
                ; print what it is:  data, code, or call gate
                (cond
                    ((call-gate? seglar)
                        "CALL GATE")
                    ((code? seglar)
                        (if (read? seglar)
                            "READABLE CODE"
                            "EXECUTE-ONLY CODE"))
                    (t(if (write? seglar)
                            "WRITABLE DATA"
                            "READ-ONLY DATA"))))
            ; if not a call gate, print its size
            (if (not (call-gate? seglar))
                (format stdout "~A ~A"
                    segsize
                    (if (= 1 segsize) "byte" "bytes")))
            ; if segment not present in memory, say so
            (if (not (present? seglar))
                (format stdout " (not present)"))
            ; print ruler at the bottom
            (format stdout "\n~A\n" (make-string #\. 70)))))

(define (print-segs)
    (enumsegs #'show-seg))

; non-transparent popup, invoke func inside popup,
; wait for keystroke
(define (popup func)
    (call (getprocaddr viocalls "VIOPOPUP") ^(word 1)
               (word 0))
    (apply func nil)
    (read-char)
    (call (getprocaddr viocalls "VIOENDPOPUP") (word 0)))

; takes optional keyword position-independent parameters,
; e.g., (total-segs :bkgr t) ;
; or (total-segs :bkgr t :pflag nil)
(define (total-segs
        &key (pflag t) (bkgr nil)
        &aux (numsegs 0) (numbytes 0) (gnum 0)
             (lnum 0) (gbytes 0) (lbytes 0))
    ; if running in background, make idle priority class
    (if bkgr
        (call
            (getprocaddr doscalls "DOSSETPRTY")
            (word 0) (word 1) (word 0) (word (getpid))))
    (enumsegs
        ; pass enumsegs a "nameless" function, created with (lambda)
        (lambda (seg seglar segsize)
            ; only do something for one out of the four identical
              segments
            (if (zerop (rpl seg))
                (progn
                    (setf
                        numsegs (1+ numsegs)
                        numbytes (+ numbytes segsize))
                    (if (global? seg)
                        (setf
                            gnum (1+ gnum)
                            gbytes (+ gbytes segsize))
                    ; else if local
                        (setf
                            lnum (1+ lnum)
                            lbytes (+ lbytes segsize)))))))
    ; since the operation takes a long time,
    ; they'll get popup notification when it's done.
    (if bkgr
        (popup
            (lambda ()
                (call
                    (getprocaddr doscalls "DOSSETPRTY")
                    (word 0) (word 2) (word 0)
                    (word (getpid)))
                (format stdout "OS2XLISP finished computing
                                TOTAL-SEGS!\n\n")
                (format stdout "SEGS: ~A\tBYTES: ~A\n\n"
                  numsegs numbytes)
                (format stdout "Press any key to
                                continue\n\n"))))
    ; either print out the results, or return them as a list
    (if pflag
        (not
            (format stdout "Segs:  ~A (GDT ~A, LDT
                            ~A)\tBytes: ~A (GDT ~A, LDT
                            ~A)\n"
                numsegs gnum lnum numbytes gbytes lbytes))
    ; else
        (list numsegs numbytes gnum gbytes lnum lbytes)))


Figure 8:  An example of OS2XLISP using (print-segs).

  > (print-segs)
  12
  13
  14
  15  LDT READ-ONLY DATA  16 bytes
  ...................................
  28
  29
  30
  31  LDT READABLE CODE   3503 bytes
  ....................................
              ∙
              ∙
              ∙


Figure 9:  Under 286 protected mode, segment numbers are actually structures
           with a number of different fields, as shown here.

  Bit Position   Field Definition

  0,1            Requested protection level (RPL)
  2              Table indicator            (global/GDT=0 local/LDT=1)
  3-15           Index into table


Figure 10:  Source Code Listing for the Dining Philosphers

MODULE Dining;

(*
The Five Dining Philosophers in Modula-2
adapted from M. Ben-Ari, Principles of Concurrent Programming (Prentice-
Hall, 1982), p.113

This module has no operating system dependencies, but depends upon Sem,
which has only been implemented for OS/2
*)

FROM Processes IMPORT StartProcess;

FROM InOut IMPORT WriteString, WriteInt, WriteLn;

FROM Sem IMPORT
    CountingSemaphore, P, V, CSInit, BinarySemaphore, Request, Clear,
    BinInit, cobegin, coend, self;

CONST
    NUM_PHILOSOPHERS = 5;
    NUM_FORKS        = NUM_PHILOSOPHERS;

VAR
    fork : ARRAY [1..NUM_FORKS] OF BinarySemaphore;
    room : CountingSemaphore;
    fini : CountingSemaphore;
    i    : CARDINAL;

MODULE io;
IMPORT
    BinarySemaphore, Request, Clear, BinInit, WriteString, WriteInt,
    WriteLn;
EXPORT think, eat;
VAR
    (* iosem has no bearing on problem itself; just for printing strings *)
    iosem : BinarySemaphore;

PROCEDURE think (p : CARDINAL) ;
    BEGIN
        Request(iosem);
        WriteString('thinking: '); WriteInt(p,1); WriteLn;
        Clear(iosem);
    END think;

PROCEDURE eat (p : CARDINAL) ;
    BEGIN
        Request(iosem);
        WriteString('eating: '); WriteInt(p,1); WriteLn;
        Clear(iosem);
    END eat;

BEGIN
    (* initialization for internal module *)
    BinInit(iosem);
END io;

PROCEDURE philosopher () ;
    VAR
        i           : CARDINAL;
        left, right : CARDINAL;
    BEGIN
        (*
            these don't have to be done each time through
            loop, since each philosopher/process has its own
            stack
        *)
        i     := self() - 1;
        left  := i;
        right := (i MOD NUM_FORKS) + 1;

        P(fini);

        LOOP
            think(i);
            P(room);
            Request(fork[left]);
            Request(fork[right]);
            eat(i);
            Clear(fork[left]);
            Clear(fork[right]);
            V(room);
        END;
    END philosopher;

BEGIN
    CSInit(room, NUM_FORKS - 1);
    CSInit(fini, NUM_PHILOSOPHERS);

    FOR i := 1 TO NUM_FORKS DO
        BinInit(fork[i]);
    END;

    cobegin();
        FOR i := 1 TO NUM_PHILOSOPHERS DO
            StartProcess(philosopher, 2048);
        END;
    coend();

    P(fini);    (* never cleared! *)
END Dining.

DEFINITION MODULE Sem;

TYPE CountingSemaphore;
PROCEDURE P ['P']           (VAR cs : CountingSemaphore);
PROCEDURE V ['V']           (VAR cs : CountingSemaphore);
PROCEDURE CSInit ['CSINIT'] (VAR cs : CountingSemaphore; count : CARDINAL);

TYPE BinarySemaphore;
PROCEDURE Request ['REQUEST'] (VAR s : BinarySemaphore);
PROCEDURE Clear   ['CLEAR']   (VAR s : BinarySemaphore);
PROCEDURE BinInit ['BININIT'] (VAR s : BinarySemaphore);

PROCEDURE cobegin ['COBEGIN'] () ;
PROCEDURE coend   ['COEND'] () ;

PROCEDURE self ['SELF'] () : CARDINAL ;

END Sem.

IMPLEMENTATION MODULE Sem;

(*
contains:
    Counting Semaphores
    Binary Semaphores
    assorted thread-related procedures
*)

FROM SYSTEM IMPORT ADDRESS, ADR;

FROM Storage IMPORT ALLOCATE;

FROM DosCalls IMPORT
    DosSemClear, DosSemSet, DosSemRequest, DosSemWait, DosEnterCritSec,
    DosExitCritSec, DosGetInfoSeg;

TYPE CountingSemaphore = POINTER TO
    RECORD
        cs       : LONGINT;
        ms       : LONGINT;
        count    : CARDINAL;
        countsem : ADDRESS;
        mutexsem : ADDRESS;
    END;

(*
  note:  P() and V() adapted from Kevin Ruddell, "Using OS/2
  Semaphores to Coordinate Concurrent Threads of Execution,"
  Microsoft Systems Journal, May 1988, Figure 9: "Simulating
  a Counting Semaphore under OS/2,"
*)

PROCEDURE P ['P'] (VAR cs : CountingSemaphore);
    VAR blocked : BOOLEAN;
    BEGIN
        blocked := TRUE;
        WHILE blocked DO
            DosSemWait(cs^.countsem, -1);
            DosSemRequest(cs^.mutexsem, -1);
            IF (cs^.count = 0) THEN DosSemSet(cs^.countsem);
            ELSE
                DEC(cs^.count);
                blocked := FALSE;
            END;
            DosSemClear(cs^.mutexsem);
        END;
    END P;

PROCEDURE V ['V'] (VAR cs : CountingSemaphore);
    BEGIN
        DosSemRequest(cs^.mutexsem, -1);
        INC(cs^.count);
        DosSemClear(cs^.countsem);
        DosSemClear(cs^.mutexsem);
    END V;

PROCEDURE CSInit ['CSINIT'] (VAR cs : CountingSemaphore; count : CARDINAL);
    BEGIN
        NEW(cs);
        cs^.cs       := 0;
        cs^.ms       := 0;
        cs^.count    := count;
        cs^.countsem := ADR(cs^.cs);
        cs^.mutexsem := ADR(cs^.ms);
    END CSInit;

TYPE BinarySemaphore = POINTER TO
    RECORD
        s   : LONGINT;
        sem : ADDRESS;
    END;

PROCEDURE Request ['REQUEST'] (VAR s : BinarySemaphore);
    BEGIN
        DosSemRequest(s^.sem, -1);
    END Request;

PROCEDURE Clear ['CLEAR'] (VAR s : BinarySemaphore);
    BEGIN
        DosSemClear(s^.sem);
    END Clear;

PROCEDURE BinInit ['BININIT'] (VAR s : BinarySemaphore);
    BEGIN
        NEW(s);
        s^.s   := 0;
        s^.sem := ADR(s^.s);
    END BinInit;

(* simulate the cobegin-coend construct *)
PROCEDURE cobegin ['COBEGIN'] ();
    BEGIN
        DosEnterCritSec();
    END cobegin;

PROCEDURE coend ['COEND'] ();
    BEGIN
        DosExitCritSec();
    END coend;

MODULE Self;
IMPORT ADDRESS, DosGetInfoSeg;
EXPORT self;
VAR
    A         : ADDRESS;
    LocalInfo : POINTER TO ARRAY [0..6] OF CARDINAL;

(* return a thread's ID number *)
PROCEDURE self ['SELF'] () : CARDINAL;
    BEGIN
        RETURN LocalInfo^[3];
    END self;

BEGIN
    (* module initialization code *)
    DosGetInfoSeg(A.OFFSET, A.SEGMENT);
    A.OFFSET := 0;
    LocalInfo := A;
END Self;

END Sem.

; SEMAPH.DEF

LIBRARY SEM INITINSTANCE
DESCRIPTION 'Counting and Binary Semaphores'
DATA MULTIPLE
EXPORTS
    SEM         ; init
    P
    V
    CSINIT
    REQUEST
    CLEAR
    BININIT
    COBEGIN
    COEND
    SELF

; START.ASM -- auto-init entry point

EXTRN   SEM:FAR

        DOSSEG
        .MODEL large
        .CODE

START   PROC FAR
        call SEM
        mov ax,1    ; return success
        ret
START   ENDP

END     START

set m2lib=\os2\mod2\lib

m2 sem.def/data:l/thread && m2 sem/data:l/thread
m2 dining/thread

' to make DLL version:
masm start.asm;
link /dosseg start sem junk,sem.dll,,,semaph.def && imblib sem.lib
semaph.def
copy sem.dll \os2\dll       'copy to LIBPATH
link dining,dining,,sem;

' to make non-DLL version:
link dining sem,dining;


Figure 11:  Creating a BASIC Utility

  declare sub DosGetInfoSeg (seg arg1%, seg arg2%)
  DosGetInfoSeg gis%, lis%
  def seg = gis%
  print "The time is"; peek(8); ":" peek(9); ":" peek(10)

  The OS/2 command-line to compile, link, and run INFOSEG.BAS is:

  C>bc infoseg.bas; && link infoseg; && infoseg


Figure 12:  ENUMPROC.BAS, a basic program that prints out a list of all
            functions exported from a DLL.

' enumproc.bas
' bc /e enumproc; && link enumproc; && enumproc brun60ep

if command$ = "" then dll$ = "BRUN60EP.DLL"
elseif instr(command$, ".") then dll$ = command$
else dll$ = command$ + ".DLL"
endif

pipe = freefile

' makes it easy to detect end of pipe's output
on error goto closefile

foundit = 0

open "pipe:exehdr " + dll$ + " 2>nul" as pipe

foundfile = 1       ' found EXEHDR

' get rid of initial EXEHDR output
do while instr(buf$, "Exports:") = 0
    line input #pipe, buf$
loop
line input #pipe, buf$

foundfile = 2       ' found named DLL

print "Routines exported by "; dll$

do while 1
    line input #pipe, buf$
    buf$ = mid$(buf$,16)
    procname$ = left$(buf$, instr(buf$, " ") - 1)
    print procname$
loop

closefile:
    if foundfile = 0 then print "Can't find EXEHDR"
    elseif foundfile = 1 then print "Can't find "; dll$
    endif
    close pipe
    system


Figure 13:  Code Listing for PEEKER.BAS

' peeker.bas
' compile with bc peeker.bas /d/e/x; && link peeker;

on error goto errorhandler

do while 1
    input; "seg"; sgm%
    print chr$(9);
    input; "offset"; ofs%
    print chr$(9);

    def seg = sgm%
    print peek(ofs%)
loop

errorhandler:
    if err = 70 then print "Permission denied"
        resume next
    else
        print "Unknown error #"; err
        system
    endif


Figure 14:  Code Listing for SMSW.BAS

' smsw.bas
' compile with bc smsw.bas; && link smsw;

declare sub DosCreateCSAlias (byval p1%, seg p2%)

code$ = _
    chr$(&hc8) + chr$(0) + _
    chr$(0) + chr$(0) + _                   ' enter 0,0
    chr$(&h8b) + chr$(&h5e) + _
    chr$(&h06) + _                          ' mov bx,word ptr [bp+6]
    chr$(&h0f) + chr$(&h01) + _
    chr$(&h27) + _                          ' smsw word ptr [bx]
    chr$(&hc9) + _                          ' leave
    chr$(&hca) + chr$(&h02) + chr$(&h00)    ' retf 2

DosCreateCSAlias varseg(code$), csalias%
def seg = csalias%
call absolute(x%, sadd(code$))

if x% and 2 then print "Coprocessor present"
elseif x% and 4 then print "No coprocessor -- emulate"
else print "Something is very wrong"
endif


Figure 15:  Source Code for SMSW2.BAS and PROTMODE.ASM

' smsw2.bas
'
' To compile and link (all on the same line):
' bc smsw2.bas; && masm protmode.asm; && link smsw2
' protmode, smsw2;

declare sub SMSW alias "_SMSW" (x%)

smsw x%
if x% and 2 then print "Coprocessor present"
elseif x% and 4 then print "Using emulator"
else print "Something is wrong!"
endif

; protmode.asm
; requires MASM 5.1

.model medium,basic
.286p
.code

_smsw   proc far    arg1:near ptr word
        mov bx,arg1
        smsw word ptr [bx]
        ret
_smsw   endp

_lsl    proc far    segm:word
        sub dx,dx
        sub ax,ax
        lsl ax,segm
        ret
_lsl    endp

end


Figure 16:  Language Comparison Chart

                 │         │        │Stony Brook│Microsoft│
                 │UR/FORTH │OS2XLISP│ Modula-2  │BASIC 6.0│   C
     ────────────┼─────────┼────────┼───────────┼─────────┼────────
     interactive │    x    │   x    │           │         │
     ────────────┼─────────┼────────┼───────────┼─────────┼────────
     multitasking│    x    │        │     x     │         │   x
     ────────────┼─────────┼────────┼───────────┼─────────┼────────
     graphics    │    x    │        │           │    x    │
     ────────────┼─────────┼────────┼───────────┼─────────┼────────
     GP fault    │         │        │           │         │
     handling    │         │   x    │           │    x    │
     ────────────┼─────────┼────────┼───────────┼─────────┼────────
     make DLL    │         │        │     x     │         │   x
     ────────────┼─────────┼────────┼───────────┼─────────┼────────
     run-time    │         │        │           │         │
     error chk   │         │        │     x     │    x    │
     ────────────┼─────────┼────────┼───────────┼─────────┼────────
     debugging   │roll your│built-in│M2DEBUG-EXE│   CVP   │  CVP
                 │   own   │        │           │         │
     ────────────┼─────────┼────────┼───────────┼─────────┼────────
     link with C │         │   x    │     x     │   x     │

████████████████████████████████████████████████████████████████████████████

The High Memory Area: Addressing 64Kb More Memory in Real Mode

Chip Anderson

Everyone knows that the greatest possible amount of memory that the Intel(R)
8086 and 8088 microprocessors can address is 1Mb. It is also commonly
believed that in real mode the Intel 80286 and 80386 processors can only
address the same 1Mb, but this is not true. A careful study of the
architecture of the 80286 and 80386 processors demonstrates that it is
possible to address almost 64Kb more memory. With a bit of extra effort,
programs running under DOS can take advantage of this extra space. This
article will present the principles needed to gain access to this extra
memory, and an analysis of the problems involved.


High Memory Area

The extra memory space is called the High Memory Area (HMA). Its maximum
size is 64Kb minus 16 bytes, and in order to access it, one needs to
enable the 80286's and the 80386's A20 address line while in real mode.
The 8086 and 8088 processors each have 20 address lines, A0──A19, which
produces an addressable range of 220 bytes, or 1Mb.

For this discussion, it is extremely important to remember the
difference between a "memory location" and a "memory address." Memory
locations are numbered sequentially starting at 0 and progress up to the
top of addressable memory. There is a natural one-to-one correspondence
between memory locations and actual physical memory.

Memory addresses are just a different way of referring to that same
physical memory. They consist of two parts, a 16-bit segment and a 16-bit
offset. In real mode a new segment starts every 10H bytes in memory. For
example, segment 1234H starts 1234H 5 10H or 12340H bytes from the start
of physical memory. If an offset is given, it is simply added to the
start of the segment. Consequently memory address 1234:5678H refers to
memory location 12340H + 5678H, or 179B8H (see Figure 1).

The 80286/386 processors have more address lines and can thus access more
memory when running in protected mode. However, when in real mode, the
21st address line of these processors, the A20 line, is disabled, thus
limiting the available address space to the same 1Mb as the 8086/88.
Anytime the processors attempt to access memory beyond 1Mb they "wrap"
back to the beginning of system memory.

When such machines as the IBM(R) PC/AT(R) and the COMPAQ(R) 386(TM) are in
real mode, you can selectively enable and disable the A20 line. When the
A20 line is enabled, attempts to access memory beyond 1Mb do not cause a
wrap back to the beginning of system memory. Instead it becomes possible
to go beyond the 1Mb boundary into extended memory.

Consider the following example. Using the standard Intel real-mode
addressing convention, the address FFFF:0FH refers to memory location
FFFFFH, which is the byte just before the 1Mb boundary (see Figure 2A).
Notice that this is the highest location that can be reached with a 20-
line, or 20-bit, addressing scheme. The address FFFF:10H should,
logically, refer to location 100000H, which is the byte at the 1Mb
boundary. But since we can only look at 20 bits worth of information, the
leading 1, which is located at the 21st bit position (or the 21st line,
A20), is ignored. Therefore, address FFFF:10H leaves us with a value of 0H
(see Figure 2B and Figure 3), in effect a wrap to the beginning of system
memory.

When the A20 line is enabled, there isn't a wrap because the required 21st
bit is no longer masked off. This means that location FFFF:10H actually
refers to 100000H, or the first byte of extended memory (see Figure 2C).

The Intel real-mode addressing convention limits the maximum possible
address to FFFF:FFFFH, so we end up with a real location of 10FFEFH, which
is 64Kb minus 16 bytes above the 1Mb boundary (see Figure 2D). This extra
addressable memory is called High Memory Area (HMA).


Unique HMA Qualities

Several aspects of the HMA distinguish it from normal memory segments.
Whereas normal memory segments are always available, the HMA can only be
accessed while the A20 line is enabled. Another difference is that the
HMA does not start at the beginning of a segment but 16 bytes after the
beginning of segment FFFFH.

The most important difference is that the HMA cannot be subdivided.
Because no segment starts inside the HMA-remember it resides entirely
inside the highest valid segment, FFFFH-the HMA cannot be split up into
smaller chunks and therefore cannot be used by more than one program at a
time.

In theory, using the HMA should be as simple as moving some code into the
first part of extended memory, enabling the A20 line, and jumping to some
address above FFFF:10H. However, its unique qualities impose several
restrictions on its use.


HMA Problems

Let's look closely at some of these HMA issues.

Managing the machine's A20 line. The procedure for enabling and disabling
the A20 line differs from machine to machine; some machines may not even
have an A20 line. Determining which machine you are running on and how
to enable and disable its A20 line reliably is a serious obstacle. A
consistent, machine-independent facility for managing this resource is
needed.

Reserving the HMA. The HMA can be considered a "grabbable" resource.
Because the HMA can only reside in one segment it cannot be shared among
several programs at the same time. While DOS is not a multitasking system,
there may very well be TSRs and smart device drivers that could cause
conflicts for resources such as the HMA. Clearly, a scheme is required for
reserving and releasing the HMA.

Preventing the HMA from being accidentally overwritten. Some TSRs and
device drivers, for example, IBM's memory disk program, VDISK, simply
grab the lower part of extended memory (including the HMA) and store their
data there. If one of these drivers is installed, the HMA cannot be used.
Other drivers, such as older versions of RAMDrive, Microsoft's memory disk
program, allocate extended memory from the top of extended memory down.
As long as these programs leave 64Kb free, the HMA will be available, but
once the HMA is in use, these programs must be prevented from overwriting
it.

Detecting the existence of the HMA. Obviously at least 64Kb of extended
memory must be present in order to provide HMA support; this condition
must therefore be checked. A check must also be made to ensure that the
A20 line can be reliably enabled and disabled.

Prioritizing HMA usage. If a TSR or DOS device driver moves itself into
the HMA, the space that it occupied below the 640Kb line becomes available
for use by all other programs. Now consider the case where two TSRs that
could use the HMA are installed. Remember that only one program can use
the HMA at any one time. Let's say that the first TSR can move 50Kb of
itself into the HMA while the second can move 62Kb of its code up (see
Figure 4). Obviously there would be an extra 12Kb of available memory
below the 640Kb line if the second TSR was moved into the HMA instead of
the first. Some facility is needed to help users get the maximum benefit
from the HMA in such situations.

The unique qualities of the HMA present some formidable problems for the
DOS programmer. When Microsoft first tried to take advantage of this
memory, it became obvious that a standard method for accessing the HMA
had to be developed before it could be widely used.


XMS to the Rescue

In July 1988, Microsoft, Intel, Lotus, and several other leading software
firms published the DOS Extended Memory Specification (XMS) Version
2.0. XMS helps DOS programmers use all parts of extended memory
effectively, including the HMA. An XMS driver solves two of the toughest
HMA usage problems by providing a machine-independent method of switching
the A20 line and by preventing two programs from using the HMA at the same
time.

In addition to HMA support, XMS allows programs to store large amounts of
data in extended memory. However, the rest of this article will only
discuss the XMS functions that permit access to the HMA. Programmers can
obtain complete XMS documentation along with Microsoft's own XMS driver,
HIMEM.SYS, at no charge by contacting Microsoft's Information Center at
(800) 426-9400.


Utilizing the HMA

In order for DOS programs to use the HMA, they need to do the following:

  ■  determine if an XMS driver is installed
  ■  get the address of the driver's control function
  ■  request the HMA

If the XMS driver grants access to the HMA, the program can then:

  ■  enable the A20 line
  ■  copy up to 64Kb of code into the HMA
  ■  execute
  ■  disable the A20 line
  ■  release the HMA
  ■  terminate

Interrupt 2FH, the multiplex interrupt, is used both for determining if
an XMS driver is installed and for obtaining the address of the driver's
control function. Executing Interrupt 2FH with AX=4300H returns a value of
80H in AL if an XMS driver is present (see Figure 5).

Executing an Interrupt 2FH instruction with AX=4310H returns the address
of the XMS driver's control function in ES:BX (see Figure 6). Programs can
use XMS functions by setting AH to one of 18 XMS function numbers and then
calling the control function (see Figure 7). Only functions 0 through 4
are needed to access the HMA. Figure 8 lists the 18 XMS functions.

DOS application programs can request access to the HMA by calling the
control function with AH=01H (Request HMA) and DX=FFFFH (TSR programs
must set DX differently, as you'll see later). If AX is set to 1 after the
call, the program has been given access to the HMA and can then use XMS
function 3 (Global Enable A20) to turn on the machine's A20 line. Once the
A20 line has been successfully enabled, the program can use the extra
memory in the HMA anyway it wants. Typically it will move as much of its
resident code and data as possible, thereby freeing space below the 640Kb
line for dynamic code and data storage.

When the program is about to terminate, it must use XMS function 4 (Global
Disable A20) to turn off the A20 line. Several older DOS programs rely on
the memory wrap, and will crash the system if they are run when the A20
line is enabled. Before terminating, the program must also release the
HMA using XMS function 2 (Release HMA).


Prioritizing HMA Usage Revisited

Now let's return to the problem of prioritizing HMA usage when several
TSRs that use HMA are installed. XMS drivers are installed by including a
line similar to DEVICE=HIMEM.SYS in the machine's CONFIG.SYS file. That
line can have an optional parameter on it (/HMAMIN=) that specifies the
least amount of space a TSR must use before it is given access to the HMA.

In addition, before a TSR calls XMS function 1 (Request HMA), it must set
DX equal to the amount of space in the HMA it would use (in bytes). The
XMS driver compares this number with the amount that is specified by the
/HMAMIN= parameter and only grants the TSR access if it requests more
memory than the parameter specifies. For example, if DEVICE=HIMEM.SYS
/HMAMIN=55 was included in CONFIG.SYS, only programs requesting 55Kb or
more would be allowed access to the HMA.

Notice that this problem does not apply to DOS applications that use the
HMA, but are not TSRs, since they release the HMA before terminating.
Because these programs are not concerned with HMA priority, they should
set DX=FFFFH before calling XMS function 1 (Request HMA), thus bypassing
the /HMAMIN= check.

Since some programs do rely on memory wrap, TSRs that use HMA must disable
the A20 line before returning control to the system. This implies that
these TSRs must have a small routine located below the 640Kb line that
enables the A20 line and then jumps to the code in the HMA when the TSR is
activated. When the TSR finishes, it must jump to another small routine
below the 640Kb line that disables the A20 line and then returns control
to DOS.


HIMEM.SYS

Microsoft's own XMS driver, HIMEM.SYS, is included with the XMS
Developer's Kit. HIMEM.SYS Version 2.04 is a full XMS driver that offers
A20 support for a wide variety of DOS machines, including the IBM PC and
PS/2(R) families and compatibles.

An early version of HIMEM.SYS (Version 1.1) is also being distributed with
Microsoft(R) Windows/286 and Microsoft Windows/386 Version 2.1. It only
ysupports the first nine of XMS's 18 functions, but these include all of
the functions needed for accessing the HMA.

The XMS API specification allows the resourceful programmer to gain an
extra 64Kb of memory on many of the machines currently in use without
requiring special hardware. Properly used, the HMA can significantly
improve speed and increase the amount of data programs can process.


Figure 1:  Memory Locations vs. Memory addresses.

        ╔══════════════╗                     ╔══════════════╗
        ║    FFFFFh    ║                     ║  FFFF:000Fh  ║
        ╠══════════════╣                     ╠══════════════╣
        ║    FFFFEh    ║                     ║  FFFF:000Eh  ║
        ╠══════════════╣                     ╠══════════════╣
        ║    FFFFDh    ║                     ║  FFFF:000Dh  ║
        ╠══════════════╣                     ╠══════════════╣
        ║       ∙      ║                     ║       ∙      ║
        ║       ∙      ║      ┌──────┐       ║       ∙      ║
        ║       ∙      ║      │12340H│       ║       ∙      ║
        ╠══════════════╣      │+5678H│       ╠══════════════╣
        ║    179B8h    ║◄═════╡──────╞═══════║  1234:5678h  ║
        ╠══════════════╣      │179B8H│       ╠══════════════╣
        ║       ∙      ║      └──────┘       ║       ∙      ║
        ║       ∙      ║                     ║       ∙      ║
        ║       ∙      ║                     ║       ∙      ║
        ╠══════════════╣                     ╠══════════════╣
        ║    00002h    ║                     ║   0000:0002  ║
        ╠══════════════╣                     ╠══════════════╣
        ║    00001h    ║      Bottom         ║  0000:0001h  ║
        ╠══════════════╣        of           ╠══════════════╣
        ║    00000h    ║      Memory         ║  0000:0000h  ║
        ╚══════════════╝                     ╚══════════════╝
        Memory Locations                     Memory Addresses


Figure 2:  Intel Addressing Convention

  A)  FFFF:000FH =
           (FFFFH*10H)+000FH   =
           FFFF0H+0000FH       =
           FFFFFH              =
            11111111111111111111B [with 20 bits]

  B)  FFFF:0010H =
           (FFFFH*10H)+0010H   =
           FFFF0H+00010H       =
           100000H             =
            00000000000000000000B [with 20 bits]
      (Results in wrap back to memory location 0000:0000H)

  C)  FFFF:0010H =
           100000000000000000000B [with 21 bits]
      (Results in extending access beyond the 1Mb boundary)

  D)  FFFF:FFFFH =
           (FFFFH*10H)+FFFFH   =
           FFFF0H+0FFFFH       =
           10FFEFH


Figure 3:  Memory Wrap

                             ┌──────────────┐─┐
                             │  FFFF:FFFFh  │ │
                             ├──────────────┤ │
                             │      ∙       │ │
                             │      ∙       │ │
                             │      ∙       │ ├──────────┐
                             ├──────────────┤ │          │
                             │  FFF:0011h   │ │ ┌────────┴────────┐
                             ├──────────────┤ │ │Noramlly, memory │
                             │  FFFF:0010h  │ │ │locations are    │
       1Mb┌──────────────┐   ├──────────────┤─┘ │limited to 20    │
          │    FFFFFh    │   │  FFFF:000Fh  │   │bits causing     │
          ├──────────────┤   ├──────────────┤   │them to wrap     │
          │    FFFFEh    │   │  FFFF:000Eh  │   │back to the      │
          ├──────────────┤   ├──────────────┤   │bottom of memory.│
          │    FFFFDh    │   │  FFFF:000Dh  │   └────────┬────────┘
          ├──────────────┤   ├──────────────┤            │
          │      ∙       │   │      ∙       │            │
          │      ∙       │   │      ∙       │            │
          │      ∙       │   │      ∙       │            │
          ├──────────────┤   ├──────────────┤     ┌──────▼───────┐
          │    0FFEFh    │   │  0000:FFEFh  │◄───►│  FFFF:FFFh   │
          ├──────────────┤   ├──────────────┤     ├──────────────┤
          │      ∙       │   │      ∙       │     │      ∙       │
          │      ∙       │   │      ∙       │◄───►│      ∙       │
          │      ∙       │   │      ∙       │     │      ∙       │
          ├──────────────┤   ├──────────────┤     ├──────────────┤
          │    00001h    │   │  0000:0001h  │◄───►│  FFFF:0011h  │
          ├──────────────┤   ├──────────────┤     ├──────────────┤
 Bottom of│    00000h    │   │  0000:0000h  │◄───►│  FFFF:0010h  │
  Memory  └──────────────┘   └──────────────┘     └──────────────┘
          Memory Locations   Memory Addresses     "Wrapped" Memory
                                                      Address


Figure 4:  Optimizing HMA Usage by TSRs

                              ┌────────────────┐      ┌────────────────┐
                              │ 14 Kb Unusable │      │                │
                           HMA├────────────────┤   HMA├────────────────┤
                              │    50Kb TSR    │      │    62Kb TSR    │
                              └────────────────┘      └────────────────┘
  640Kb╔═══════════════╗ 640Kb╔════════════════╗ 640Kb╔════════════════╗
       ║               ║      ║                ║      ║                ║
       ║               ║      ║                ║      ║                ║
       ║               ║      ║                ║      ║                ║
       ║               ║      ║                ║      ║                ║
       ║   468Kb Free  ║      ║   518Kb Free   ║      ║   530Kb Free   ║
       ║               ║      ║                ║      ║                ║
       ║               ║      ║                ║      ║                ║
  172Kb╟───────────────╢      ║                ║      ║                ║
       ║   62Kb TSR    ║      ║                ║      ║                ║
  110Kb╟───────────────╢      ║                ║      ║                ║
       ║   50Kb TSR    ║ 122Kb╟────────────────╢ 110Kb╟────────────────╢
   60Kb╟───────────────╢      ║    62Kb TSR    ║      ║    50Kb TSR    ║
       ║               ║  60Kb╟────────────────╢  60Kb╟────────────────╢
       ║      DOS      ║      ║                ║      ║                ║
       ║               ║      ║      DOS       ║      ║      DOS       ║
    0Kb╚═══════════════╝   0Kb╚════════════════╝   0Kb╚════════════════╝
             No HMA            Moving 1st TSR up       Moving 2nd TSR up


Figure 5:  Determining if an XMS Driver is Installed

  ; Is an XMS driver installed?
  mov        ax,4300h
  int        2Fh
  cmp        al,80h
  jne        NoXMSDriver


Figure 6:  Finding the XMS Driver's Control Function

  mov        ax,4310h
  int        2Fh
  mov        word ptr [XMSControl],bx    ; XMSControl is a DWORD
  mov        word ptr [XMSControl+2],es


Figure 7:  Calling an XMS Function

  ; Get the XMS driver's version number
  mov        ah,00h
  call       [XMSControl]        ; Get XMS version number


Figure 8:  XMS API Functions

Function      Description

  00H         Get XMS Version Number
  01H         Request High Memory Area
  02H         Release High Memory Area
  03H         Global Enable A20
  04H         Global Disable A20
  05H         Local Enable A20
  06H         Local Disable A20
  07H         Query A20
  08H         Query Free Extended Memory
  09H         Allocate Extended Memory Block
  0AH         Free Extended Memory Block
  0BH         Move Extended Memory Block
  0CH         Lock Extended Memory Block
  0DH         Unlock Extended Memory Block
  0EH         Get Handle Information
  0FH         Reallocate Extended Memory Block
  10H         Request Upper Memory Block
  11H         Release Upper Memory Block


████████████████████████████████████████████████████████████████████████████

Developing and Debugging Embedded Systems Applications

Y. P. Chien, Ph.D☼

Operating systems are necessary for most general computing
environments──from the largest mainframe to the smallest PC──but one
environment which does not require an operating system is that of
embedded system applications. Typically an embedded system is a
microprocessor-based computer system that performs dedicated control
tasks. Airborne flight controllers and satellite instrumentations are
exotic examples of embedded systems; a less exotic application is the
cruise control on a car.

Traditionally assembly language has been the language of choice for
developing embedded system applications, as it is closest to the
instruction set of the microprocessor. Assembly language offers
execution speed and microprocessor-level access, such as memory and
I/O port addressing and bit manipulation. It is not, however, a natural
language for expressing algorithms. As microprocessor applications
become more complex and demanding, there is a growing need for more
efficient software development tools.

Recently programmers have turned to C for developing applications that
run on embedded systems. C offers a large set of operators for
microprocessor-level access, so C programs can produce very efficient
machine code. Furthermore, C has the traditional high-level structured
programming constructs-iteration, selection, and decision-including data
structuring capabilities. These features make it easier to write and
document C programs.

Embedded systems are characterized by the need to store the application
programs in read-only memory (ROM) chips, so that when system power is
turned on, the application programs can immediately take control. They
are usually standalone systems having no operating system support. For
example, a satellite data logger has no need for a sophisticated file
system, nor does it need to provide a fancy user interface or shell. The
primary task of a satellite data logger is to gather data through its
sensors, process the data, and transmit it back to a tracking station on
earth.

Embedded system software must be able to handle the asynchronous
software/hardware interactions typical of real-time control applications.
For example, an automatic braking system may use interrupts to signal
the controller when the braking wheels stop turning. The controller must
respond to this asynchronous stimulus as soon as possible, releasing the
brakes to avoid an uncontrolled skid.

This article addresses issues related to the development of embedded
system applications using the Microsoft(R) C optimizing compiler. The
embedded systems we'll look at are based on the 80286/386 family of
microprocessors. We will first compare the special needs of embedded
system applications versus MS-DOS(R) applications, then describe how to
generate C programs for embedded systems. Finally, we will tackle the
topic of debugging software/hardware interactions on target software.


Code Generation

An executable file for the MS-DOS run-time environment does not contain
absolute addresses. The MS-DOS linker uses a load-time relocatable
format to organize the executable file. That is, the linker does not bind
the program (both code and data) with fixed addresses. The MS-DOS loader
determines the absolute location when it loads the program from disk to
memory for execution.

An embedded system application, on the other hand, has fixed addresses.
When designing the target hardware, specific memory locations for ROM
and RAM chips must be reserved. During development of the application
program, program code is assigned to ROM and program data to RAM. Usually
ROM chips are placed in high memory close to the end of the 1Mb address
range because the physical addresses starting at FFFF0H contain the
bootstrap code. This section of code normally consists of an 8086 machine
instruction, which performs a far jump to the entry point of the
application program. Upon processor reset, the microprocessor will
execute the bootstrap code to pass execution control to the application
program. Figure 1 compares a typical memory configuration of an embedded
system to the standard MS-DOS memory configuration.


Initialized Variables

In C a program begins execution by calling its main function. The
program assumes that all initialized extern (global) and static (local)
variables contain initial values. All other uninitialized variables
contain the value zero.

In the MS-DOS environment, when the MS-DOS loader loads a C program from
disk to memory, it also loads all the initialized variables with proper
values, then passes control to the program. Before the C program starts
executing, the loader passes control to a start-up routine. The start-up
routine sets up the run-time environment. During start-up, the routine
sets all the uninitialized variables to zero, and then passes control to
the main function.

In the embedded environment the program code and initializers are stored
in ROM space. Upon processor reset, a start-up routine initializes the
hardware and sets up the run-time environment for the C program. During
initialization, the start-up routine copies the initializers stored in ROM
to the program variables located in RAM, sets the uninitialized variables
to zero, then passes program control to the main function. Since hardware
must be manipulated directly, start-up routines should always be written
in assembly language. Figure 2 shows a sample start-up routine for
embedded system applications.


C Run Time

Under MS-DOS, the Microsoft C run-time library contains a set of
functions for use in C programs, including functions that interface to the
MS-DOS operating system, as well as many other common functions.
Generally, these functions fall into two categories: MS-DOS dependent and
MS-DOS independent. The dependent functions make system calls to
MS-DOS, such as opening and closing files. The independent functions,
such as the string manipulation functions, do not invoke MS-DOS calls.
Since embedded systems do not for the most part contain MS-DOS, only the
independent functions can be used in embedded applications without
modification. Figure 3 lists examples of the DOS dependent and DOS
independent functions in the C run-time library.


Loading/Testing

In MS-DOS, you can test C programs with a resident debugger such as
Microsoft CodeView(R). Program development facilities are usually not
available in the embedded environment. Therefore, you have to develop C
programs on a host computer, such as an IBM(R) PS/2(R), PC/AT(R), or
compatible. You can then use an in-circuit emulator to debug the program
on target hardware.

An in-circuit emulator, which consists of both hardware and software, is
an ideal debugging tool for integrating application software with the
target hardware. The hardware includes a probe that plugs into the
microprocessor chip socket and duplicates the behavior of the target
microprocessor in real time, either full speed or in single-step
execution. The software provides an interface that lets users control the
interactive debugging sessions. So an in-circuit emulator system
provides a controlled environment for exercising the hardware design
and monitoring the results of software running on the actual hardware.
Other supplementary debugging tools, such as resident debuggers,
simulators, and monitor debuggers, are available.


C Programs for Embedded Systems

Due to the special requirements of an embedded system, we need an
absolute linker that can satisfy these requirements. An example of such a
tool is Systems & Software, Inc.'s Link&Locate++ Firmware Generation
Package, which supports the Microsoft C Optimizing Compiler 5.1 and
Microsoft Macro Assembler (MASM) 5.1.

We will discuss how such a tool can be used to build C programs for
embedded systems. Figure 4 shows a functional diagram of the
Link&Locate++ package. The package contains a linker, locator, object
formatter, hex formatter, and other support utilities.


Combining Object Files

Usually a C program consists of a collection of functions. The C compiler
and macro assembler compile and assemble the source files into object
files, then the special linker is used to combine all the object files and
produce a relocatable object file and a map file. The map file is a text
file which lists the names of all the object modules in the relocatable
object file. The standard MS-DOS LINK linker cannot be used because it
only produces an executable (EXE) file for the MS-DOS run-time environment.

During the link process, the linker resolves external references with
public declarations and combines program segments that have the same
attributes. The linker also supports incremental link──that is, external
references need not be resolved in a single link. The linker can resolve
them incrementally in multiple links.


Absolute Addresses

The relocatable object file generated by the linker does not contain
absolute address information. A special locator assigns absolute
addresses to the program code and data to generate an absolute object file.

The 8086 family of microprocessors has a segmented architecture. The
microprocessor contains four segment registers: CS, DS, SS, and ES. Each
segment register points to the base of a physical segment, which is a
64Kb section of memory locations. Memory locations within a physical
segment may be referenced by an offset with respect to the segment's
base, which is contained in each respective segment register.

The Microsoft C optimizing compiler translates source files into 8086
machine code, and places the code in multiple logical segments, pieces of
code 64Kb in size at most. For example, the C compiler places program
code in a logical segment called _TEXT, and certain program data in a
logical segment called _DATA.

For embedded system applications, program code is stored in ROM and
program data is placed in RAM. Both ROM and RAM occupy fixed memory
locations, so the logical segments of a C program must be mapped into
physical segments. A locator assigns absolute addresses to the logical
segments. The locator will allow placement of the logical segments of a C
program anywhere in the 1Mb address space of the 8086 family of
microprocessors. The locator will process the relocatable object file to
produce an absolute object file and usually a map file as well.

The map file contains a summary of segment and symbol addresses. The
absolute object file generated by the locator is in standard Intel(R) OMF
format. If the input relocatable object file contains CodeView debug
information, further processing of the absolute object file to convert
CodeView debug information into Intel OMF debug information will be
required.

Finally, you can now load the absolute object file into an in-circuit
emulator, such as the Intel I²ICE emulator, which lets you test the
program on target hardware in real time. Meanwhile, to store the program
in ROM, you must convert the absolute object file into a format for
programming ROMs, the most widely supported of which is Intel Hex (without
Extended Address Record). A hex formatter will be required to generate
Intel Hex files from the absolute object file for ROM programming.


Debugging

The in-circuit emulator is useful for debugging real-time C programs.
Since we are using C to develop the embedded system applications, we will
want to debug programs at the C source level as well. Remember that these
applications are typically I/O-intensive-that is, the microprocessor
must frequently interact with external devices. Therefore, the application
programs should be debugged directly on the actual target hardware. In
particular, it is important to be able to catch problems related to real
time I/O interaction, such as the real-time response to nested interrupts.

It is desirable to generate absolute object files in Intel OMF, as the
resulting file will then be fully compatible with the Intel I²ICE
emulator. This emulator provides a window-oriented user interface similar
to CodeView called ICEVIEW, which supports C source level and symbolic
debugging on the target system. Figure 5 shows the programming
environment for developing embedded system applications in Microsoft C.

The Intel I²ICE emulator can also isolate problems with real-time I/O
interaction. For example, let us say a hardware interrupt signals the
availability of an input byte. Figure 6 shows the C source code for an
interrupt handling routine, which gets the input byte and places it in the
ring buffer, and for an output routine, which reads data from the ring
buffer one byte at a time.


ICEVIEW

ICEVIEW is the user interface that makes it easy to use the emulator. Its
pull-down menus with context-sensitive help assist you in setting up and
controlling the emulation environment; its multiple windows let you
look at program source, execution trace, register contents, and other
debug information all at the same time.

ICEVIEW provides a full source-level debug capability. A source window
displays the program listing of the current execution module, and lets you
single-step through the program one source line at a time. Up to ten watch
symbols are available to display current values of symbols. You can watch
data change at execution breakpoints or during single-step execution.


Hardware Breakpoints

The I²ICE emulator delivers full speed, real-time microprocessor
emulation at speeds up to 10MHz. Probes for the 8086, 8088, 80186, 80188,
and 80286 are available, and the emulator also supports the 8087 and 80287
coprocessors. The I²ICE emulator lets you specify conditions for the
emulator to halt program execution (breakpoints). You can set breakpoints
based on instruction execution as well as microprocessor bus activities.
The bus activities include fetches, as well as reads and writes on the
address lines, data lines, status lines, and the emulation clip pod's
input lines. To see examples, break during a read or write to a specific
address, a data or I/O port, or on a combination of events.

One method of defining a breakpoint condition is to use one or more debug
registers. These are user-named software registers containing user-defined
debug procedures. There are five types: arm, break, system, trace, and
event. The first, the arm register, sets conditional breakpoints that
allow breaking within arm windows. The break register defines breakpoints
on the address of executable statements, and the system register defines
them on operand access, operand data, logic clips, system breaking, and
coprocessor cycles. The trace register sets conditions to selectively
collect program execution history; and last the event register controls
the event machine of the I²ICE emulator.

The debug register feature lets you specify very complex breakpoint
conditions. As an example, we set up an event register to detect an input-
overrun error condition (see Figure 7☼). For any array element in our ring
buffer example, a read must follow a write before another write can take
place. Otherwise, an input-overrun error has occurred and we want to halt
program execution.

The I²ICE emulator contains a trace buffer which automatically collects
trace data during emulation. Specifying a trace window sets the start and
stop conditions for trace data collection. You can define trace
specifications either directly or through a trace debug register, and
display trace data in either instructions mode or cycles mode. These modes
display the trace buffer in disassembled mnemonics and bus cycles,
respectively; the trace information is important in determining the
cause of errors (see Figure 8☼).

You can also program the emulator to collect trace information within an
arm window, which is a period of time delimited by a begin condition and
an end condition. A begin condition is defined by an arm
specification, an end condition by a disarm specification.

For example, if your program crashes after writing to a specific output
port, you may open an arm window to start collecting trace data when a
write to that port occurs. Close the arm window by defining a disarm
specification; in this example, the disarm specification may be 100
bus cycles after the arm condition becomes true. In this way, you can
examine the collected trace data and find out what causes the program to
crash.


Performance Analyzer

The I²ICE emulator interfaces with the Intel Performance Analysis Tool
(iPAT) for examining software execution speeds and code coverage in real
time. With the iPAT, you can gather timing-and-count data on code
execution, code interaction, execution flow, interrupts, and interrupt
latency, as well as display the data in either tables or histograms.

With these capabilities, you can easily locate performance problems such
as software bottlenecks, unused code, and worst case interrupt latency.
Software bottlenecks occur when code is heavily used but relatively slow;
where performance can be enhanced by improving the algorithms or
rewriting critical routines in assembly language. Worst case interrupt
latency is the delay between origination of an interrupt and the target
systems' response to the interrupt. Worst case interrupt latency
information is important in order to be sure that system performance is
within given specifications.

We have discussed the special requirements for developing and debugging
embedded system applications. A typical development environment
includes the Microsoft C Optimizing compiler Version 5.0 or later;
Microsoft MASM Version 5.0 or later; a tool such as the SSI Link&Locate++
firmware development package; and the Intel I²ICE emulator, which gives
you source level debugging capability to test your C programs in target
hardware.

This programming environment effectively addresses the issues of
creating ROMable code generation using the Microsoft C compiler, software
integration, and software/hardware debugging. The only thing missing is a
good idea for an embedded system, which we leave to the reader as an
assignment.

[Tracy Kidder's The Soul of a New Machine, Little, Brown and Company,
Inc., 1981, contains several excellent chapters, that detail the trials
and tribulations of real-time hardware and software debugging. Readers
interested in this topic should make the effort to read it.──Ed.]


Figure 1:  A comparison of the memory configurations of embedded and MS-DOS
           environments.

    Memory Map of the MS-DOS              Typical Memory Configuration
          Environment                         of an Embedded System

      ┌─┌─────────────┐◄FFFF:000FH          ┌─┌─────────────┐◄FFFF:000FH
      │ │             │ (1 MByte)           │ │  Bootstrap  │ (1 MByte)
    ROM │  ROM BIOS   │                     │ ├─────────────┤◄FFFF:0000H
      │ │             │                   ROM │ Initializer │
      └─├─────────────┤◄F000:0000H          │ │             │
        │  Other ROM  │                     │ │  and Code   │
        ≈             ≈                     └─├─────────────┤◄E000:0000H
        │   and RAM   │                       │   Memory    │ (Assume 128K
      ┌─├─────────────┤◄Top of RAM            ≈   Mapped    ≈  ROM)
      │ │ COMMAND.COM │ (A000:0000H           │    I/O      │
      │ │ (transient) │ for IBM PC)         ┌─├─────────────┤
      │ ├─────────────┤                     │ │ Initialized │
      │ │    Free     │                     │ │    Data     │
      │ ≈             ≈                     │ ├─────────────┤
      │ │    RAM      │                     │ │Uninitialized│
      │ ├─────────────┤                   RAM │    Data     │
      │ │ Application │                     │ ├─────────────┤◄0000:0200H
      │ │   Program   │                     │ │  Interrupt  │
      │ ├─────────────┤◄Location of         │ │   Vectors   │
      │ │ COMMAND.COM │ application         └─└─────────────┘◄0000:0000H
      │ │ (resident)  │ program is
      │ ├─────────────┤ determined
      │ │   MS-DOS    │ at load time
      │ │  Operating  │
      │ │ System and  │
      │ │ Installable │
      │ │   Device    │
      │ │   Drivers   │
      │ ├─────────────┤◄0000:0600H
      │ │  RAM BIOS   │
      │ │   Tables    │
      │ ├─────────────┤◄0000:0400H
      │ │  Interrupt  │
      │ │   Vectors   │
      └─└─────────────┘◄0000:0000H


Figure 2:  Sample Microsoft C Start-Up Routine for Embedded Applications

     NAME STARTUP_CODE

; THIS IS A SAMPLE START-UP PROGRAM FOR AN EMBEDDED SYSTEM.
; THE MEMORY LAYOUT CONFORMS TO THE NAMING CONVENTION USED
; IN MICROSOFT C, VERSION 4.X AND 5.X.
; THIS ASSEMBLY FILE SHOULD BE ASSEMBLED USING MICROSOFT
; MACRO ASSEMBLER USING THE FOLLOWING COMMAND, ASSUMING THIS
; FILE IS CALLED STARTUP.ASM:
; >MASM STARTUP/MX,STARTUP,STARTUP,NUL
; THE /MX OPTION IS USED TO PRESERVE LOWER-CASE IN PUBLIC
; AND EXTERNAL NAMES.
;
;  SETUP OF SEGMENT CLASSES IN MICROSOFT C 5.X PROGRAMS.
;
;  HIGH ADDRESS
;    - ────────────
;    | |'BOOTSTRAP'|
;    | ────────────
;    | :           :
;    | :           :
;    R ────────────           -    THESE TWO CLASSES ARE TO BE
;    O | 'CONST'   |          ^     COPIED TO THEIR RAM
;    M ────────────           |     LOCATIONS AT POWER-UP
;    | | 'DATA'    |          v
;    | ────────────      <-   ROMCODE_END SEGMENT
;    | | 'CODE'    |
;    - ────────────      <-   ROMCODE_BEG SEGMENT
;             :           :
;             :           :
;    - ────────────      <-   UDATA_END SEGMENT
;    | | 'STACK'   |
;    | ────────────
;    | | 'BSS'     |
;    | ────────────      <-   IDATA_END & UDATA_BEG
;    | | 'CONST'   |          ^     SEGMENTS
;    R ────────────      |-   THESE TWO CLASSES ARE TO BE
;    A | 'DATA'    |          v     INITIALIZED BY START-UP
;    M ────────────      <-    ROUTINE AT POWER-UP
;    | :           :          |
;    | :           :          |-   IDATA_BEG SEGMENT
;    | ────────────
;    | | 'INT_PTR' |
;    - ────────────
;  LOW ADDRESS
;
; CHOOSE THE MEMORY MODEL BY SETTING THE APPROPRIATE LABEL
; TO 1.
;
SMALL      EQU 0
COMPACT    EQU 0
MEDIUM     EQU 0
LARGE      EQU 1
HUGE    EQU  0
STACK_SIZE EQU 400H      ; RESERVE 1Kb WORD OF STACK OR ANY SIZE YOU WANT
__acrtused EQU 1         ; IT IS USED TO SATISFY THE EXTERNAL REFERENCE
                         ; FOUND IN EVERY OBJECT FILE GENERATED
                         ; BY THE MICROSOFT C COMPILER, VER 5.X
  PUBLIC  __acrtused     ; THE SYMBOL HAS TO BE IN LOWER-CASE

DGROUP GROUP
IDATA_BEG,_DATA,CONST,IDATA_END,UDATA_BEG,_BSS,STACK,UDATA_END

; THIS SEGMENT MARKS THE BEGINNING OF ROM CODE
ROMCODE_BEG SEGMENT PARA 'ROMCODE_BEG'
ROMCODE_BEG ENDS

IF SMALL OR COMPACT
_TEXT SEGMENT BYTE PUBLIC 'CODE'
     EXTRN     _main:NEAR     ;C main()
ASSUME  CS:_TEXT
ENDIF
IF MEDIUM OR LARGE OR HUGE
     EXTRN     _main:FAR ;C main()
STARTUP_TEXT SEGMENT BYTE PUBLIC 'CODE'
ASSUME  CS:STARTUP_TEXT
ENDIF
     ASSUME DS:DGROUP, SS:DGROUP
     PUBLIC START
START:
START_I2ICE LABEL BYTE    ; ADDRESS LABEL FOR INTEL I2ICE
     CLI
     CLD
; ************************************************************
; *** YOUR CODE HERE TO DO HARDWARE INITIALIZATION AND RAM CHECK ***
;************************************************************
        PUBLIC INIT_RAM_DATA
INIT_RAM_DATA:
INIT_RAM_DATA_I2ICE LABEL BYTE     ; ADDRESS LABEL FOR INTEL I2ICE
; *** TRANSFER INITIALIZATION DATA FROM ROM TO RAM ***
; COMPUTE THE SIZE OF 'DATA' AND 'CONST' CLASS IN NO. OF PARAGRAPHS:

     MOV AX,SEG IDATA_END ; FRAME NO. OF IDATA_END SEGMENT
                          ; = END OF 'CONST' CLASS IN RAM
     MOV BX,SEG IDATA_BEG ; FRAME NO. OF IDATA_BEG SEGMENT
                          ; = START OF 'DATA' CLASS IN RAM
     SUB AX,BX            ; AX = SIZE OF 'DATA' & 'CONST' CLASSES
     MOV CL,3             ; SET UP TO MULTIPLE BY 8
     SHL AX,CL            ; COMPUTE NO. OF WORDS TO BE TRANSFERRED
     MOV CX,AX            ; TRANSFER COUNTER
     JCXZ NO_IDATA_DATA

; OBTAIN THE FRAME NO. OF 'DATA' CLASS PLACED IN ROM

     MOV AX,SEG ROMCODE_END ; FRAME NO. OF ROMCODE_END SEGMENT
                            ; = BEGINNING OF 'DATA' CLASS IN ROM
     MOV DS,AX              ; SOURCE STRING
     MOV SI,0

; OBTAIN THE FRAME NO. OF 'DATA' CLASS LOCATED IN RUN-TIME RAM SPACE

     MOV AX,SEG IDATA_BEG ; FRAME NO. OF IDATA_BEG SEGMENT
                          ; = BEGINNING OF 'DATA' CLASS IN RAM
     MOV ES,AX            ; DESTINATION STRING
     MOV DI,0

; INITIALIZE THE 'DATA' AND 'CONST' CLASSES IN
; RUN-TIME RAM SPACE

REP  MOVSW                         ; BEGIN WORD TRANSFER
        PUBLIC NO_IDATA_DATA
NO_IDATA_DATA:
NO_IDATA_DATA_I2ICE LABEL BYTE     ; ADDRESS LABEL FOR INTEL I2ICE

;*** CLEAR UNINITIALIZED DATA
; COMPUTE THE SIZE OF 'BSS' & 'STACK' CLASSES IN NO. OF PARAGRAPHS:

     MOV AX,SEG UDATA_END         ; FRAME NO. OF UDATA_END SEGMENT
                                  ;  = END OF 'STACK' CLASS IN RAM
     MOV BX,SEG UDATA_BEG         ; FRAME NO. OF UDATA_BEG SEGMENT
                                  ;  = START OF 'BSS' CLASS IN RAM
     SUB AX,BX                    ; AX = SIZE IN PARAGRAPHS
     MOV CL,3                     ; SET UP TO MULTIPLE BY 8
     SHL AX,CL                    ; COMPUTE NO. OF WORDS TO BE INITIALIZED
     MOV CX,AX                    ; SETUP COUNTER
     JCXZ NO_UDATA
     MOV ES,BX                    ; SETUP DESTINATION POINTER
     MOV DI,0
     MOV AX,0                     ; INITIALIZE TO 0
REP  STOSW
        PUBLIC NO_UDATA
NO_UDATA:
NO_UDATA_I2ICE LABEL BYTE         ; ADDRESS LABEL FOR INTEL I2ICE

; *** SETUP DATA AND STACK SEGMENT ***

     MOV AX,DGROUP
     MOV DS,AX                    ; SETUP DATA SEGMENT
POINTER
     MOV SP,OFFSET DGROUP:STACK_TOP
     STI                          ; ENABLE INTERRUPT NOW
     JMP _main                    ; ENTER C main() FUNCTION
     HLT
IF SMALL OR COMPACT
_TEXT     ENDS
ENDIF
IF MEDIUM OR LARGE OR HUGE
STARTUP_TEXT   ENDS
ENDIF

; THIS SEGMENT MARKS THE END OF ROM CODE
ROMCODE_END SEGMENT PARA 'ROMCODE_END'
ROMCODE_END ENDS

; *** THE FOLLOWING SEGMENTS ARE ALL LOCATED IN RAM ***

INT_PTR SEGMENT AT 0H         ;'INT_PTR'
; INTERRUPT POINTER TABLE, LOCATE AT 0H IN RAM.
     PUBLIC TYPE_0,TYPE_1,TYPE_2,TYPE_3,TYPE_4,TYPE_32
TYPE_0    DD  ?     ;DIVIDE-ERROR
TYPE_1    DD  ?     ;SINGLE-STEP
TYPE_2    DD  ?     ;NON-MASKABLE INTERRUPT
TYPE_3    DD  ?     ;BREAKPOINT
TYPE_4    DD  ?     ;OVERFLOW
; SKIP INTEL RESERVED VECTORS
     ORG  32 * 4
TYPE_32   DD  ?     ;MAY PLACE YOUR VECTORS HERE
; MORE INTERRUPT VECTORS
INT_PTR   ENDS

; THIS SEGMENT MARKS THE BEGINNING OF DGROUP AND
; INITIALIZED DATA IN RAM
IDATA_BEG SEGMENT PARA PUBLIC 'IDATA_BEG'
IDATA_BEG ENDS

_DATA SEGMENT WORD PUBLIC 'DATA'
_DATA ENDS

CONST SEGMENT WORD PUBLIC 'CONST'
CONST ENDS

; THIS SEGMENT MARKS THE END OF INITIALIZED DATA IN RAM
IDATA_END SEGMENT PARA PUBLIC 'IDATA_END'
IDATA_END ENDS

; THIS SEGMENT MARKS BEGINNING OF UNINITIALIZED DATA IN RAM
UDATA_BEG SEGMENT PARA PUBLIC 'UDATA_BEG'
UDATA_BEG ENDS

_BSS SEGMENT WORD PUBLIC 'BSS'
_BSS ENDS

STACK     SEGMENT PARA STACK 'STACK'
     DW STACK_SIZE DUP (?)
STACK_TOP LABEL WORD
STACK     ENDS

; THIS SEGMENT MARKS THE END OF UNINITIALIZED DATA IN RAM
UDATA_END SEGMENT PARA PUBLIC 'UDATA_END'
UDATA_END ENDS

; THIS SEGMENT CONTAINS BOOTSTRAP CODE AT HARDWARE RESET OR POWER-UP
; FAR JUMP TO PROGRAM START, LOCATE AT FFFF0H IN ROM.
BOOTSTRAP SEGMENT AT 0FFFFH   ; 'BOOTSTRAP'
     JMP FAR PTR START
BOOTSTRAP ENDS
     END START


Figure 3:  Examples of Functions in Microsoft C Run-time Library

DOS Dependent Functions (Not suitable for embedded systems)

  fopen     opens a file
  fclose    closes a file
  malloc    allocates a memory block
  printf    formats and prints a series of characters

DOS Independent Functions (Suitable for embedded systems)

  atoi      converts string to int
  sprintf   formats and stores a series of characters
  strcmp    compares strings
  strcpy    copies strings


Figure 4:  Block Diagram of the Link&Locate++ Package

           ╔════════╗      ┌───────────┐      ┌──────────────┐
           ║ Object ╟──┬──►│ CREF86    ├─────►│ Cross        │
           ║ Files  ║  │   │ Utility   │      │ Reference    │
           ╚════════╝  │   └───────────┘      └──────────────┘
                       │   ┌───────────┐      ┌──────────────┐
                       ├──►│ XLIB86    ├─────►│ Intel OMF    │
                       │   │ Librarian │      │ Library File │
                       │   └───────────┘      └──────┬───────┘
                       │   ┌───────────┐             │
                       └──►│ XLINK86   │◄────────────┘
                           │ Linker    │
                           └────┬──────┘
                           ┌────▼──────┐
                           │ XLOC86    │
                           │ Locator   │
                           └────┬──────┘      ┌───────────┐
                           ┌────▼──────┐      │ Intel OMF │
                           │ CV20MF    ├─────►│ Absolute  │
                           │ Formatter │      │ Code      │
                           └────┬──────┘      └───────────┘
                           ┌────▼──────┐      ┌───────────┐
                           │ PROM86    ├─────►│ Intel Hex │
                           │ Formatter │      │ File      │
                           └───────────┘      └───────────┘


Figure 5:  The environment for developing embedded system applications in
           Microsoft C.
                                  ┌──────────────┐
                        ╔═════════▼══════════╗   │
                        ║  Microsoft Editor  ║   │
                        ║                    ║   │
                        ╚═════════╤══════════╝   │
                        ╔═════════▼══════════╗   │
                        ║    Microsoft C     ║   │
                        ║Optimizing Compiler ║   │
                        ╚═════════╤══════════╝   │
                        ╔═════════▼══════════╗   │
                        ║SSI Link & Locate ++║   │
                        ║      Package       ║   │
                        ╚═════════╤══════════╝   │
                        ╔═════════▼══════════╗   │
                        ║    Intel I²ICE     ║   │
                        ║     Emulator       ║   │
                        ╚═════════╤══════════╝   │
                                  └──────────────┘


Figure 6:  C Source for Functions that Implement a Ring Buffer

/* To generate in-line 8086 opcodes for intrinsic functions,
   such as inp, outp, and _enable, specify /Oi in C compiler */

#define RING_BUF_SIZ 128
#define PICIER 0xC2
#define PICEOI 0xC0
#define IRQDIS 0x20
#define EOI 0x20
#define SIOPORT 0x10
#define OUTPORT 0x12
#define enable_sio() picmask = inp(PICIER); \\
                     picmask &= (~IRQDIS); \\
                     outp(PICIER, picmask);
#define disable_sio() picmask = inp(PICIER); \\
                      picmask |= IRQDIS; \\
                      outp(PICIER, picmask);
char ring_buf[RING_BUF_SIZ];
char *in_ptr = ring_buf;
char *out_ptr = ring_buf;
int count = 0;
char picmask;
void process_data(char);

main()
{
    while(1)
    {
        enable_sio();             /* enable serial i/o */
     if (count > 0)
     {
         process_data(*out_ptr);
         disable_sio();           /* disable serial i/o */
         count-;
         if (out_ptr++ == ring_buf + RING_BUF_SIZ)
          out_ptr = ring_buf;     /* ring buffer */
         enable_sio();            /* enable serial i/o */
     }
    }
}

void process_data(c)
char c;
{
    outp(OUTPORT, c);
}

void interrupt far sio_handler()
{
    _enable();                    /* enable higher order interrupt */
    *in_ptr = inp(SIOPORT);
    if (in_ptr++ == ring_buf + RING_BUF_SIZ)
     in_ptr = ring_buf;
    if (count != RING_BUF_SIZ)
     count++;
    outp(PICEOI, EOI);            /* reset interrupt */
}

████████████████████████████████████████████████████████████████████████████

C Scope and Linkage: The Keys to Understanding Identifier Accessibility

Greg Comeau☼

The proper use of data structures is one of the chief influences on how
well you write your C programs. To program properly you must understand
data structures from both a syntactical viewpoint and a semantic
viewpoint. You must be able to read and write the C declarations
associated with your programs' identifiers so well that you could describe
them in English and write them in C from a description in English (see
"Understanding C Declarations," MSJ, Vol. 3, No. 5).

Furthermore, you need to be able to comprehend fully the semantics and
usage of identifiers and their relationships so as to make better use
of them in such common coding practices as structured programming and data
hiding. Once you've grasped their usage, you will better understand the
compiler and run-time errors associated with the data structures within
your programs, which will make debugging substantially easier.

The ideas discussed here build on those presented in the C declarations
article in the last issue. We will discuss the various concepts
pertaining to the use of identifiers both within and across your programs'
different source files. You won't be using C to its full capacity until
you know about all the various attributes that can be given to an
identifier. Learning this will surely lead to better and more professional
programming technique.


Declaration or Definition

Most of the time, programmers use the words declaration and definition
interchangeably, which is a mistake. A declaration is a description of
the various attributes of a given identifier, that is, its size, shape,
and characteristics. A declaration does not reserve any space for the
identifier; it only states something about it. A definition, on the other
hand, is a declaration that will reserve storage when the given program
is executed, an important difference that will become more apparent as
you read on.


Scope

C has several rules that control when a given identifier is visible
within your code. This visibility is called the scope of the identifier
and determines the accessibility of the identifier by the other parts of
the program that the identifier resides in. For example, function
parameters cannot be accessed by name except within the function that they
are declared in. Therefore the scope of these parameters is limited only
to functions, that is, they are only visible inside of a function.

There are four places where the scope of an identifier pertains: blocks,
functions, function prototypes, and files. The applicable scope of an
identifier will begin on entering one of these four places and will end on
exiting them. For instance, an identifier with block scope begins at the
beginning of a block and ends at the end of that block.

Figure 1 is a simple table comparing the four scopes. The first column
lists the types of scope and the second column lists the positional
properties of the places where that scope may be used in your code. Let's
look at each type of scope in more detail; refer to Figure 1 as the
discussion proceeds.

Function Scope The only type of identifier capable of having function
scope is a label. Since labels must be unique within functions they may
not appear within the same function more than once, even within a block.
You can use them in other functions without conflict if you need to. You
will see why this is not a problem when we discuss linkage.


File Scope Identifiers with file scope are those with declarators or type
specifiers outside any parameter list or function. File scope begins at
the end of the declarator and does not end until the current source file
has been analyzed. This means that once an identifier with file scope
goes "into" scope, it can be referenced from any other variable or
function that is below it but not above it.

The code in Figure 2 illustrates this. Note that even though file_scope
appears outside all the routines, it is not known to main because it is
not declared until after main. In other words, its scope is not determined
until its name becomes visible through its declaration. Only when the
declaration has been completed can file_scope be referenced by the other
parts of the program following it.

An interesting question is whether or not it would make sense to put
file_scope after main and f( ) regardless of whether file_scope was
actually referenced in that particular file or not. (Part of the answer
to this will appear later in the article.)

Block Scope The philosophy of block scope is somewhat like that of file
scope, but its universe is a bit smaller. An identifier with block scope
has declarators or type specifiers that are in a block, that is, within
braces ({}), or are parameters. Identifiers with block scope are typically
called local variables because they are only known within the function
that they appear in. This description of block scope is correct;
however there is a confusing exception involving "local externals,"
which will be explained when we discuss the storage class.

Function Prototype Scope Function prototype scope is the easiest scope to
remember. It is simply an identifier declared within a function prototype.
The scope of the identifier ends when the function declarator ends.
Although the inclusion of an identifier in a function prototype is
optional, this works out nicely since the identifier serves as a comment
and encourages more readable code.

Note, however, that an identifier with function prototype scope is only
in effect in what is normally called a declaring or forward referencing
prototype. If the prototype is a defining prototype (a prototype used at
the same time that the code for the function is being presented and
defined), then those identifiers are legitimate parameters and, as
mentioned earlier, will have block scope if they are coded that way.


Loose Ends on Scope

To use scope to your advantage, there are a few more things you need to
know. First, identifiers go in and out of scope. For instance, take a
look at the code and output in Figure 3. In line 1, id comes into scope
(file scope to be exact) and remains that way until line 5. Here there is
another identifier with the same name with block scope. In such a
situation, the previous identifier temporarily goes out of scope until
the scope of the current identifier (the newly scoped one with the same
name) terminates. In this case, that would be at the end of the function
(line 15). Therefore at line 16, the id with file scope is visible again
and will print out correctly on line 19 as there is no other identifier
named id in scope.

Now notice what is happening at line 10. A new block is introduced by
being enclosed between braces in the middle of the main function. This is
valid C and permits you to declare/define your identifiers closer to
where they are actually used. Also, in some cases, it could generate more
efficient use of the stack or, in the case of local and blocked register
variables, more efficient code. So, at line 10 yet another instance of id
is brought into scope, at the third scoping level, since the two id's at
lines 1 and 5 are still waiting to come back into scope. At this point,
the id in line 10 goes into scope, stacking up the id on line 5 until this
"new" id goes out of scope on line 13. Again, this new id references a
different identifier from the other ids.


Linkage

Linkage is a mechanism whereby the identifiers declared in your code as
objects or functions can be resolved regardless of the similarity or
difference of scope. The resolution involves breaking down each
translation unit, that is, source file, such that references to given
objects or functions, whether or not they are duplicates, can be
acknowledged and linked with appropriate library routines and a
resulting program image, typically an executable binary file, created.

Just as in scope, there are various levels of linkage: external linkage,
internal linkage, and no linkage. Figure 4 summarizes each type of
linkage.


External Linkage

An identifier with external linkage has either an extern storage class or
is an identifier with file scope that has not been explicitly declared
static. Also, each reference to an identifier with external linkage is a
reference to the same object or function regardless of how many or
which source files or libraries reference it.

Therefore, the identifier ext in Figure 5 would have external linkage. If
the two routines, main and f( ), were compiled and linked, they would both
use the same ext. So, if you were to execute the program you would get a
predictable output as illustrated. Note that from point a to point b, ext
still hasn't changed its value. But once f( ) sets it to 2, it will remain
that way until another subsequent assignment to it, which hasn't happened
when it reaches point d.

A definition of a nonstatic identifier with file scope is implicitly
taken to be an extern. Note though that there may only be one definition
of an identifier regardless of how many source files are involved. Many
compilers are relaxing the constraints of this rule.


Internal Linkage

Identifiers with internal linkage look very much like those with external
linkage. The similarity is that they both reference identifiers with
file scope; the difference is that those with internal linkage have been
explicitly declared with the storage class static.

Internals are useful for data-hiding situations. For instance, some of the
routines in the standard library such as fopen, fclose, and malloc,
usually need to use identifiers with internal linkage. The constraint
against using internals is that information about them is not published
to the linker for use by other modules. Thus they are not known to or
accessed by any other source files.

When they are known by the linker, or perhaps by a debugger, the linker
will effectively treat internals as comments because they will not be put
into the external symbol table. You are free to use an internal within a
given source file without being concerned about its name clashing with
another variable of the same name in another source module, regardless of
whether that other identifier is external or internal.

Figure 6 shows a program that illustrates identifiers with internal
linkage. The program may be a bit hard to comprehend since this technique
is not used very often, but it does work. I encourage you to experiment
with the example in Figure 6 until you understand the concept of internal
linkage.

A brief explanation to help you: The program has two source files, one
containing main and another containing f( ). As mentioned before,
identifiers with internal linkage are those with file scope that have been
explicitly declared with the static keyword. Also, we know that internals
are not used in the linker's external symbol table when it begins
resolving missing externals.

Therefore, each source file in Figure 6 has its own copy of an identifier
named internal (yes, they are two distinct ints). This is verified in the
output since the values at points a and d are the same. The output from
point b is a bit more tricky. We get a value of 0 because all variables
with static storage, regardless of whether they have internal or external
linkage, are automatically initialized to 0; this is just standard C.
Finally, notice that when the internal in f( ) is assigned the value 2 (as
seen in point c), it does not influence the value printed at d (again,
they are two different memory locations).

By combining the example in Figure 6 with the example in Figure 5, you
could concoct something like the code and output shown in Figure 7.

Pay careful attention to the program in Figure 7 (example1.c through
example 4.c) and thoroughly examine the resulting output. A few comments
are in order: points a, d, g, and j represent the variable messy, which
is associated with main; points b and c represent a different variable
messy, which is associated only with the function x; points e, f, h, and i
represent yet another variable named messy, which is associated only with
functions y and z. Note how points f and h print the same value as they
both use the same variable messy.


No Linkage

Finally, there are identifiers that have no linkage: parameters,
nonobjects and/or nonfunctions (including typedefs, aggregate tags, and
function prototype identifiers), and nonexterns in blocks.

This implies that only identifiers with file scope are candidates for
linkage. Does this make sense? In the case of typedefs, structure tags,
and union tags it does because they are not still around during execution
time, that is, they are not objects. Also, because labels, parameters,
and block identifiers are only found within functions, they are typically
indexed off the stack, or they are register-based and need not be
resolved. Hence the linker does not have to be told anything about them.

At this point, take another look at the definition of linkage. It should
be clear that there is no reason to resolve anything classified with no
linkage because no other source file should attempt to access it by name.


Linkage Problems

Many of the advantages of using scope can be extended once the linkage of
identifiers is understood. You now have a good framework of how to
control instances and the lifetime of identifiers; however, there are
some problems.

For instance, what happens if you have a routine that accesses an
identifier with both internal and external linkage? Such a program is
shown in Figure 8. This example just doesn't seem right, yet C lets this
get by. On the other hand, different compiler vendors treat this problem
in different ways. With some it works; with others ext_int either remains
external and never internal or vice versa. Still others will treat this
as a syntax error. The draft ANSI C standard has recognized this problem
and has called such coding practices "undefined behavior," which means
stay away from it.

Another problem with linkage occurs when there are identifiers with
mismatched types across source files. This can happen very easily without
your being aware of it and will cause serious difficulties if you don't
understand what you've done. For instance, a C programmer (one not very
experienced with using C, we hope) might decide to use an array in his or
her program and at some point either abuses or misunderstands the
relationships between arrays and pointers. Can you figure out why the
following code will compile and link, yet not work?

  int    array[10];
  void   func();
  main
  {
   <...>
   array[5] = <...>
   <...>
   func();
   <...>
  }

with another source file containing:

  int    *array;
  void   func()
  {
   <...>
   array[5] = <...>
   <...>
  }

Think about it for a bit. The reference to array in the source file
containing main is fine. However, let's take a closer look at what is
happening in the other source file.

Does array[5] really do what it appears to be doing? The answer is no;
this is due to the fact that array has been redeclared as a pointer. This
is a problem because it will take the "thing" array points to, index 5
ints down, and place a value at that address according to the assignment,
which is an error. Is it clear what value the array pointer points to?
Assuming that sizeof(int) == sizeof(int pointer), that value is actually
array[0].

For instance, if I had initialized array as follows:

  int  array[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };

in the source file containing main, the reference to array[5] in func,
which maps into *(array + 5), would become *((int *)1 + 5). It produces
(int *)1, or simply 1, because both references to array in the two source
files will become resolved by the linker as beginning at the same address,
which is wherever array resides. Since a pointer is a variable that
contains the address of another variable, a reference to array in func is
a reference to the object that array apparently points to; in this case,
it is the 1.

On many CPU architectures this error would be obvious the very first time
array[5] was accessed in func because many of these machines do not allow
the access of an integer on odd boundary addresses. Normally this would
result in a memory addressing fault of some sort so that a programmer at
least has a chance to debug it. However, if the value at array[0] is an
even integer, a fault will not occur. Also, on such machines as the IBM(R)
PC, which permit odd addressing of integers, the problem will not be
apparent either. Debugging this sort of thing is generally futile because
the programmer won't recognize the semantic difference between the
accesses to array in the two source files.

Yet another problem is that C is case-sensitive, while many linkers are
not. This leads to situations in which the linker will map identifier
names that are in lowercase to uppercase. Unfortunately this means an
identifier named SampleIdentifier and another named sampleidentifier would
be treated as if they were both at the same memory location──not a very
pleasant scenario. Fortunately, many linkers are being updated to solve
this problem.

Along the same lines, another problem is one of identifier name length.
Most compilers will usually allow up to 31 or 32 characters for
constructing identifier names. However, this is a syntactical constraint
chosen by your compiler, and even though your program may compile
successfully, a linker may choose to truncate your identifiers to as
little as six characters.

For example, an identifier named Identifier1 and another named Identifier2
might end up truncated to Identi and will therefore (sadly) represent the
same variable. Even worse, some systems will not even inform you that
this has been done. This problem is directly related to your operating
system and/or development tools. Microsoft(R) C does not do this, but you do
need to be aware of this possibility when you port your programs to other
systems.

Another serious problem, which fortunately does not occur with MS(R) C,
happens on systems that choose to publish internals to the linker in
such a fashion that they will be resolved with externals of the same
name. This means that the programs in Figures 6 and 7 will not work as I
have described them on such systems. There aren't too many linkers that
work in this manner, and they are usually found on specialized or
proprietary systems. However, if you are writing code and intend to port
it to other systems you need to be aware of this. If you do a lot of
porting, I strongly advise you to write a few programs that test the
linkage aspects of your programs before doing the actual port.


Redeclarations

Many C compilers do not like to parse duplicate declarations or
definitions of identifiers. I do not know why this happens since C allows
it as long as the declaration or definition defines the same object with
the same sizes and types. This discussion may sound a bit silly since most
people would not code something like:

  int a;
  int a;

However, it does come in very handy in cases where you have programs that
automate your development by writing other C programs. Or maybe you are
including various header files and two of them end up including the same
one, say STDIO.H. Also, consider the case of:

  char *p1 = &p2;
  char *p2 = &p1;

Since the scope rules of C maintain that p2 is not known (that is, not in
scope) while parsing p1, how can we then get the address of it? The only
way is to create a forward reference to it with either:

  char *p2;

or:

  extern char *p2;

placed before p1.

Some of you may argue that since C is a two-pass compiler, it should be
able to pick up p2 the second time around. First, there is no guarantee
that any particular C compiler will be implemented as a two-pass
compiler. Second, even though it is technically possible to determine if
p2 actually exists (for instance, many assemblers work this way), it is
not within the definition of the C language to investigate it.

Some of this thinking goes back to C's philosophy of keeping things
simple and letting the compiler finish as quickly as possible. Compiler
writers did not want their compilers using too many complex algorithms for
the sake of efficiency. Today, the mentality behind these concerns is
probably much different, but the choice usually comes down to keeping
within the spirit of C, so many elements of C have remained intact.


Storage Duration

One final topic I've alluded to throughout this article is the storage
duration of an identifier. The MS C documentation and most texts have an
acceptable description of this, so I will only focus on those issues about
storage classes that relate to our discussion.

First, let's define what storage duration is and then how it relates to
scope and linkage. The storage duration of an identifier mandates how long
it will exist. It is worth noting that only objects can have a storage
duration. The storage duration of an identifier is specified
syntactically via its storage class and can assume either of two
values: static or automatic. An object is either in existence all the
time, meaning that it is static, or its lifetime can be sporadic and
dynamic while the program is running, meaning that it is automatic.

Objects with static storage are those with external linkage, internal
linkage, or those objects that have no linkage but have been declared
explicitly with the static storage class. Objects with automatic storage
are those with no linkage and no explicit static storage class.

You can see that the lifetime of an object directly influences its linkage
and in most cases its scope. For instance, all identifiers in your
programs with external and internal linkage are implicitly static. Also,
all identifiers defined with the static storage class keyword are also
static, regardless of their scope (file or block). Therefore, all static
objects remain intact from program startup through program execution and
termination. All automatics have limited and asynchronous lives,
including new and multiple ones.


Storage Classes

There are a few points to clarify now that most of the characteristics
of identifiers have been explained. First of all, there is a slightly
confusing syntactical allowance that doesn't quite fit with what I've
said. This is the ability to reference identifiers with file scope by
referencing the declaration in such a way that they appear to have block
scope and, perhaps worse, no linkage. In other words, sometimes it
appears that a given identifier has mixed linkage.

However, this is not really the case. For example, Figure 9 contains an
identifier i within its main program that does not go into file scope
until after the main routine is finished. C allows you to use a shortcut,
the extern int i, to reference it.

The question is, does i have block scope or file scope? Without the
extern, it is an identifier with block scope and no linkage. However, we
know it references an identifier with file scope and external linkage. C
resolves this by doing the logical thing; the identifier is given block
scope visibility. This permits the function to access the external
without bothering any other routines.

In addition to these considerations, some compilers will act
differently. For instance, MS C treats all extern function declarations
with file scope and visibility from within the rest of the program
regardless of where they were declared, even if they were declared from
within blocks that have been terminated. This is not a widely used
mechanism because of its variety, but it is endorsed by the ANSI draft;
since the compiler already has the information there is no reason to throw
it away.

The concepts of scope and linkage are not always obvious to programmers
because of their diversity of implementation and use. However, they are
powerful features of C and when well understood should lead to programs
that are integrated correctly and thus behave more rationally. It is
another piece of C knowledge the programmer needs in order to be as
professional as possible and is even more important when working on large
projects, especially those involving more than one programmer.


Figure 1:  Scope Types

  Types of    Identifier Visibility
  Scope

  Function    Labels

  File        Declarations or type specifiers outside a function or
              parameter list

  Block       Declarations or type specifiers inside a function or
              parameter list

  Function    Declarations or prototype type specifiers within a parameter
  Prototype   declarator list


Figure 2:  Example of File Scope

  void f();

  main()
  {
       f();
       printf("%d\n", file_scope);     /* error */
  }

  int  file_scope = 1;

  void f()
  {
       printf("%d\n", file_scope);     /* okay */
  }


Figure 3:  Example of a variable going in and out of scope.

  1  int  id = 5;
  2
  3  main ()
  4  {
  5  int  id = 4;
  6
  7       printf("id at a%d\n", id);
  8       f();
  9       {
 10            int  id =3;
 11
 12            printf("id at b=%d\n", id);
 13       }
 14       printf("id at c=%d\n", id);
 15  }
 16
 17  f()
 18  {
 19       printf("id at d=%d\n", id);
 20  }

Resulting output:

id at a=4    /* local variable within main - decl at line 5 */
id at d=5    /* variable with file scope even through f() is */
             /* called from line 8 - decl at line 1   */
id at b=3    /* local variable within block within main - decl at line 10 */
id at c=4    /* local variable within main - decl at line 5  */


Figure 4:  Linkage Types

  Types of     Identifier Constraints
  Linkage

  External     Extern or file scope

  Internal     File scope but static

  None         Parameters, nonobjects, nonfunctions, nonexterns in blocks


Figure 5:  Example of External Linkage

extern    int  ext;
void      f();

main()
{
     ext = 1;
     printf("ext at point a=%d\n", ext);
     f();
     printf("ext at point d=%d\n", ext);
}

Function f( ) is located in a different source file:
extern    int  ext;

void f()
{
     printf("ext at point b=%d\n", ext);
     ext = 2;
     printf("ext at point c=%d\n", ext);
}

Resulting output:
     ext at point a=1
     ext at point b=1
     ext at point c=2
     ext at point d=2


Figure 6:  Example of Internal Linkage

static  int    internal;
void            f();

main()
{
        internal = 1;
        printf("internal at point a=%d\n", internal);
        f();
        printf("internal at point d=%d\n", internal);
}

Function f( ) is located in a different source file:
static  int    internal;

void           f()
{
        printf("internal at point b=%d\n", internal);
        internal = 2;
        printf("internal at point c=%d\n", internal);
}

Resulting output:
    internal at point a=1    /* use the internal from main */
    internal at point b=0    /* use the internal from f    */
    internal at point c=2    /* use the internal from f    */
    internal at point d=1    /* use the internal from main
                              * notice it has the same value
                              * as in point a since it hasn't
                              * been reassigned */


Figure 7:  Output resulting from the compilation of example 1.c through
           example 4.c into one executable.

/* example 1.c */

static int messy;
void x(), y(), z();

main()
{
     messy = 1;
     printf("a=%d\n", messy);
     x();
     printf("d=%d\n", messy);
     y();
     printf("g=%d\n", messy);
     z();
     printf("j=%d\n", messy);
}

/* example 2.c */

static int messy;

void x()
{
     printf("b=%d\n", messy);
     messy = 2;
     printf("c=%d\n", messy);
}

/* example 3.c */

int messy;

void y()
{
     printf("e=%d\n", messy);
     messy = 3;
     printf("f=%d\n", messy);
}

/* example 4.c */

extern int messy;

void z()
{
     printf("h=%d\n", messy);
     messy = 4;
     printf("i=%d\n", messy);
}

Resulting output:
   a=1
   b=0
   c=2
   d=1
   e=0
   f=3
   g=1
   h=3
   i=4
   j=1


Figure 8:  C allows the same variable to be both external and internal;
           however, the result is compiler dependent. The draft ANSI C
           standard labels this practice as undefined behavior.

int  ext_int;  /* this establishes external linkage */
void f();

main()
{
     ext_int = 1;
     f();
}

static    int ext_int:   /* this establishes internal linkage */

void f()
{
     ext_int = 2;
}


Figure 9:  If i has file or block scope, C gives i block scope visibility.

void f();

main()
{
     extern int i;

     i = 5;
     f();
}

int  i;

void f()
{
     printf("i=%d0, i);
}

████████████████████████████████████████████████████████████████████████████

Extending the Functions of the Windows Clipboard with Scrapbook+

Kevin P. Welch and David E. West☼

The Windows clipboard is probably the most commonly used mechanism for the
exchange of data between applications. With the clipboard, a spreadsheet
or drawing created in one program can easily be transferred to another
program without the need for an intermediate file.

Windows comes with a seldom used application called the clipboard viewer,
CLIPBRD.EXE. This program, as its name suggests, allows you to view the
current clipboard contents. Unfortunately, it provides little
additional functionality.

After using the Windows clipboard and associated viewer for a while,
several limitations become obvious:

  ■  the clipboard is restricted to one data item (albeit multiply
     rendered)

  ■  you cannot erase or save the current clipboard contents

  ■  you often need the originating program to copy its data to the
     clipboard

  ■  there is no system-supported mechanism for clipboard format
     manipulation

  ■  you cannot control the data renderings presented by the clipboard

ClickArt(R) Scrapbook+ is a sophisticated Windows application that was
developed to try to overcome some of these limitations. Acting as an agent
for clipboard data, Scrapbook+ lets you place frequently used data in
easily accessible files. When you need the data, it is available to you in
any Windows program simply by pasting from the clipboard. By automatically
creating miniature renderings of the contents in a visual index,
Scrapbook+ allows you to scan quickly through pages of images and data of
all types, then transfer them almost instantly via the clipboard to other
Windows applications. Scrapbook+ was developed using the techniques
described in "Exchanging Data Between Applications Using the Windows
Clipboard" (MSJ, Vol. 3. No. 5). Figure 1☼ demonstrates selecting artwork
from Scrapbook+ and Figure 2☼ demonstrates pasting the artwork into
Aldus(R) PageMaker(R).


Design Concepts

Behind every product there are requirements, issues, frustrations, and
dreams, and Scrapbook+ is no exception. The original idea for the
product arose in the spring of 1987 from a series of informal discussions
between ourselves and the staff at T/Maker, the publisher of Scrapbook+.

Originally conceived as a clip-art manager, Scrapbook+ rapidly evolved
into a full-fledged clipboard data storage and retrieval tool. From the
very start we wanted to build a product that was simple, easy to learn,
and intuitive while also fast and reliable. More importantly, we sought
to create a program that users felt was indispensable, much like The
Norton Utilities(TM).


Data Storage Tool

One of the main problems with the Windows clipboard is its temporal
nature. We wanted to give users a means to save clipboard data for later
use. Initially we hoped to store any kind of data, but midway through the
development of Scrapbook+ we limited it to commonly used and well-
understood formats.

Besides pasting data into Scrapbook+ from the clipboard, we felt it was
necessary to be able to import data directly from disk. This would extend
its functionality to include other types of information available to
the user, some of which were inaccessible within Windows. Although we
realized that this would require the development of some difficult file-
to-clipboard conversion routines, we felt it was necessary for a well-
integrated product.

Taking the data storage concept beyond file-to-clipboard data
conversion, we also wanted to offer several enhanced mechanisms whereby
users could automatically save their clipboard and display state. After
several incarnations this finally evolved into the AutoPaste, Screen
Capture, and the camera features.


Data Retrieval Tool

As frequent users of Aldus PageMaker and Microsoft(R) Excel, we were
frustrated by the inability to access things without the original
application. We wanted to be able to browse visually through large
quantities of text, drawings, or images in search of just the right
addition to the work in progress.

From this need to review and retrieve data rapidly came thumbnails, small
representations of an entire Scrapbook+ page. The thumbnail concept
permitted us to concentrate information and present very large
amounts of data efficiently. By using this visual index a particular
page could be easily found, the correct representation selected, and
the resulting format copied to the clipboard.

Since typical users would probably create their own Scrapbook+ files, we
assumed that they would have access to the program(s) that produced the
data, so we decided not to provide any editing tools. Besides, creating an
editor for each supported format would guarantee that many things would be
done poorly. Eventually we incorporated some limited tools that permitted
selection of a portion of the data which could then be copied to the
clipboard or formed into a new thumbnail representation of the entire
page.

As another data retrieval enhancement, since data could be imported
directly from disk, we considered it appropriate to provide export
capabilities as well. A natural consequence of this decision is that
data could be exported to disk from both the clipboard and the active
Scrapbook+ file.


Agent for Clipboard Data

From the very beginning one aspect of Windows that attracted us was the
ability to exchange information easily between applications. Our concept
for Scrapbook+ extended this notion to include the exchange of data
between disk and application via the clipboard, using Scrapbook+ as an
agent in this process. Building on this idea, we wanted the tool to be
able to circumvent some of the more troublesome clipboard roadblocks
while seamlessly handling the richness it provided.

Although the current version of Scrapbook+ does not directly support
remote data requests and submissions, future editions may address this
issue. With dynamic data exchange (DDE), applications could request or
submit a specific data item. Furthermore, applications could use
Scrapbook+ as an engine for converting to and from the various clipboard
formats and external disk files. By doing so, many developers would be
freed from the tedious task of supporting the ever increasing number of
file and format standards, focusing instead on creating new and
innovative applications.


Visual Components

Scrapbook+ was specifically designed as a visual application. Most users
respond visually to a program, and their understanding is greatly enhanced
if the visual metaphors used reflect the internal operation of the
application.

When you first bring up Scrapbook+ you will see something like what is
shown in Figure 3☼. Although most of the components of the Scrapbook+
window are familiar to experienced Windows users, some require further
explanation (the numbers following the component names are the numbers
with which they are labeled in Figure 3☼).

The description box (6) shows an account of the current page, including
the date and time it was added to the current Scrapbook+ file. The
toolbox (8) is a movable child window that lets you select the current
viewing mode or one of four possible tools. The viewing area (9) displays
one page of the current Scrapbook+ file, as well as allowing you to block
select or lasso a portion of it for copying to the clipboard.

The thumbnail size box (11) controls the number of thumbnail lines
displayed in the Scrapbook+ window. The scroll bar (12) lets you scroll
to thumbnails not visible in the viewing area. By clicking on a page in
the thumbnail area you select it for display in the viewing area. If you
choose to view the thumbnail area (13), the bottom of the Scrapbook+
window will display a thumbnail of each page in the file. Although each
thumbnail may be too small to see page contents in detail, they're large
enough to compare pages quickly when selecting the one you want to display
in the viewing area.


Viewing Modes

Scrapbook+ operates in two viewing modes, clipboard mode and file mode.
Under clipboard mode,contents can be displayed in the viewing area. When
the clipboard is holding information, a small rectangle appears in the
center of the icon (14). If the clipboard contains more than one
rendering, you can select another by choosing one from the View pull-down
menu.

In clipboard mode certain menu options are automatically enabled or
disabled. For example, you can erase the clipboard contents (thus freeing
memory), but selection tools are only available while viewing a page in
the file, not while viewing the clipboard.

You can switch from clipboard mode to file mode by clicking on the page
slider or selecting the Scrapbook option under the Page pull-down menu. In
file mode most menu options are available for manipulating data, subject
to the limitations imposed by the format you are viewing.

The toolbox (see Figure 4☼) is used to choose a selection tool or change
the current viewing mode. Selection tools are displayed on the left side
of the toolbox and mode tools on the right. Four selection tools are
supported: block, for the selection of rectangular areas from the viewing
area; lasso, for the selection of arbitrary shapes; scroll, for rapidly
scrolling the viewing area; and camera, for the capture of rectangular
display regions to the clipboard.

In any of the three viewing options in file mode, only two different mode
tools are available. Clicking on the desired mode in the toolbox changes
the viewing mode. Figure 5☼ shows Scrapbook+ in page only viewing mode,
Figure 6☼ shows Scrapbook+ in page & thumbnails viewing mode, and Figure 7☼
shows Scrapbook+ in page & thumbnails viewing mode.


Using Scrapbook+

Scrapbook+ was intended to be used in the same way as any other Windows
application. When you start the program it automatically defaults to using
the SCRAP.ART file. You can create a new file at any time by selecting the
New command from the File pull-down menu. You can specify the maximum
pages for the file you create and the desired thumbnail size with the
resulting dialog box (see Figure 8☼). Being able to choose the size of each
thumbnail is an important feature as Windows supports a wide variety of
display subsystems.

Once a file has been created you can use four standard editing functions:
Cut, Copy, Paste, and Erase. When used in file mode they apply to the
currently selected page, including all associated renderings.

To provide more flexibility, we extended these standard editing
functions through Special Edit commands. The Special Edit menu options are
normally hidden and can only be accessed when the Edit menu is selected
with the Shift key depressed. The standard Cut, Copy, Paste and Erase
options then change to Cut Special, Copy Special, Paste Special and Erase
Special. When you select one of these a dialog box is displayed (see
Figure 9☼) listing the available formats, allowing you to specify which to
use in the operation.

The AutoPaste feature is related to the standard editing functions. When
activated all clipboard changes are automatically pasted into the
current Scrapbook+ file. This valuable feature can provide a log of all
clipboard changes or simply act as a mechanism with which a series of
editing commands can be recorded, for example, separating large
"sheets" of raster artwork into individual bitmaps.

Another feature often used in conjunction with AutoPaste is the
ScreenCapture function. Using ScreenCapture lets you copy the client area,
a window, the entire window, or even the display to the clipboard in
bitmap form. It's activated by the Alt-PrtSc key combination, and when
you use it with AutoPaste you can easily capture screen images, including
pull-down menus, even while Scrapbook+ is iconic (see Figure 10☼).

An alternative method for capturing screen images is the camera tool. It
lets you select a rectangular region of the screen and copy it to the
clipboard, from which it can be pasted into the current file. This is
particularly useful for developers and users alike as it permits you to
collect anything visible on the screen.

Besides the standard edit functions, the import and export capabilities
of Scrapbook+ enable you to transfer data between the current file or
clipboard and disk. While doing so you can perform a limited amount of
conversion between the standard clipboard formats and some of the more
popular file standards (see Figure 11).

One interesting item for Windows developers to note is the support of the
BMP file format. With Scrapbook+ you can maintain large libraries of
bitmap resources (greater in size than those supported by the icon editor)
and export selected ones to disk for inclusion in your application's
resource file.

One of the more useful functions of Scrapbook+ is Merge (see Figure
12☼), which allows you to combine all or portions of one Scrapbook+ file
into another. This is particularly helpful when building large files that
are comprised of many smaller ones.


Development Challenges

Developing a program like Scrapbook+ in a new and challenging
environment such as Windows meant encountering many interesting and
difficult problems. One of the most arduous design challenges was to
provide rapid access to a large number of pages without using inordinate
amounts of memory. Each Scrapbook+ file consists of a fixed size header,
followed by a variable size index with one entry per page, followed by the
data for the pages themselves. To facilitate quick access to the page we
kept both the header and index in memory. This consumes less than 8Kb of
memory for the default 500-page file.

The ability to peruse a file quickly in a graphical manner was another
important design consideration for Scrapbook+, which led to the concept of
thumbnails. First, we decided that thumbnails should be created and
written to disk when a page was inserted, not "on the fly." We wanted to
display thumbnails quickly, yet conserve system memory. Our solution to
this problem was to use discardable bitmaps for thumbnails. Windows is
then free to discard them as requests for memory are made, allowing for
fast thumbnail scrolling, particularly when scrolling back to
thumbnails that have been read in and not yet discarded.


System Interdependencies

One great benefit of the Windows environment is its device independence.
Although software designed for Windows will run without modification on a
variety of displays and under various memory configurations, including
systems equipped with expanded memory, the software developer is not
completely off the hook.

Scrapbook+ had to be tested and adjusted for the most common display and
system configurations to ensure that each resource had the proper
characteristics and that the memory management techniques worked
correctly. Also, comprehensive error handling had to be built in to
handle situations where the display adapter in use was incompatible with
the data maintained by Scrapbook+.

Development of Scrapbook+ began under Microsoft(R) Windows Version 1.03;
later, we switched to Windows 2.03. The change from a tiled windowing
system to a pop-up windowing system required several modifications to our
design. Also, the formal definition of keyboard hooks and the addition
of expanded memory support forced us to make significant changes to the
implementation of the screen capture function. This included the
development of a dynamically linked library for our screen capture
keyboard hook function in order to prevent it from being paged out under
expanded memory.


Diversity of Formats

Our original design goal for Scrapbook+ was to handle any kind of
clipboard data. In practice, this was very difficult, and in several
cases simply impossible. Some clipboard formats, such as owner display,
and several proprietary formats just had no meaning outside the context
of the program that copied them to the clipboard. Other formats such as
text, bitmaps, and metafiles formed the basis of clipboard data exchange
in Windows, and there was never any doubt about their usefulness.

Early in the design phase of Scrapbook+ we committed ourselves to
supporting Encapsulated PostScript(R) (EPS). Since we were the first to
handle this format we had to define a standard protocol for the exchange
of EPS images via the clipboard. For those unfamiliar with this format,
EPS is a combination of PostScript text with an optional display
representation of the image, which can be a metafile, Tagged Image File
Format (TIFF), or even a Macintosh(R) PICT file.

Our commitment to EPS forced us to support TIFF, something which we
privately hoped to avoid as it could have significantly delayed the
project. However, despite the complexity of this format, supporting EPS
and TIFF turned out to be a major selling point of the product, something
we hadn't really anticipated.


Interaction with Apps

One phase of development that ended up taking more time than we thought
was the interaction between Scrapbook+ and other Windows programs. Many
of the subtleties and complexities of Windows become apparent in this
area. We learned much about the correct use of the clipboard by making
sure that Scrapbook+ worked well with a variety of other applications,
including Microsoft Excel, Aldus PageMaker, and Micrografx Designer(R).

We felt that users will more and more demand that applications not only
work well by themselves, but that they work well together. Because of
this we spent a lot of time understanding the interaction between
Scrapbook+ and other major Windows applications, paying special attention
to clipboard interaction and memory use.

In the end, and not unexpectedly, Scrapbook+ performed considerably
better when we kept each code segment including _TEXT under 8Kb in size
and minimized the size of our data segment.


Performance Optimization

Because we envisioned Scrapbook+ as a system utility that would be
present on the desktop at almost all times, we made an effort to
characterize its performance and improve it in certain critical areas.

We used both static and dynamic modeling approaches with several
proprietary code analysis techniques to arrive at a good mathematical
model of the application. This model was optimized, and resources were
focused on the crucial areas. The result was a considerably improved
application with well-balanced segments. In certain situations the time
required for particular operations was reduced by a factor of five.


Development Tool

While working on Scrapbook+ we employed it extensively as a development
tool for managing bitmap, cursor, and icon resources. Each incarnation
of these resources was saved in a single file for review and possible
retrieval. With the numbnail viewing mode we could rapidly compare recent
designs with those created earlier in the development cycle. Figure 13☼
shows the handling resources of Scrapbook+.

In addition to serving as a development tool, we used Scrapbook+ in many
cases to test complicated clipboard interactions with itself or those
supported supported by other applications. Furthermore, it was an
invaluable tool when documenting the program. Using it we created a
single file, approximately 1.5Mb in size, that contained every
illustration, picture, screen image, and icon in the user's manual.


Putting It in Perspective

Now that the first version of Scrapbook+ is complete, some reflections
on the entire process are in order. First of all, we are now even more
acutely aware of the challenges provided by Windows software development.
Although in part due to the complexity of the environment, the standards
for software have reached the point where they have almost outstripped our
ability to meet them.

We remained firmly convinced that the monolithic approach to software
development is a relic of a bygone era and that applications of the future
will consist of small, well-integrated bundles combined into a functional
desktop by the user.

We also learned how much we were inclined to underestimate the quality
assurance phase of Windows software development. The cooperative, shared
resource, and multiprocessing nature of Windows makes applications very
difficult to test, transforming simple errors into complicated interactions
that can take weeks to resolve.

Finally, we learned firsthand how insidious the trap of "creeping
features" can be. It is so simple to say, "Let's just add this one extra
feature──it will only take a little more time." The problem is that each of
these features exacts its toll, and the delivery date of the product slips
further into the future.

All things considered, Scrapbook+ turned out to be a much better program
than we had anticipated. On many occasions we found ourselves asking,
"How did we ever get along without it?" Would we do it again? Yes, in an
instant, just as soon as we find that one great idea.


Figure 11:  Import/Export Formats

The following table lists the tools available with supprted import and
export file types for each clipboard format:

╓┌────────────┌────────┌─────────┌─────────┌───────────┌─────────────────────╖
Format       Block    Lasso     Scroll    Import      Export

Text                            yes       txt         txt

Rich text                       yes       rtf         rtf

Bitmap       yes      yes       yes       msp, bmp    msp, bmp

Printer      yes      yes       yes                   msp, bmp
Format       Block    Lasso     Scroll    Import      Export
Printer      yes      yes       yes                   msp, bmp
Bitmap

Picture                         yes       wmf         wmf

Printer      yes      wmf
Picture

CSV                             yes       csv         csv

SYLK                            yes       syk         syk

DIF                             yes       dif         dif

TIFF                            yes       tif         tif

PostScript                      yes       eps, ps     eps,ps


The following directory lists some of the common sources of the file types
supported by Scrapbook+

txt       almost any word processor
rtf       many word processors
msp       Windows Paint
bmp       Windows Bitmap Editor
wmf       some drawing programs
csv       most spreadsheet and database applications
slk       Microsoft Excel, Multiplan
dif       most spreadsheet and database applications
tif       Scanning Gallery, other applications
eps       Adobe Illustrator
ps        Adobe Illustrator