PCjs Machines

Home of the original IBM PC emulator for browsers.

Logo

Microsoft Systems Journal (Vol. 2)

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

Microsoft Systems Journal Volume 2

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

Vol. 2 No. 1 Table of Contents

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

IRMA: A 3278 Terminal Emulator for Micro-to-Mainframe Communication

The development team at Digital Communications Associates (DCA) took the
IBM(R) PC and grafted an EBCDIC-speaking synchronous coaxial cable
system into it. The result, IRMA(R), is a precedent-setting
PC-to-mainframe link reflecting innovative systems design and
hardware/software engineering.


Upgrading Applications for Multi-user Environments

Software developers who hesitated to write multi-user versions of their
products have a clearer upgrade path to follow with the appearance of IBM's
PC Network and software modules such as MS-DOS(R) 3.x, MS-Net, and the
NetBIOS. We look at rewriting applications for multi-user environments.


Expanded Memory: Writing Programs That Break the 640K Barrier

Once, 640K of memory seemed like more than enough, but as programs written
for DOS grew larger, the 640K limit was just too small. The LIM Expaneded
Memory Specifications (EMS) defines a standard that allows developers
to access expanded memory beyond 640K.


Keep Track of Your Windows Memory with FREEMEM

Not every Microsoft(R) Windows program is long and complex. This short
(fewer than 100 lines of code), simple program not only displays the amount
of available memory in an icon at the bottom of the screen and updates it
every second, it also reveals a good deal about programming in Windows.


A Guide to Debugging with CodeView

The CodeView(TM) debugger included with the Microsoft(R) C Compiler,
Version 4.00, provides flexible commands and a higher degree of integration
than previous debuggers. This behind-the-scenes view of CodeView's
development focuses on the design of the debugger and its use in a
typical situation.


Page Description Languages: High-Level Languages for Printer Independence

Page description languages (PDLs) are becoming popular as a precise
and formalized means of controlling printer output. We take a look at
the three major languages that appear to be serious market
contenders──Postscript(TM), Interpress(TM) and Document Description
Language (DDL).


DIAL 2.0 Provides Software Developers With Integrated Support System

As part of its commitment to upgrade technical support to developers and
programmers, Microsoft offers a new, improved version of Direct Information
Access Line (DIAL) service, which includes access to bulletin boards,
direct technical assistance, and interactive develper forums.


Rich Text Format Standard Makes Transferring Text Easier

Microsoft is proposing the Rich Text Format (RTF) as a standard for moving
formatted text between applications. RTF allows the transfer of text from
one application to another without losing the formatting and without
worrying about translating text to each application's unique file format.


Ask Dr. Bob


Carl's Toolbox


EDITOR'S NOTE

We are pleased to present what might reasonably be called the first "real"
issue of Microsoft Systems Journal. This issue is twice as large as previous
issues and contains a good deal more of the technical support material that
we had planned from the beginning. We believe that MSJ is already on its way
to becoming an important source of information and support for the
development community, but in order to achieve this goal we need your
feedback. We hope that you will write us at the address below, or via MCI
Mail (MSJ), with your comments, questions, criticisms, or complaints.

One of MSJ's prime objectives is to promote excellence and innovation in
software design and development. Therefore, we are especially proud to
present a programmer-oriented article on IRMA, the revolutionary board from
Digital Communications Associates (DCA) that enables an IBM PC to emulate
IBM 3278 or 3279 terminals, allowing communication between mainframes and
PCs. IRMA, first released in 1983, took the industry by storm: DCA has sold
over 350,000 boards. Though considered a hardware product, IRMA's success is
really due to its supporting software and firmware, representing a triumph
of engineering and design.

Standardization and communication between different systems is a hot topic
these days, and many developers are looking for data exchange standards to
help ease the disparity among different systems. This issue of MSJ has an
article on the Rich Text Format, a standard proposed by Microsoft to encode
formatted text. To fully illustrate RTF's utility, we include details of the
specs used by three page description language (PDL) vendors to create a
universally formatted text design.

As a quick read of the abstracts on the cover suggests, Microsoft Systems
Journal wants to explore any development or technology that is of interest
to the community of professional developers. We plan to cover many diverse
topics in forthcoming issues. If there are any specific topics that you
think we should include, please write to us.──Ed.


Masthead

JONATHAN D. LAZARUS
Editor and Publisher

EDITORIAL

BARRY OWEN
Managing Editor

CHRISTINA G. DYAR
Associate Editor

GERALD CARNEY
Staff Editor

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

BETSY KAUFER
Office Manager

Copyright(C) 1987 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 liabilty for any damages resulting from the
use of the information contained herein.

Microsoft, the Microsoft logo, MS-DOS and XENIX are registered trademarks
and CodeView is a trademark of the Microsoft Corporation. IBM is a
registered trademark of International Business Machines Corporation.
PageMaker is a registered trademark of Aldus Corporation. PostScript
is a registered trademark of Adobe Systems, Inc. dBase II is a registered
trademark of Ashton-Tate. Crosstalk is a registered trademark of Microstuf,
Inc. WordStar is a registered trademark of MicroPro International. Above is
a trademark of Intel Corporation. Advantage! is a trademark of AST
Research, Inc.

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

IRMA: A 3278 Terminal Emulator for Micro-to-Mainframe Communications

Frank J. Derfler, Jr., and Edward Halbert☼

Computer products, like plants, evolve in many different ways. Some new
species of plants mutate from a single root stock, but others are hybridized
by cross-pollinating different strains. One of the earliest attempts to
combine an old proven stock with the wild new personal computer resulted in
a product whose name is now synonymous with micro-to-mainframe
communications──IRMA.

The developers of the IRMA board took the PC as it was produced by IBM and
programmed by Microsoft──an ASCII device with only asynchronous
communications capability──and grafted an EBCDIC-speaking synchronous
coaxial cable system into it. While designing IRMA, they had to make several
decisions and solve many technical problems.


What IRMA Is

IRMA (the name is not an acronym and was chosen because it is memorable) is
a circuit board that can be installed in one of the slots in the IBM(R) PC.
The card has a BNC connector and is attached to the coaxial cable coming
from a communications controller in front of an IBM mainframe computer.
IRMA's software allows the PC to emulate IBM 3278 or 3279 terminals and to
transfer files between a mainframe and a PC.

IRMA is truly integrated into the PC's hardware and operating system. It is
a standalone coprocessor system hosted by the PC. If you place an IRMA card
on a table and connect 5 volts and the coaxial cable to it, the IBM 3274
communications controller talks to it just like any other terminal.


IRMA's Lineage

IRMA is now marketed by Digital Communications Associates, Inc. DCA markets
products for microcomputer communications (the company recently acquired
Microstuf and its Crosstalk communications package) and large-scale network
communications. The full DCA product line includes network processors,
statistical multiplexers, high-speed time-division multiplexers, protocol
converters, X.25 PADs, integrated software products for network management
and control, and modems.

DCA acquired IRMA in 1983 as a result of a merger with Technical Analysis
Corp. (TAC). TAC started engineering design on IRMA in 1982 and released the
product in 1983.

The man behind IRMA is Andrew Miyakawa. He joined TAC as a programmer in
1972; in 1978, he became TAC's director of hardware engineering. Miyakawa
designed the first IRMA printed circuit card. In fact, if you have an old
IRMA board you can see his signature in the corner, which was later thought
to be a scratch on the photo negative and removed.

The engineering staff at TAC did a lot of custom work with the IBM coax A
and coax B interfaces to develop the Agile printer interface for IBM
systems. The coax A interface uses polling for terminal communications. The
Agile interface uses a microprocessor to control communications between the
polled coax system and the dumb printer. There is an obvious parallel
between this system and the PC, and the engineering team's experience with
this communications interface was important in the development of IRMA.

The version of the IBM PC that was available in August 1982, when the IRMA
team started work, was a limited machine with even more limited development
tools. The PC's eight interrupts and four DMA channels made it difficult to
enhance the machine──even if most of the interrupts had not already been
assigned. The PC's character set was, and still is, small, and only the most
primitive assembler was available. Finally, the 8088 just isnot up to the
job of responding to the 3270 communications scheme. The coax A polling
system was designed to interact with terminals using hard-wired logic and
allows only 5.5 microseconds for a terminal to respond. The interrupt
latencies on an 8088 don't allow it to recover quickly enough to respond to
the communications channel.

IRMA's designers forged ahead to make the interface work, almost in spite of
IBM's product. They decided to add a coprocessor to the PC that is
considerably faster than the 8088. Having decided upon the design approach,
the IRMA development team divided the project into four major areas:
coprocessor hardware, microcode for the hardware, terminal emulation
software, and the user interface.

The IRMA team spent most of its development time on implementation of the
interface, rather than on design. TAC received no significant help from IBM
other than the standard documents available to the general public. IBM's
Technical Reference manual contained much information about the user
interfaces, such as the monitor, the keyboard, and memory. This information
was crucial to the development of the IRMA board.


The Hardware

The IRMA board consists of four major components. The Signetics 8X305,
IRMA's processor, is the large chip on the left side of the board. This
microprocessor has the power to handle the 3270 protocol and the associated
polling, data transfer, and handshaking. Even though the chip runs at 4
MIPS, it can only execute roughly 22 instructions in the 5.5 milliseconds
allowed for generating a response to the 3274 communications controller.

The IRMA board functions as an intelligent buffer processor and interface
between the coaxial cable information flow and the IBM PC. Signals from the
coax travel through the DP8340 and DP8341 3270 Coax Transmitter/Receiver
Interface, which occurs in the two large chips along the top of the card.
The serialization and deserialization of data takes place in these two
chips. The chips also provide the interface between the coaxial cable and
the microprocessor.

The independence of the IRMA processor in the PC is made possible by the
Decision Support Interface (DSI). DSI is implemented in the microcode of
IRMA's processor and support program. DSI allows the 8X305 system to meet
the requirements of the 3270 protocol by managing streams of buffered data
and handling all of the timing. The technique that moves the data between
the processors is called a mailbox structure (see Figure 1).

The mailbox is a 4-byte, dual-ported register array (4 74LS670) located in
addresses 220H to 223H. The dual port array is the bridge between the 8088
and the 8X305 processors. Flags are used to detect idle states within the
DSI structure; an idle state lasts only 5.5 microseconds between messages
from the controller. When DSI isn't doing anything, it looks to this array
for any commands that need processing. The command words sent by the IRMA
program read or write bytes of data in the screen buffer, process
keystrokes, and call other special features.

Data is transmitted between the two processors through the use of four
address locations (220H─223H). The processor traffic is controlled by the
Command Request flag located at a higher address (226H). This flag is
cleared by either processor when information in the array has been
successfully read. There is also an Attention Request flag (227H). The
commands that are passed through these four registers are parameter driven.
The command is placed in the base address, or word 0, at 220H, and up to
three arguments can be specified at 221H─223H, depending on the command.
Before issuing any command on the array, the Command Request flag is set to
high. The command is then placed in the register(s). After it has been
picked up by the receiving processor, the request flag is cleared. Address
locations 224H and 225H are reserved for future use.

 There are sixteen commands supported by DSI:

  Code          Command Definition

    0           Read buffer data
    1           Write buffer data
    2           Read status/cursor position
    3           Clear main status bits
    4           Send keystroke
    5           Light pen transmit
    6           Execute Power-on Reset
    7           Load trigger data and mask
    8           Load trigger address
    9           Load attention mask
   10           Set terminal type
   11           Enable auxiliary relay
   12           Read  terminal   information
   13           No-op
   14           Return revision ID and OEM number
   15           Reserved; Do not use The Video Interface

Even with its standalone power, the IRMA coprocessor board needs the PC as a
keyboard input and display device. However, the PC lacks many of the
features and the flexibility found in 3270-series terminals, so the
designers had to work around these limitations. Special techniques are used
to move video, characters and colors are substituted on the screen, and
keyboard emulation alternatives are provided.

The use of an on-board screen buffer is very important for the IRMA/PC video
interface. Data on the IRMA's video buffer is always active and being
updated regardless of whether or not the user is looking at it. Only when
the user switches to the emulation mode is the data moved into the PC's
video buffer.

The screen buffer is controlled by DSI and occupies a total of 8K of fast
RAM on the IRMA board. This RAM resides in the four large chips located
along the bottom edge of the card. The IRMA board supports displays of up to
132 columns by 43 lines, such as those used by some 3278 models. This
requirement drove a RAM with a size of 3K for the characters and another 3K
for the attributes. This is because each character needs 16 bits in memory:
8 bits for the ASCII code and another 8 bits for the attributes. The
remaining 2K are used by the 8X305 for local storage.

The attribute characters that are supported by IRMA include protected/
nonprotected, numeric/alpha/both, light pen detect, tab stops,
intensified/normal, nondisplay type fields, and modified data tag.

The Extended Attribute Characters control character type (normal, blinking,
reverse video, underlined), character color, and character set.

Using the 8X305 processor, DSI converts the keystrokes and attribute
characters into 3270 functions and emulates those same functions on the PC.
The field attributes are mapped to the colors on the PC, corresponding
approximately to those on the 3279 terminal. If an IRMA board is in a PC
with a color display, each color represents a different type of field. The
attribute characters on the PC translate to the field types in Figure 2☼.

In order for a field to be unprotected, numeric data only, highlighted, and
detectable, the program would have to send a 11011000B, D8H to the screen
buffer. This field attribute continues to be in effect until another field
type is specified.

IRMA replicates some of the quirks of 3270 functions such as "attribute
wrap," which occurs when the programmer fails to end a field. The attribute
for that field not only affects screen locations after it, but also wraps
around and affects locations before it on the screen. Programmers have to be
just as careful with IRMA as they are with real 3278/9 terminals.

In the 3278/9 terminals, a specific bit within the attribute byte, known as
the Modified Data Tag (MDT), is changed whenever a field is modified. If the
byte had been sent out as D8H and read back as D9H, the program would be
signaled that the operator has modified the field. The IRMA software must
interpret the position of the data on the screen of the PC and then convert
it to the appropriate coordinates for the 3278/9 fields. The Extended
Attribute Bytes (EAB) are defined in Figure 3☼.

The IRMA screen-handling software has features designed to allow developers
to see screen attributes that might not otherwise be displayed. A single
keystroke shows the attributes of a field. Unprotected fields can be
temporarily filled to view all of their locations.

Attributes are normally displayed as blanks and take up a byte on the screen
just as attributes on the 3278/9 terminals do. Addressing for text in the
IRMA board starts at 50H and increases in increments of 80 for each line up
to 780H on line 24 during emulation of MOD 2 terminals. Emulation of MOD 3
terminals requires 32 lines and ends at A00H. Emulation for MOD 4 terminals
must support 43 lines, and the last line starts at D70H. The storage for the
status line in the emulation of all models starts at 0H in lower memory, but
the line is displayed at the bottom of the screen.


User Interface

There were some significant problems in the development of the user
interface for the IRMA board. More than half of the problems involved
keyboard operation. The major problem was figuring out how to emulate
special 3270 series terminal keys on the PC's limited keyboard. The PC's
keyboard has fewer keys to work with, and the PC's BIOS does not allow all
combinations of keys on the keyboard to generate a scan code. This problem
was eventually solved by including software that takes control of and
reprograms the entire keyboard.

The 3270 terminals have 24 PF keys and more than 30 other keys that the PC
must satisfy. At first, the developers felt it would be best to keep the
positions of the special function keys to be emulated on the PC as close to
their positions on a 3278 as possible. Most of the 3270's PF keys were
originally mapped to the numeric keypad on the right side of the PC's
keyboard. This was later changed because of conflicts with the use of the
numeric keypad. The PF keys 1─12 are mapped to Alt-1 through Alt-=, and PF
keys 13─24 are mapped to Ctrl-1 through Ctrl-=.

Either IRMA's developers had great foresight, or they have set a standard
for PF keys, because most current PC-to-mainframe applications now map
PF13-PF24 to the PC's control key sequence. Most of the terminal control
keys on the left side of the 3278 keyboard have been faithfully duplicated
on the PC's keyboard for IRMA. In all cases, the normal functions of the
PC's keyboard are unmodified, and the 3270 emulated functions are reached
through the Alt and Ctrl keys.

The only keys that were located far from their normal positions on the 3278
keyboard are the Dup, Field Mark, PA1, and PA2 keys, which are mapped to
Ctrl-G, Ctrl-H, Ctrl-J, and Ctrl-K, respectively. Other special functions
not offered by a 3278 terminal are the Show Attribute and Display All
Unprotected Fields functions. The designers left these in for the benefit of
users and developers.

Another problem faced by the IRMA design team was screen display emulation.
The 3278 uses icons for certain actions and messages. These icons cannot be
recreated with the standard IBM ASCII character set. The designers
considered drawing these icons with the IBM PC graphic display system, but a
little experimentation showed that the system would be too slow for good
emulation. Also, using graphic characters would limit users to graphic
terminals. The compromise was to use a combination of ASCII characters to
emulate the special characters available on the IBM terminal. An example of
some major substitutions are listed in Figure 4☼.

Through this selection of characters, the designers came up with a
reasonable facsimile of those used in 3278-type terminals. Screen emulation
was made easier by the fact that many operators of 3270-series terminals
recognize characters by where and when they come up as much as by what they
look like. Thus, almost anything appearing in the same general location on
the screen at the right time would be interpreted by the operator as the
same message. The alternative characters developed by the IRMA team have
become standard and are used by most other manufacturers of terminal
emulators, including IBM.

Color selection during emulation of the 3279 color terminal was also a
problem, because the IBM Color Graphics Adapter has a much smaller palette
than the 3279. Until just recently, the normal protected fields were dark
blue and hard to read. DCA provided a patch in the setup menu to substitute
cyan for blue.


Converging Limitations

The board was nearly complete when the IRMA team discovered a strange quirk
in the IBM 3274 controller. The 3274 would not send color or graphic
characters to the IRMA board. After an extensive investigation, the
designers discovered that the controller looks for a special color
convergence test feature found only on the 3279 terminals to determine if
the terminal at the distant end is really capable of using color. This color
convergence test allows the user to align the red, blue, and green guns by
using the cursor keys until they form a single white dot. This dot signifies
correctly aligned electron guns in the CRT. The presence of data in a
special convergence test video buffer tells the 3274 that the terminal is
aligned and ready to accept color graphics. Since the 3274 controller
couldn't get a color convergence test response from the IRMA board or the
PC, it would not transmit color graphics.

The design team had to program the IRMA board to fake the controller into
thinking that it had this test feature. The controller only checks for this
test at terminal power-up, so the test is faked at the board's
initialization. The test code is temporarily overlaid in undisplayed video
buffers.


Locked Out

Another problem cropped up in early 3276 controllers. The phase-locked loop
devices in the 3276 controllers were not consistent, and individual machines
had significant timing differences during data transmission. At first, IBM
field technicians consistently blamed problems on improper cable. Since
changing the length of the cable can adjust the timing of received data,
this fix often worked. The programming of the IRMA board was modified to
work around the 3276 problem by addressing the data separator at specific,
gated times. The bug was fixed on the newer models of the 3276, and
modifications were developed for older models.


User Relations

A major issue in the development of the user interface was user-developed
software. DCA chose not to copy-protect its software. DCA feels that copy
protection is just not worth the ill will that it brings.

DCA ships the source code for the terminal emulation and file transfer
programs with every IRMA board. DCA includes the source code for its
programs because everyone benefits if someone can improve the code. The user
manual includes programs written in BASIC so that even the novice programmer
can see how the board communicates. The BASIC programs were never really
intended for use in applications because they run very slowly under a BASIC
interpreter, but many people have tried them anyway.

The design team assumed that sophisticated users would provide their own
file transfer protocol or use a third-party protocol, but they found that
they had to supply a more complete system. The file transfer software
provided for the mainframe link has been updated from the slow FT78X to the
FTCMS and FTTSO programs. The old FT78X used the VM/CMS Xeditor and just
copied files byte by byte from disk through a fake keyboard buffer to the
file on the host. The new program can send large blocks of data and is much
faster. The new programs are written in PL/1 and require that the PL/1
transient library installed on the host.

The IRMA package also includes an extensive technical reference manual with
all the functions and commands documented. Miyakawa speaks for all MS-DOS
developers when he expresses his wish that someone had defined MS-DOS user
interfaces or that some of the BIOS responsibility was taken off the
developer.

DCA has gained much from such large users of the IRMA board as Texaco, Coca
Cola, and Blue Cross and Blue Shield. Miyakawa feels that user feedback is
extremely valuable and provides the best information for user interface
development.


IRMA's Future

A new program called IRMAX has been developed to enable the IRMA card to run
as what IBM calls a Distributive Function Terminal. This allows an IBM PC to
emulate the 3270 PC, running multiple sessions on a single coax connection.
This configuration is more desirable because the 3270 PC has had
compatibility problems running PC software. IRMAX will maintain
compatibility while allowing the full capabilities of the Distributive
Function Terminal.

IRMAX functions by removing most of the work from the 3274 controller and
placing it on the IRMA card. The card carries the SNA, a 3270 display
support program, and a terminal control program similar to the one in the
3270 PC.

DCA's commitment to communications will carry it into the LAN market with
IRMA LAN, which will operate with any manufacturer's version of the network
basic input/output software (NetBIOS) using the function calls originally
developed by IBM and Sytek.

IRMA LAN is configured with an IRMA card in a network server, acting as what
is functionally termed a network gateway. A modified version of the IRMA
software runs on a PC operating as a network workstation and communicates to
the IRMA card through the NetBIOS. In an efficient high-speed network, the
person using the workstation can't tell that the IRMA card is located in a
remote server.

In some cases (IBM, Sytek, 3COM, and other companies), a "real" NetBIOS lies
between the networking software and DOS. Other companies, such as Novell and
Banyan, emulate NetBIOS as part of a complete operating system. In either
case, the IRMA LAN software relies on NetBIOS to carry data messages to the
gateway server, where they are addressed to the IRMA card.

In the LAN environment, the IRMA software generates a stream of code called
a network control block (NCB), which activates NetBIOS and passes data
through a set of buffers established by NetBIOS and controlled by the NCB.
The software generates an interrupt 5CH that calls NetBIOS. Other fields of
the NCB point to a message buffer address and tell NetBIOS where to send the
information in the buffer. This is a standard implementation of the
capabilities provided by the NetBIOS communications interface.


IRMA Showed How

IRMA is an excellent example of the integration of an advanced
communications capability into the somewhat limited PC system. The
development team was innovative and employed sound engineering techniques
to develop a precedent-setting link between PCs and mainframe computer
systems.


Figure 1:  Mailbox Structure

                                                     ┌──COAXIAL─CABLE───
┌────────────────────────────────────────────────────│──(TO 327X CONTROLLER)
│                                           ┌────────┴────┐       │
│                                           │             │       │
│                      ╔═══════════╗  ╔═════╧═════╗  ╔════╧══════╗│
│                      ║  8K RAM   ║  ║   COAX    ║  ║   COAX    ║│
│╔══════════════╗      ║  BUFFER   ║  ║TRANSMITTER║  ║ RECEIVER  ║│
│║              ║      ║  MEMORY   ║  ║  DP8340   ║  ║  DP8340   ║│
│║              ║      ╚══╤═════╤══╝  ╚══╤═════╤══╝  ╚══╤═════╤══╝│
│║              ║         │     │        │     │        │     │   │
│║              ╟─────────┘     └────────┘     └────────┘     │   │
│║MICROPROCESSOR║                                             │   │
│║    8X305     ║                                             │   │
│║              ╟───────────────────────┐     ┌───────────────┘   │
│║              ║                       │     │                   │
│║              ║                       │     │                   │
│║              ║                       │     │                   │
│╚══════════════╝           ╔══════╤════╧══╤══╧═══╤══════╗        │
│                           ║      │       │      │      ║        │
│                           ║    DUAL PORTED REGISTER    ║        │
│                           ║      │       │      │      ║        │
│                           ╚══════╧════╤══╧══╤═══╧══════╝  IRMA  │
└───────────────────────────────────────│─────│───────────────────┘
                                      SYSTEM UNIT
                                        │ BUS │

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

Upgrading Applications For Multi-user Environments

───────────────────────────────────────────────────────────────────────────
Also see the related article:
  MS-Net Lock (Function 5CH, Code 00H)
───────────────────────────────────────────────────────────────────────────

Robert Cowart☼

With the appearance of IBM's PC Network and the accompanying software
modules (MS-DOS 3.x; the Network Program Microsoft-Networks, popularly known
as MS-Net; and the NetBIOS) as at least semi-stable industry performers,
software developers who were previously hesitant to release multi-user
versions of their products now have a clearer upgrade path to follow.

This development raises several questions: What's involved in rewriting
existing applications for use with MS-DOS 3.x and these new standards, or
for those many products that adhere to them? Is it worth the effort? How
long might it take? Do you have to rewrite your entire program? And most
importantly, if you do end up deciding to go multi-user, how should the
logistics of implementation be handled?

Obviously, the first point to consider is whether your application stands to
gain from adaptation to a multi-user environment. There are certainly
applications that don't lend themselves to multi-user operations. Word
processors are a case in point; it's hardly advantageous or even advisable
for more than a single user to be editing a given document. But for such
applications as database management, accounting systems, manufacturing
control, or even certain spreadsheet packages, upgrading your product to
multi-user capability may serve to increase sales and fill a need among
users, particularly as the market's interest in networking swells. IBM's
clear commitment to the fully integrated and networked office environment of
the future reinforces this trend.


Single-user Upgrades

In making this decision, you should bear in mind the distinction between
single-user and multi-user modes of operation on the same PC network.
Running an existing program in single-user mode allows one or more people to
load the program into their own computers and run it, thus creating their
own independent data files. So-called multi-user operation allows users to
share those same data files. The multi-user program must, in this case,
offer sufficiently intelligent data management to prevent users from
accidentally clobbering each other's data.

Most existing single-user applications run without concern for these matters
if they are in a single-user mode under the current network operating
systems. More often than not, few, if any, adjustments are necessary to get
such an application running reasonably well under MS-Net. At the most,
certain considerations pertaining to file access modes must be taken into
account to prevent data corruption. This boils down to deciding what
status──the most frequently used settings are read/write or read-only──each
file used by a program should be given when a user is running the program on
a network.

According to Gary Stroud, PC systems programmer for Berkeley, Calif.-based
Centram Systems West, makers of the TOPS network,

"The first thing to do when adapting a single-user application on a network
is to set your primary program file and all its overlay files to read-only
mode. This way, any number of people can run the program, and nobody can
accidentally erase one of the necessary program files while somebody else is
in the middle of using them. That can cause a catastrophe. Microsoft Word
sets its files to read-only automatically, for example. Next, it's
imperative that, if your application uses temporary scratch files when users
are creating documents, you ensure that the names for these files are always
unique. Otherwise MS-DOS will confuse, say, my temporary file and your
temporary file, and we'd have a mess. Starting with MS-DOS 3.0, there is a
DOS call for obtaining a unique filename. Also, there is a new DOS file
create call that will fail if a file of the same name already exists. You
should use these facilities."

If your current program uses the old-fashioned FCB (file control block)
method of opening and closing files, you should convert your program to the
newer (MS-DOS 2.0 or later) calls for file handling. Otherwise, files may
not be closed properly, which would prevent other people from accessing
them. This was typical of programs ported over from CP/M or written for MS-
DOS 1.x. For WordStar(R) and dBASE II(R), for example, the use of FCB was
the only way to open and close files.


Multi-user Upgrades

More-serious philosophical and logistical considerations come into play when
the developer intends to provide real simultaneous multi-user access to data
files on the LAN. A deeper understanding of some technical aspects of data
sharing and real-world LAN usage is necessary. Although MS-DOS 3.1, with its
extended function calls that are available through Int 21, provides all the
control necessary for most typical LAN-based data-sharing scenarios, using
the calls properly and effectively takes some planning and some reading of
the MS-DOS 3.x Technical Reference manual.

The developer should determine how many users would typically be sharing
data at one time. How many files would actually be involved? Where might
bottlenecks crop up in the system? Every command or function of the
single-user version of the program must be closely scrutinized to determine
the demands that the multi-user environment will introduce.

To assist in the upward migration process and encourage developers to write
for MS-Net, Microsoft offers an Independent Software Vendor (ISV) kit for
$50. The kit includes a variety of published material about MS-Net, along
with the programmer's reference manual (normally $50) and a selection of
applications notes.


Microrim's R:base(R)

As a case in point, I've investigated the migration of Microrim's R:base
from single-user PC-DOS 2.x to the multi-user MS-Net environment. (Figure 1
shows R:base on a multi-user network.) Since Microrim essentially used the
same network interface module for its System V, the strategies apply to that
product as well.

As is typical of many database companies, Microrim postponed its multi-user
implementation until the Microsoft/IBM standard began to take hold and users
asked for a multi-user, "record-locking" version of R:base. At this point,
Microrim began the work, which, according to Collin Miller, Microrim's
director of product development, required about 8 person-months to complete.

The first task was to understand the distinction between true multi-user
environments and a PC-based LAN, and what impact that would have on product
design.

"A true multi-user environment employs some central intelligence to control
traffic flow through a database," says Miller. "The central control, which
would run in the server, would know which database and record each user was
using at any given time, on all nodes. On PCs, users are really only sharing
the same data, not the R:base program itself. R:base is running individually
in each user's PC."


More Intelligence?

Microrim considered writing a central module that would be RAM-resident in
the server to better arbitrate who was using files and records. "That would
have given us something more approaching the classical multi-user system,"
says Miller. "This way we could have established levels of priorities for
individual user access. We opted not to do that, just on philosophical
bases."

There were several reasons for this choice. First, writing a multi-user
system would have added extra levels of complexity for PC users. "PC users
don't want to get into the degree of complication necessary in really
sophisticated multi-user databases," says Miller. "We simply wanted
something that ran as easily as the existing R:base program. Second, this
meant the single-user version of R:base is essentially identical to the
multi-user version in both user interface and data file structure, making it
easier to maintain compatibility for users."

Also, Microrim felt that attempting to write a true multi-user system runs
counter to the IBM philosophy of PCs──that everybody has his own processor
and copy of the program running independently. IBM encourages developers to
keep things as station-independent as possible to prevent unnecessary
complications.


MS-DOS vs. NetBIOS

There are primarily two levels of compatibility to which a network
application can be written──MS-DOS 3.1 and the NetBIOS (Network Basic Input
Output System). The DOS level handles most file and record operations
adequately enough for most purposes and acts on a higher level than the
NetBIOS, which is the interface between MS-Net/MS-DOS and the particular
networking card.

"Someone like Microrim has a choice. They can write to DOS level calls, or
to the lower-level NetBIOS interface," says Dave Melin, product marketing
manager for networking products at Microsoft. "Both are standards and both
are popular, but the official recommendation from IBM and Microsoft, as is
always the case, is to write to the highest possible level in terms of
programming interfaces. If MS-DOS will do what you need to do, use it. If
you do need to go below it for some reason such as concurrency or
proprietary security, then go ahead, but most applications won't require
it."

It is a common belief that writing to the NetBIOS provides a performance
advantage. This probably comes from programmers' experience that
circumventing some other MS-DOS calls by writing directly to hardware does
increase speed. According to Melin,

"It is true that performance may be improved under the right conditions by
bypassing the DOS and writing to the NetBIOS directly. By writing to the
NetBIOS, programmers have more control over network session──related
parameters. Of course, they must deal with the added complexity of writing
to a low-level interface. Some people are writing software that doesn't
require any networking operating system, such as MS-Net, to be present.
Primarily to minimize memory requirements, they do not want to require the
presence of all that networking software, such as the redirector. They want
to provide all the services themselves within their application. In this
case, they will likely write directly to the NetBIOS."

However, there are hazards involved when writing directly to the NetBIOS,
particularly because contention management at the NetBIOS level is weak. At
this level, applications can bump into each other if the NetBIOS is not
handled very carefully. Considering that this is what DOS is really for, it
makes sense to employ it whenever possible. "If there is no reason to use
the NetBIOS, it's not worth it," says Melin. "Besides, the MS-DOS interface
is what stays stable between revisions, whereas lower-level things have a
history of changing, jeopardizing your compatibility over time."

In light of these facts, Microrim intentionally worked exclusively on the
DOS level in implementing the R:base upgrade. Also, it wanted to keep the
number of extended MS-DOS calls to a minimum. Fewer than five new calls were
used, according to Miller.


Avoiding Corruption

More troublesome to Microrim were concerns over its implementation of file
and record handling within R:base. Because of the peculiarities of R:base
files, Microrim faced some unique roadblocks when it was planning this angle
of its strategy. Unlike dBASE, for example, where database files are
essentially standalone entities, a series of R:base data files is controlled
by two more-complex files. The first one (file 1) contains the "schema" or
database structure, information about the size and layout of each table and
their relationships to one another in a multitable database. The actual data
of all related tables is stored in a second, larger file (file 2).

Regulating traffic flow through these two files posed some interesting
challenges for network implementation on MS-Net. For example, say two people
attempt to change the structure or information about the number of records
in a table in the database, that is, modifying file 1. Normally, the
approach would be to lock the file temporarily for use by the person
changing it. But this file could bring other users to a screaming halt
because access to any of the tables in file 2 requires access to file 1
first. "We wanted to have a bottleneck in file 1, since everyone has to read
it," says Miller. "We wanted to lock it as little as possible, so we had to
come up with some other solutions."

Microrim decided that four discrete levels of locking were necessary for the
amount of flexibility desired and for the type of command a user might be
issuing.

The first level is the Database lock. It essentially allows a single user to
have exclusive read/write access to selected files. This lock is necessary
because it allows for modification of file 1 and packing (eliminating
unwanted records from) a database. Assurance of single-user access is
mandatory for these two activities, since the database is often
significantly reorganized during these periods, and these processes may take
some time.

The next level is used for a very quick update to the schema (part of
file 1) and is called a Schema or combination lock. Microrim offers this
as an alternative to the complete lockout mentioned above. Examples of
situations in which this lock would be used include defining a new relation,
joining tables, or doing a database "add" or "subtract," that is, creating
a new database that is a subset of an existing one. At this level, file 1
is momentarily locked and then quickly released for use by others. The new
table(s) being built is/are the only thing(s) locked.

The third level is a Table level lock, allowing for a load or a browse/edit
on one table in a database. Loading allows a user to make a batch update to
a table; browse/edit allows a user to edit a screenful of records at one
time. In either case, other users are allowed read/write access to other
tables in the database, and may read, but not write to, the locked table.

Finally, Microrim incorporated a Record level lock, which it calls
concurrency control, for use in managing individual records under multi-user
table access. (See Figure 2.) Normally, this would be done by using MS-DOS's
range-locking function.

"MS-DOS 3.1 allows you to lock a record," says Microrim's Collins. "But if
it's locked, other users have no access to it for any reason, even for
reading. If you're editing a record in a table and go out to lunch, everyone
else is locked out of that record. Let's say someone is running a report on
outstanding orders, while someone else has a record locked in the same order
table. The report could be inaccurate because one record wasn't accessible.

"You could call this technique collision avoidance, to use networking
terminology, because it prevents the possibility of collisions. But we
wanted everyone to always be able to read the database, so we developed a
collision detection scheme."

Microrim's detection technique operates only if two or more users actually
do try to alter the same record simultaneously. The scheme keeps track of
where each user is in the database, allowing access to data records at any
time. Two people can even be reading or editing the same record at the same
time. Only for the split second that a user actually writes the change out
to disk is the record locked. If a change is actually made to the record by
one user, the second user is then notified that a change has been made since
his last read. At this point, the second user may want to reread it before
making his edit.

"Actually, the likelihood of two or more users editing the same record at
the same time is not that great when you only have five or six users on the
network," says Miller. "With a small number of users, collision detection is
fine. But when you try to support a larger number of users, collision
avoidance would be the better choice, because collision detection begins to
slow down exponentially with increases in retries."

Microrim's attitude is that collision avoidance comes with a high price tag,
because it prevents concurrent access to the database records. However,
Microrim admits that it's a scheme that works well, particularly with a
large number of users on the network. "By contrast," notes Miller, "our
[collision detection] scheme has more overhead during a collision. The
second person has to physically resolve the collision himself, through user
intervention."

Karl Schulmeisters, software design engineer for systems software at
Microsoft, says that the exclusive locking procedure Microrim has
circumnavigated to some degree, is a protective mechanism and intentionally
designed into MS-DOS to avoid loss of data integrity and to prevent
incomplete data from being transferred to different users. "There are pros
and cons to Microrim's solution," Schulmeisters notes. "You really only have
to lock a record for a brief moment during a read or write of a critical
area." (See Figure 3.)


Problems and Fixes

There are three other more or less generic problems that developers
have to consider addressing when upgrading their applications. The
first of these could be called queuing. If a number of users want
access to a particular resource, whether it is a printer, a file, or
a modem, some arbitration mechanism may be desirable to handle these
requests are on a first-come-first-served basis.

Related to the queuing question is the problem of waiting. How do you
control how long a process waits for an unavailable resource? When
encountering an error, such as a locked record or file, DOS performs error
retries only for a short period of time. But say you want to go to lunch and
let the machine retry a batch update until the resource is available.
Multi-user R:base allows the user, or a custom program, to retry for as long
as 4.5 hours, determined by an R:base command, SET WAIT.

Finally, there is the nefarious "deadly embrace." (See Figure 4.) This
impasse occurs when one user is waiting for resources, such as data tables,
that are already in exclusive use by the other user. It can result in an
endless tie-up, in which your program appears to be hung. Microrim solved
the deadly embrace problem by providing a command, SET LOCK, that the user
can issue. The command lets a user lock a number of tables in advance of
beginning a batch process that would involve all those tables. If the tables
are not all available, the transaction stops there and gives the user a
message to that effect.


Conclusion

There are many points to consider before embarking on any serious rewrite of
an application for LAN use, not all of which could be discussed here. I have
tried to bring up the most salient issues. Keep in mind that the planning
process is the hardest and most critical stage. You may want to begin by
analyzing how your program works in single-user mode. Then you should
consider what control mechanisms would be necessary to guarantee lack of
data corruption and minimize intrusions by users upon each other.

If your application includes a programming language, as R:base does, you
have to map out each of the commands in the language and analyze what and
when resources will have to be locked for each command. If your application
lacks a programming language, things should be that much easier. All this
planning may well take up the biggest part of your development time, but it
should pay off in the long run.

For broader system compatibility, try to stick to the MS-DOS 3 function
calls, unless you are writing a really esoteric application. Finally, the
MS-DOS 3.1 Technical Reference manual is complete enough to answer most
questions. According to Microrim's Collins, "We lived and died by the 3.1
Technical Reference. I don't think we ever had to phone up Microsoft."


Figure 1:  This sample network configuration shows several combinations
           typical of networks.

╔══════════════════ SERVER ╗ ┌────────────────────────────────────────┐
║           ┌────────────┐ ║ │  ┌──────────────────────────────┐      │
║           │ Database   │ ║ │  │   ╔══════════════════════════│══════│════╗
║         ┌─┤ directory  ├───│──┘   ║ ┌────┐ ┌─────────────┐ ┌─┴──┐ ┌─┴──┐ ║
║         │ │ c:\dbfiles ├───│──┐   ║ │ a: │ │    c:\      │ │ g: │ │ e: │ ║
║         │ │ (g:)       │ ║ │  │   ║ └────┘ │   ┌─┴────┐  │ └────┘ └────┘ ║
║         │ └────────────┘ ║ │  │   ║ Local  │ Other    │  │    └──┬──┘    ║
║         │ ┌────────────┐ ║ │  │   ║ Floppy │ directory│  │     Remote    ║
║         │ │ R:base     │ ║ │  │   ║ Drive  │          │  │     Drives    ║
║ ┌─────┐ │ │ System V   │ ║ │  │   ║        │  c:\rbfiles │               ║
║ │     │ ├─┤ directory  ├─────┐│   ║        └─────────────┘               ║
║ │     │ │ │ c:\rbfiles │ ║ │ ││   ║        Local Hard Drive              ║
║ │     │ │ │ (f:)       │ ║ │ ││   ╚═══════════════════════ WORKSTATION 2 ╝
║ │ C:\ ├─┤ └────────────┘ ║ │ │└───────────────────────────────────┐
║ │     │ │ ┌────────────┐ ║ │ │                                    │
║ │     │ │ │ User's     │ ║ │ └─────────────────────────────┐      │
║ │     │ │ │ private    │ ║ │ ┌──────────────────────┐      │      │
║ └─────┘ ├─┤ directory  ├───┘ │    ╔═════════════════│══════│══════│══════╗
║         │ │ c:\user2   │ ║   │    ║ ┌────┐ ┌────┐ ┌─┴──┐ ┌─┴──┐ ┌─┴──┐   ║
║         │ │ (e:)       │ ║   │    ║ │ a: │ │ b: │ │ e: │ │ f: │ │ g: │   ║
║         │ └────────────┘ ║   │    ║ └────┘ └────┘ └────┘ └────┘ └────┘   ║
║         │ ┌────────────┐ ║   │    ║    └──┬──┘       └──────┬─────┘      ║
║         │ │ User's     │ ║   │    ║  Local Floppy         Remote         ║
║         │ │ private    │ ║   │    ║     Drives            Drives         ║
║         └─┤ directory  ├─────┘    ╚═══════════════════════ WORKSTATION 1 ╝
║           │ c:\user1   │ ║
║           └────────────┘ ║
╚══════════════════════════╝


Figure 2:  Conflict in Updating. When multiple users are accessing a
           database, they must take care when updating to avoid conflicts.
           As shown above, it is not enough just to facilitate multiple
           access;interlocks must be provided to isolate the entire update
           process.

   ┌─────────────┐                                       ┌─────────────┐
   │   USER 1    │          VALUE IN DATABASE            │    USER 2   │
   └─────────────┘          ┌───────────────┐            └─────────────┘
         ┌──────────────────┤               │█
         ▼     Read Value   │     100       │█   Read Value
 ┌────────────────┐         │               │█─────────────────┐
 │ Type in change │█        │     100       │█                 │
 │ (subtract 20)  │█        │               │█                 ▼
 │  ┌──────────┐  │█        │     100       │█         ┌────────────────┐
 │  │ Process  │  │█        │  ┌─────────┐  │█         │ Type in change │█
 │  │ change   │  │█        │  │         │  │█         │ (subtract 10)  │█
 │  │ (100-20) │  │█───────►│  │   80    │  │█         │  ┌──────────┐  │█
 │  └──────────┘  │█        │  │         │  │█         │  │ Process  │  │█
 │                │█        │  └─────────┘  │█         │  │ change   │  │█
 │     Write      │█        │  ┌─────────┐  │█         │  │ (100-10) │  │█
 │     change     │█        │  │         │  │█         │  └──────────┘  │█
 └────────────────┘█        │  │   80    │  │█ ◄───────┤                │█
   █████████████████        │  │         │  │█         │     Write      │█
                            │  └─────────┘  │█         │     change     │█
                            └───────────────┘█         └────────────────┘█
                              ████████████████           █████████████████


Figure 3:  R:base Concurrency Control and Locking

╓┌───────────────┌──────────────────┌────────────────────────────────────────╖
Command         Control/Lock       Description

EDIT form       Concurrency        Allows concurrency control
ENTER form                         for other users and items
                                   unless a table lock is issued.

Command         Control/Lock       Description

APPEND          Concurrency and    Table lock interrupts
CHANGE          Table Lock         concurrency control while
DELETE ROWS                        data is modified; table lock
                                   may also exclude other table
                                   locks issued after it.

EDIT ALL        Table Lock         Table lock set if concurrency
ENTER form                         control is not in effect for
FROM filespec                      any part of the table. When
LOAD                               set, the lock excludes other
FORMS☼                             table locks issued after it.
REPORTS☼
SET LOCK ON
UNLOAD...
FOR tblname☼
VIEW☼

DEFINE          Database Lock      Locks all tables and allows no
EXPAND                             other locks to be set while it
Command         Control/Lock       Description
EXPAND                             other locks to be set while it
RELOAD                             is in effect. This lock will not
REDEFINE                           be set while a table lock is in
REMOVE                             effect.
REMOVE COLUMNUN
UNLOAD☼

BUILD           Table Lock         Used together for some
FORMS☼          Database Lock      operations. While the
INTERSECT                          database structure is
JOIN                               modified, a database lock is
PROJECT                            in effect. After the structure
RENAME                             is modified, the database
REPORTS☼                           lock is replaced by a table
SUBTRACT                           lock.
UNION
VIEW☼


Figure 4:  Deadlock can occur when two of more users who have access to some
           tables wait for access to others. Microrim solved this problem
           with the SET LOCK command that lets a user lock a number of
           tables before beginning a batch process.

                                                    User 2
                                                    cannot reach
                             ┌──────────────────┐   CUSTOMERS
              ┌──────────────┤   CUSTOMERS      │   because user 1
              │              │   ─────────      │   has locked it.
              │              │   User 1 locks   │
              │              │   CUSTOMERS      │  X  ◄─────────┐
              │              │   waits for      │               │
              │              │   TRANSACTIONS   │               │
              │              └──────────────────┘               │
              │                                                 │
              │                                                 │
              │              ┌──────────────────┐               │
              │              │   TRANSACTIONS   ├───────────────┘
              │              │   ────────────   │
              │              │   User 2 locks   │
              └────────►  X  │   TRANSACTIONS   │
                             │   waits for      │
             User 1          │   CUSTOMERS      │
             cannot reach    └──────────────────┘
             TRANSACTIONS
             because user 2
             has locked it.


───────────────────────────────────────────────────────────────────────────
MS-Net Lock (Function 5CH, Code 00H)
───────────────────────────────────────────────────────────────────────────

This excerpt opens a file named DATABASE in Deny None mode and locks two
portions of it: the first 128 bytes and bytes 1024 through 5119. After some
(unspecified) processing, it unlocks the same portions and closes the file.
Function 5CH, Code 00H denies all access (read or write) by any other
process to the specified region of the file. BX must contain the handle of
the file that contains the region to be locked. CX:DX (a 4-byte integer)
must contain the offset in the file of the beginning of the region. SI:DI
(a 4-byte integer) must contain the length of the region.If another process
attempts to use (read or write) a locked region, MS-DOS retries three
times; if the retries fail, MS-DOS issues Interrupt 24H for the requesting
process. You can change the number of retries with Function 44H, Code 0BH
(IOCTL Retry).If a program closes a file that contains a locked region or
terminates with an open file that contains a locked region, the result is
undefined.Programs should not rely on being denied access to a locked
region. A program can determine the status of a region (locked or unlocked)
by attempting to lock the region and examining look at the error code.

  Call
              AH =  5CH
              AL =  00H
              BX    Handle
              CX:DX Offset of region to be locked
              SI:DI Length of region to be locked

  Return
      Carry set:
              AX    1 = Invalid function code
                    6 = Invalid handle
                   33 = Lock violation
                   36 = Sharing buffer exceeded
      Carry not set: No error
  Macro Definition:

  OPEN_HANDLE   macro                 path, access
                mov                   dx,offset path
                mov                   al, access
                mov                   ah, 3DH
                int                   21H
                endm
  ;
  WRITE_HANDLE  macro                 handle,buffer,bytes
                mov                   bx,handle
                mov                   dx,offset buffer
                mov                   cx,bytes
                mov                   ah,40H
                int                   21H
                endm
  ;
  LOCK          macro                 handle,start,bytes
                mov                   bx, handle
                mov                   cx, word ptr start+2
                mov                   dx, word ptr start
                mov                   si, word ptr bytes+2
                mov                   di, word ptr bytes
                mov                   al, O
                mov                   ah, 5CH
                int                   21H              endm
  ;
  UNLOCK        macro                 handle,start,bytes
                mov                   bx,handle
                mov                   cx,word ptr start+2
                mov                   dx,word ptr start
                mov                   si,word ptr bytes+2
                mov                   di,word ptr bytes
                mov                   al,1
                mov                   ah,5CH
                int                   21H
                endm
  ;
  CLOSE_HNDL    macro                 handle
                mov                   bx,handle
                mov                   ah,3EH
                int                   21H
                endm

  stdout        equ                      1
  ;
  start1        dd                       0
  lgthl         dd                     128
  start2        dd                    1023
  lgth2         dd                    4096
  file          db                    "DATABASE", 0
  op_msg        db                    " opened.",ODH,OAH
  11_msg        db                    "First 128 bytes locked.",ODH,OAH
  12_msg        db                    "Bytes 1024-5119 locked.",ODH,OAH
  u1_msg        db                    "First 128 bytes unlocked.",ODH,OAH
  u2_msg        db                    "Bytes 1024-5119 unlocked."ODH,OAH
  c1_msg        db                    "closed.",ODH,OAH
  handle        dw                    ?
  ;
  begin:        open_handle           file,01000010b
                jc                    open_error
                mov                   handle,ax           ; save handle
                write_handle          stdout, file,8
                jc                    write error
                write_handle          stdout,op_msg,10
                jc                    write error
                lock                  handle,start1,lgth1
                jc                    lock_error
                write_handle          stdout,11_msg,25
                jc                    write_error
                lock                  handle, start2,lgth2
                jc                    lock_error
                write_handle          stdout,12_msg,25
                jc                    write_error
  ;
  ;  (Further processing here)
  ;
                unlock                handle,start1,lgth1
                jc                    unlock_error
                write_handle          stdout,u1_msg,27
                jc                    write_error
                unlock                handle,start2,lgth2
                jc                    unlock_error
                write_handle          stdout,u2_msg,27
                jc                    write_error
                close_handle          handle
                jc                    close_error
                write_handle          stdout,file,8
                jc                    write_error
                write_handle          stdout,cl_msg,10
                jc                    write_error
  ;
  ;  (Further processing here)
  ;
  open_error                                             ; routine not shown
  write error                                            ; routine not shown
  lock_error                                             ; routine not shown
  unlock_error                                           ; routine not shown
  close_error                                            ; routine not shown

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

Expanded Memory: Writing Programs That Break the 640K Barrier

Marion Hansen, Bill Krueger, and Nick Stuecklen☼

When the size of conventional memory was set at 640K, that seemed like all
the memory that anyone with a PC could ever use. But as programs written for
DOS grew larger, and the amount of data they could handle increased, what
had once seemed inexhaustible pinched like a pair of size 8 shoes on size 10
feet. Swapping to disk, or the use of overlays, is a solution, but it often
limits performance to unacceptable levels.

That's why Lotus Development Corp., Intel Corp., and Microsoft Corp. got
together to do something about DOS's 640K memory limit. Together they came
up with the Lotus/Intel/Microsoft Expanded Memory Specification (EMS). The
programming examples accompanying this article use the EMS and will run
under the AST Research Enhanced Expanded Memory Specification (EEMS), a
variation of the EMS, as well.

Expanded memory is memory beyond DOS's 640K limit. Just as DOS manages
conventional memory, the Expanded Memory Manager (EMM) manages expanded
memory. The EMM can manage up to 8 megabytes (MB) of expanded memory.
Programs that adhere to the EMS can use expanded memory without fear of
conflict.

Contrary to what you may have heard, you can put code as well as data into
expanded memory. Programs can store anything in expanded memory except their
stacks, which should reside in conventional memory. While placing the stack
in expanded memory is theoretically possible, managing a paged stack is
generally very difficult.

Expanded memory is implemented in one of two ways. One way is an expanded
memory board, where expanded memory physically resides on an add-in board.
Intel's Above(TM) Board and AST's Advantage(TM) are examples of expanded
memory boards. The other way is a LIMulator, such as the Compaq Deskpro
386's CEMM (Compaq Expanded Memory Manager), running on a 386-based system.
A LIMulator emulates expanded memory in extended memory (which is memory
from 1MB to 16MB) using the 80386 paging hardware.

Application programs can't use expanded memory automatically. This article
explains how to write programs that take advantage of expanded memory,
including programming techniques and examples, and the EMM functions.


Expanded Memory

In the current DOS environment, code and data can reside in one of three
memory locations. Each memory type has advantages and disadvantages.

Conventional Memory: Conventional memory is always available, except
whatever is used by application programs and resident software, and it's
easily accessible. Moving about in conventional memory, whether through code
or data, requires very little overhead. Segment register updates (when the
software crosses segment boundaries) are the only substantial software
overhead. Segment register updates are common to all three types of memory
and as such are not a limitation unique to conventional memory. Conventional
memory's drawback is its 640K limit. Large application programs, network
software, and resident spelling checkers, to name just three types of
software a typical user might have, consume prodigious amounts of
conventional memory.

Disk Memory: There's more than enough room on a disk for any software,
but the constant paging in and out of data and code in even the simplest
applications creates a great deal of overhead. This makes disk memory
undesirable for speed-sensitive applications.

DOS is not re-entrant, and you can invoke a terminate-and-stay-resident
(TSR) program in the middle of a DOS function. For this reason, TSR programs
sometimes have difficulties using DOS for disk I/O.

Expanded Memory: Like conventional memory, expanded memory is nearly always
available. And with fully populated expanded memory boards, it is sufficient
for most applications. Accessing expanded memory requires slightly more
overhead than accessing conventional memory but significantly less overhead
than accessing disk memory. When an application stays within a single 64K
page, expanded memory overhead is comparable to conventional memory
overhead.

Expanded memory is especially suitable for four types of software: TSR
programs, graphics packages, databases, and network software.

TSR programs permanently consume the memory they occupy. If a TSR program is
large in code or data, it consumes a great deal of conventional memory. A
TSR program that is designed to use expanded memory effectively keeps most
of its code and data in expanded memory, while maintaining a small kernel in
conventional memory for housekeeping chores, such as trapping interrupts,
and activating the rest of the TSR program in expanded memory.

Drawing and drafting packages frequently have to maintain multiple copies of
their graphics bit map. Secondary drawings, double buffers for animations,
and additional menus are all stored for later retrieval. Because recall
speed is essential, these bit maps must be maintained in memory. Just one
monochrome (1 bit per pixel) bit map with 640-by-350 resolution requires
nearly 28K of storage. Several such bit map copies can eat up conventional
memory, but they are easily accommodated in expanded memory.

Database programs sort huge volumes of data, typically much more than
conventional memory are able to handle. Expanded memory can be used to
store and sort large databases and is much faster than swapping to disk.

Network software creates large tables and volumes of resident data.
Although network software may be used infrequently──usually just for
peripheral sharing and file transfers──it can consume up to 50 percent of
available conventional memory. Putting network software in expanded memory
frees conventional memory for software that you use more frequently.

Using application software efficiently is a trade-off between the
convenience of generous amounts of expanded memory and the overhead of
paging in 64K blocks of it at a time. You should consider two questions
when deciding whether to use expanded or conventional memory for your
applications.

First, does the code execute a large number of far calls or jumps relative
to the time it spends executing other instructions? If it does, put the code
in conventional memory. If it doesn't, put the code in expanded memory.

Second, does the application's data require segment register initialization
each time it is accessed? If it does, use conventional memory. If it doesn't
use expanded memory.

As a rule of thumb, use expanded memory if both the time spent using data or
executing code and the preparation overhead are large.


The Page Frame

Expanded memory is managed the same way, whether it resides on an add-in
board or is emulated in extended memory. The Lotus/Intel/Microsoft EMS
defines a 64K segment of memory that resides between 640K and 1MB. This page
frame is a window into expanded memory (see Figure 1).

Just after the application program starts executing, it allocates a certain
number of 16K pages of expanded memory for its own use. Four pages of
expanded memory can be mapped into the expanded memory page frame at one
time. By mapping pages in and out of the page frame, the program can access
any area of the expanded memory that it allocated.

The EEMS allows the page frame to reside at any unused memory address
between 0K and 1,024K. Theoretically, this allows a page frame length of
1MB. Practical considerations, such as DOS and application programs, which
use conventional memory, and the BIOS and ROM on add-in boards, which use
memory above 640K, restrict the page frame to fewer than the possible 64
pages. Generally, in a typical AT system with an EGA, the maximum number of
mappable pages that DOS doesn't rely on is six 16K pages.

When the EMM software is installed, the user selects where in memory (above
640K) the page frame resides. The page frame address is user-selectable, so
that if another device uses memory at a particular address, the user can
then relocate the page frame.


Checking for Memory

Before an application program can use expanded memory, it must determine
if expanded memory and the EMM are present. There are two methods of
determining if the EMM is present: the open-handle technique and the
get-interrupt-vector technique.

Because the EMM is implemented as a device driver, in the open-handle
technique the program issues an open handle command (DOS function 3FH) to
determine whether the EMM device driver is present.

In the get-interrupt-vector technique, the program issues a get-interrupt-
vector command (DOS function 35H) to get the contents of interrupt vector
array entry number 67H. The pointer thus obtained accesses information that
tells the program whether the EMM is installed. The get-interrupt-vector
technique is easier to implement. Most programs can use either technique,
but if a program is a device driver or if it interrupts DOS during file
system operations, it must use the get-interrupt-vector technique.


Residents, Transients

Application programs that use expanded memory can be classified as either
resident or transient. A transient application program is resident only as
long as it executes. When it is finished running, the memory it used is
available for other programs. Examples of resident application programs
include spreadsheets, word processors, and compilers.

A resident application program remains in memory after it executes. Resident
application programs are usually invoked by a hardware interrupt, such as a
keystroke, or a software interrupt, such as a RAMdisk. Pop-up desktop
programs, RAMdisk drives, and print spoolers are examples of resident
application programs.

Resident programs and transient programs handle expanded memory differently.
Resident programs may interrupt transient programs that might be using
expanded memory, so resident programs must save and restore the state of the
page-mapping registers when they use expanded memory.

Transient programs don't interrupt other programs, so they don't need to
save and restore state. A resident program typically keeps the EMM handles
assigned to it and the expanded memory pages allocated to it by the EMM
until the system is rebooted. A transient program, in contrast, should
return its handle and pages just before it exits to DOS.


EMM Functions

The EMM functions, summarized in Figure 2, provide the tools that
application programs need to use expanded memory. Functions 1 through 7 are
general-purpose functions. Functions 8 and 9 are for interrupt service
routines, device drivers, and other memory-resident software. Functions 10
and 11 are reserved. Functions 12 through 14 are for utility programs.
Finally, Function 15 is for multitasking operating systems, although it can
be used for interrupt service routines as easily as Functions 8 and 9.

To use expanded memory, programs must perform these steps in the following
order:

  1. Check for the presence of the EMM by using the get-interrupt-vector or
     open-handle techniques.

  2. Check whether the EMM's version number is valid (only if the
     application is EMM version-specific)──Function 7 (Get EMM Version).

  3. Determine if enough unallocated expanded memory pages exist for the
     program──Function 3 (Get Unallocated Page Count).

  4. Save the state of expanded memory hardware (only if it is a resident
     program)──Function 8 (Save Page Map) or Function 15 (Get/Set Page Map).

  5. Allocate the number of 16K expanded memory pages needed by the
     program──Function 4 (Allocate Pages).

  6. Map the set of expanded memory pages (up to four) into the page
     frame──Function 5 (Map Handle Page).

  7. Determine the expanded memory page frame base address──Function 2 (Get
     Page Frame Address).

  8. Read/write to the expanded memory segment within the page frame, just
     as you read or write to conventional memory.

  9. Deallocate the expanded memory pages when the program is finished using
     them──Function 6 (Deallocate Pages).

 10. Restore the state of expanded memory hardware (only if it is a memory-
     resident program)──Function 9 (Restore Page Map) or Function 15
     (Get/Set Page Map).

Each EMM function's number is passed in register AX. The EMM will return the
function's status in the same register.

Programs use Int 67 to invoke the EMM. This works like DOS Int 21: preload
certain registers and issue an Int 67. All required registers are rigidly
specified, and certain conventions exist; for example, the AX register
always returns status.


Programming

The following two examples contain programs that have both code and data in
expanded memory. The first example (written in Microsoft C, Version 3.00)
illustrates how expanded memory can be used to save and restore data. The
main program (see Figure 3) calls a series of subprocedures that allocate
one 16K page of expanded memory, save the video RAM area (the user's screen)
to expanded memory, clear the screen, and then restore the screen from
expanded memory. The program assumes the user has a monochrome display
adapter operating in text mode (nongraphics) and video page zero is
displayed.

The program contains four subprocedures. The detect_emm subprocedure (see
Figure 4) determines whether the EMM software is installed. If it is
installed, the subprocedure returns to the caller. If the EMM software isn't
installed, the subprocedure generates an error message and exits the
program.

The get_expanded_memory_page subprocedure (see Figure 6) returns a pointer
to the expanded memory page and a 16-bit tag or handle associated with that
page. The subprocedure uses the EMM to allocate a page of expanded memory.
If an unallocated page exists, the procedure allocates it and maps it in and
returns the EMM handle that is associated with that page.

The check_status subprocedure (see Figure 5) is called after each EMM
function to verify that no EMM errors have occurred. The
release_expanded_memory_page subprocedure (see Figure 7) releases expanded
memory pages by deallocating the handle associated with those pages.

The second example illustrates one program loading another program into
expanded memory, which is especially applicable for developers of terminate-
and-stay-resident (TSR) applications. Both programs are written in Microsoft
Macro Assembler, Version 4.0.

The first program, expanded_memory_dispatcher_kernel (see Figure 8), loads a
set of subprocedures into expanded memory, from where they can be invoked at
any time. The set of loaded subprocedures is called a pseudo-overlay. This
program loads only one pseudo-overlay and immediately invokes all the
subprocedures contained in it. You can easily load as many pseudo-overlays
as you want by allocating additional pages in expanded memory, mapping up to
four of the newly allocated pages into the page frame, and then loading
additional pseudo-overlays.

The program has one subprocedure, test_for_EMM (see Figure 9), which
determines whether the EMM software is installed and returns the appropriate
status.

The kernel program loads the program OVERLAY.EXE (see Figure 10) into
expanded memory. A pseudo-overlay can't be larger than 64K because of the
four-page EMM page frame, so the developer must decompose the program into
separate modules that contain code or data no larger than 64K. You can have
up to 8MB of expanded memory and, therefore, up to 128 overlays.

Although the DOS "load overlay" function (DOS function 4B03H) is used to
load the pseudo-overlays, the code and any data loaded remain resident after
the load takes place. The subprocedures contained in the pseudo-overlay can
be accessed by using the list of pointers returned to the kernel by the
initialization code in the pseudo-overlay.

The pseudo-overlay program has five subprocedures. If the pseudo-overlay
program is invoked from the command line, then the command_line_entry_point
subprocedure (see Figure 11) tells the user that this is a pseudo-overlay
and thus can't be executed.

The initialization subprocedure (see Figure 13) is critical. The kernel
calls this subprocedure after the program is loaded. The initialization
subprocedure passes back to the kernel the data segment environment, a count
of the number of callable subprocedures in the overlay, and a far pointer to
each subprocedure.

The sum and diff subprocedures are examples of typical applications. The sum
subprocedure (see Figure 14) adds the numbers in the AX and DX registers and
displays the result, while the diff subprocedure (see Figure 15) subtracts
the numbers in the AX and DX registers and displays the result. The
display_result procedure (see Figure 16) converts the result into
printable ASCII form and then displays it.

The pseudo_overlay program places data into expanded memory. The data
segment for the pseudo_overlay program is shown in Figure 12. The common
data area for both programs is shown in Figure 17.


To Get EMS

If you're interested in developing application programs that use expanded
memory, call Intel for a free copy of the Lotus/Intel/Microsoft Expanded
Memory Specification. In the continental United States, but outside Oregon,
call (800) 538-3373. In Oregon, Alaska, Hawaii, or outside the United States
(except Canada), call (503) 629-7354. In Canada, call (800) 235-0444. For
more information on the AST EEMS, contact the AST Product Information Center
at (714) 863-1480.


Figure 1:  The Lotus/Microsoft EMS defines a 64K segment of memory that
           resides between 640K and 1MB.

         16M┌─────────────────────┐
            │                     │
            │                     │                       ┌───────────┐
            │                     │                    ┌──┴─────────┐ │
            ≈   Extended Memory   ≈                 ┌──┴──────────┐ │ │
            │                     │             ┌───┴───────────┐ │ │ │
            │                     │             │    Expanded   │ │ │ │
          1M├─────────────────────┤            .│    Memory     │ │ │ │
            │  Reserved by IBM    │         .   │               │ │ │ │
        896K├─────────────────────┤      .      │               │ │ │ │
            ├─────────────────────┤   .         │  Available to │ │ │ │
            │   64K Page Frame    │.            │DOS application│ │ │ │
            ├─────────────────────┤   .         │    programs   │ │ │ │
        786K├─────────────────────┤      .      │  adhering to  │ │ │ │
            │  Reserved by IBM    │         .   │  LIM Expanded │ │ │ │
        640K├─────────────────────┤            .│     Memory    │ │ ├─┘
            │    Conventional     │             │ Specification │ ├─┘
            │       Memory        │             │               ├─┘
            │                     │             └───────────────┘
            │    Managed by DOS   │
          0K└─────────────────────┘


Figure 2:  EMM Functions

EMM functions provide the tools that application programs need to use
expanded memory.

╓┌────────┌──────────────┌──────────────────┌────────────────────────────────╖
Function  Function       AX                Action
Number    Name           Register

 1        AH: 40         Get Status        Returns a status code to tell you
                                           whether the EMM is present and
                                           the hardware/software is working
Function  Function       AX                Action
Number    Name           Register
                                           the hardware/software is working
                                           correctly.

 2        AH: 41         Get Page Frame    Gives the program the location of
                         Address           the page frame.

 3        AH: 42         Get Unallocated   Tells the program the number of
                         Page Count        unallocated pages and the total
                                           number of pages in expanded
                                           memory.

 4        AH: 43         Allocate Pages    Allocates the number of expanded
                                           memory pages requested by the
                                           program; assigns a unique EMM
                                           handle to the set of pages
                                           allocated.

 5        AH: 44         Map Handle Page   Maps the specified logical page
                                           in expanded memory to the
Function  Function       AX                Action
Number    Name           Register
                                           in expanded memory to the
                                           specified physical page within
                                           the page frame.

 6        AH: 45         Deallocate Pages  Deallocates the pages currently
                                           allocated to an EMM handle.

 7        AH: 46         Get EMM Version   Returns the version number of the
                                           EMM software.

 8        AH: 47         Save Page Map     Saves the contents of the page
                                           mapping registers of all expanded
                                           memory boards.

 9        AH: 48         Restore Page Map  Restores the contents of the page
                                           mapping registers.

10        AH: 49                           Reserved.

Function  Function       AX                Action
Number    Name           Register

11        AH: 4A                           Reserved.

12        AH: 4B         Get EMM Handle    Returns the number of active EMM
                         Count             handles.

13        AH: 4C         Get EMM Handle    Returns the number of pages
                         Pages             allocated to a specific EMM
                                           handle.

14        AH: 4D         Get All EMM       Returns the active EMM handles
                         Handle Pages      and the number of pages allocated
                                           to each one.

15        AH:4E; AL:00   Get/Set           Saves and restores the mapping
          AH:4E; AL:01   Page Map          context of the active EMM handle.
          AH:4E:AL:02


Figure 3:  Main Program

The main program allocates one 16K page of expanded memory, saves the video
RAM area to expanded memory, clears the screen and then restores the screen
to expanded memory.

#include <dos.h>
#include <stdio.h>

#define EMM_INT 0x67              /* EMM interrupt number */
#define GET_PAGE_FRAME_BASE 0x41  /* EMM func = get page frame base address *
#define GET_FREE_COUNT 0x42       /* EMM Func = get unallocated pages count *
#define ALLOCATE_PAGES 0x43       /* EMM Func = allocates pages */
#define MAP_PAGES 0x44            /* EMM Func = map pages */
#define DEALLOCATE_PAGES 0x45     /* EMM Func = deallocate pages */
#define GET_INT_VECTOR 0x35       /* DOS func = get interrupt vector */
#define DEVICE_NAME_LEN 8         /* Number of chars in device driver name
                                     field */
#define VIDEO_RAM_SIZE 4000       /* Total bytes in video RAM (char/attr) */
#define VIDEO_RAM_BASE 0xB0000000 /* Video RAM start address (MDA) */

union REGS input_regs, output_regs; /* Regs used for calls to EMM and DOS */
struct SREGS segment_regs;
unsigned int emm_status;            /* Status returned by EMM */

main ()

{
unsigned int i;
long target_time, current_time;
char *video_ram_ptr = {VIDEO_RAM_BASE}; /*  Pointer to video RAM  */
unsigned int emm_handle;                /*  EMM handle  */
char *expanded_memory_ptr;              /*  Pointer to expanded memory */

/* Ensure that the Expanded Memory Manager software is installed on the
   user's system.  */

   detect_emm();

/*  Get a page of expanded memory.  */

   get_expanded_memory_page (&expanded_memory_ptr, &emm_handle);

/*  Copy the current video RAM contents to expanded memory.  */

   memcpy (expanded_memory_ptr, video_ram_ptr, VIDEO_RAM_SIZE);

/*  Clear the screen to nulls.  */

   memset (video_ram_ptr, '\0', VIDEO_RAM_SIZE);

/*  Delay for 1 second so the user can see the blanked screen.  */

   time (&current_time);
   target_time = current_time + 1;
   while (current_time < target_time)
    {
     time (&current_time);
    }

/*  Restore the video RAM contents from expanded memory.  */
memcpy (video_ram_ptr, expanded_memory_ptr, VIDEO_RAM_SIZE);

/*  Deallocate the expanded memory page  */

   release_expanded_memory_page (emm_handle);

   exit(0);
}


Figure 4:  Detect_EMM Subprocedure

The detect_emm subprocedure determines whether the EMM driver software is
installed.

detect_emm ()

{
static char EMM_device_name [DEVICE_NAME_LEN] = {"EMMXXXX0"};
char *int_67_device_name_ptr;


/*  Determine the address of the routine associated with INT 67 hex.  */

  input_regs.h.ah = GET_INT_VECTOR;  /*  DOS function  */
  input_regs.h.al = EMM_INT;         /*  EMM interrupt number  */
  intdosx (&input_regs, &output_regs, &segment_regs);
  int_67_device_name_ptr =
  (segment_regs.es * 65536) + 10;    /*  Create ptr to device name field  */

/*  Compare the device name with the known EMM device name.  */

  if(memcmp(EMM_device_name,int_67_device_name_ptr,DEVICE_NAME_LEN) !=0)
   {
     printf ("\x07Abort: EMM device driver not installed\n");
     exit(0);
   }
}


Figure 5:  Check_Status Subprocedure

The check_status subprocedure is called after each EMM function to make
sure that no EMM errors have occurred.

check_status (emm_status)
unsigned int emm_status;
{
static char *emm_error_strings[] = {
  "no error",
  "EMM software malfunction",
  "EMM hardware malfunction",
  "RESERVED",
  "Invalid EMM handle",
  "Invalid EMM function code",
  "All EMM handles being used",
  "Save/restore page mapping context error",
  "Not enough expanded memory pages",
  "Not enough unallocated pages",
  "Can not allocate zero pages",
  "Logical page out of range",
  "Physical page out of range",
  "Page mapping hardware state save area full",
  "Page mapping hardware state save area already has handle",
  "No handle associated with the page mapping hardware state save area",
  "Invalid subfunction"
 };

/*  IF EMM error, THEN print error message and EXIT  */

   if (emm_status != 0)                    /*  IF EMM error...  */
    {
      emm_status -= 0x7F;                  /*  Make error code zero-based */
      printf ("\x07Abort: EMM error = ");  /*  Issue error prefix  */
      printf ("%s\n", emm_error_strings[emm_status]);
                                           /*  Issue actual error message */
      exit(0);                             /*  And then exit to DOS */
    }
}


Figure 6:  Get_Expanded_Memory_Page Subprocedure

The get_expanded_memory_page subprocedure returns a pointer to the expanded
memory page and a 16-bit tag or handle associated with that page.

get_expanded_memory_page (expanded_memory_ptr_ptr, emm_handle_ptr)

unsigned int *emm_handle_ptr;     /*  16 bit handle returned by EMM  */
char *(*expanded_memory_ptr_ptr); /*  Pointer to expanded memory page  */

{
unsigned int page_frame_base;     /*  Expanded memory page frame base  */
unsigned int physical_page = {0}; /*  Physical page number  */

/*  Get unallocated pages count.  */

input_regs.h.ah = GET_FREE_COUNT;    /*  EMM function  */
int86x (EMM_INT, &input_regs, &output_regs, &segment_regs);
emm_status = output_regs.h.ah;
check_status(emm_status);            /*  Check for errors  */
if (output_regs.x.bx < 1)            /*  Check unallocated page count  */
 {
  printf ("\x07Abort: insufficient unallocated expanded memory pages\n");
  exit(0);
 }

/*  Allocate the specified number of pages.  */

input_regs.h.ah = ALLOCATE_PAGES;     /*  EMM function  */
input_regs.x.bx = 1;                  /*  Number of pages to allocate  */
int86x (EMM_INT, &input_regs, &output_regs, &segment_regs);
emm_status = output_regs.h.ah;
check_status(emm_status);             /*  Check for errors  */
*emm_handle_ptr = output_regs.x.dx;   /*  Get EMM handle  */

/*  Map the logical page into physical page 0.  */

   input_regs.h.ah = MAP_PAGES;          /*  EMM function  */
   input_regs.h.al = 0;                  /*  Logical page number  */
   input_regs.x.bx = physical_page;      /*  Physical page number  */
   input_regs.x.dx = *emm_handle_ptr;    /*  EMM handle  */
   int86x (EMM_INT, &input_regs, &output_regs, &segment_regs);
   emm_status = output_regs.h.ah;
   check_status(emm_status);             /*  Check for errors  */

/*  Determine the page frame address.   */

   input_regs.h.ah = GET_PAGE_FRAME_BASE; /*  EMM function  */
   int86x (EMM_INT, &input_regs, &output_regs, &segment_regs);
   emm_status = output_regs.h.ah;
   check_status(emm_status);              /*  Check for errors  */
   *expanded_memory_ptr_ptr =
     (output_regs.x.bx * 65536)
     + (physical_page * 16 * 1024);       /*  Set the expanded memory ptr */
}


Figure 7:  Release_Expanded_Memory_Page Subprocedure

The release_expanded_memory_page subprocedure releases the expanded memory
pages by de-allocating the handle associated with those pages.

release_expanded_memory_page (emm_handle)

unsigned int emm_handle;    /* Handle identifying which page set to
                               deallocate */
{

/*  Release the expanded memory pages by deallocating the handle
    associated with those pages.  */

   input_regs.h.ah = DEALLOCATE_PAGES;  /*  EMM function  */
   input_regs.x.dx = emm_handle;        /*  EMM handle passed in DX  */
   int86x (EMM_INT, &input_regs, &output_regs, &segment_regs);
   emm_status = output_regs.h.ah;
   check_status(emm_status);            /*  Check for errors  */
}


Figure 8:  Kernel Model

The pseudo-overlay is loaded into expanded memory by the kernel.  The
kernel then calls the initialization procedure within the pseudo-overlay
that returns a data structure to the kernel. The data structure describes
the first object that will be located in expanded memory starting at the
data and extra segments of the pseudo-overlay, the number of subprocedure
entry points in the pseudo-overlay, and a list of far pointers to each of
the subprocedures contained in the pseudo-overlay. The developer must
establish a convention for the sequence of far pointers and what the
procedures they point to do. Other information could be passed in this
structure as well, for example, number and types of parameters that are
required by the subprocedures in the pseudo-overlay. This example uses a
literal to determine the maximum number of far pointers that may be passed.
To allocate additional space for a larger number of entries, simply
increase the value of max_proc_entries. The example assumes a maximum of 64
entries can be returned.

CODE    SEGMENT PARA PUBLIC 'CODE'
ORG     100h
ASSUME  CS:CODE, DS:DATA, ES:NOTHING, SS:STACK

max_proc_entries              EQU        64

pseudo_over_struct            STRUC
      proc_data_segment       DW        ?
      proc_extra_segment      DW        ?
      proc_entry_count        DW        ?
      proc_entry_ptr          DD max_proc_entries DUP (?)
pseudo_over_struct            ENDS

main  PROC  NEAR

      MOV   AX, DATA                       ; Segment initialization
      MOV   DS, AX

check_for_emm_loaded:
      CALL  test_for_EMM                   ; Use the "interrupt vector"
      JE    get_emm_page_frame             ; technique to determine
      JMP   emm_err_exit                   ; whether EMM is loaded

get_emm_page_frame:
      MOV   AH, 41h                        ; Get the page frame base
      INT   67h                            ; address from EMM
      OR    AH, AH
      JZ    allocate_64K
      JMP   emm_err_exit

allocate_64K:
      MOV   exp_mem_segment, BX            ; Allocate 4 pages of expanded
      MOV   AH, 43h                        ; ed memory for this example.
      MOV   BX, 4                          ; More can be allocated
      INT   67h                            ; depending on the number of
      OR    AH, AH                         ; overlays to be loaded.
      JZ    map_64K                        ; Actually, in this case,
      JMP   emm_err_exit                   ; only a single page is
                                           ; required because the example
                                           ; pseudo-overlay is extremely
                                           ; small.
map_64K:
      MOV   handle, DX                     ; Map in the first 4 logical
      MOV   CX, 4                          ; pages at physical pages 0
map_pages_loop:                            ; through 3
      MOV   AH, 44h                        ;    logical page 0 at
      MOV   BX, CX                         ;       physical page 0
      DEC   BX                             ;    logical page 1 at
      MOV   AL, BL                         ;       physical page 1
      MOV   DX, handle                     ;    logical page 2 at
      INT   67h                            ;       physical page 2
      OR    AH, AH                         ;    logical page 3 at
      LOOPE map_pages_loop                 ;       physical page 3
      JE    init_load_struct               ; If additional overlays were
      JMP   emm_err_exit                   ; required, each overlay
                                           ; would be loaded after
                                           ; mapping and a new set of
                                           ; logical pages would be
                                           ; mapped at the same
                                           ; physical pages.

init_load_struct:
      MOV   ES, exp_mem_segment            ; Initialize pseudo-overlay
      MOV   DI, 0                          ; environment and procedure
      MOV   CX, (SIZE pseudo_over_struct)  ; pointer area. This structure
      MOV   AL, 0                          ; begins at the page
      REP   STOSB                          ; frame segment address.

      MOV   AX, (SIZE pseudo_over_struct)  ; Compute the load address
      ADD   AX, 000Fh                      ; within expanded memory for
      AND   AX, 0FFF0h                     ; the overlay. The address is
      MOV   CX, 4                          ; rounded up to the next
      SHR   AX, CL                         ; higher paragraph boundary
      ADD   AX, exp_mem_segment            ; immediately following the
      MOV   parm_block.load_segment, AX    ; pseudo-overlay environment
      MOV   parm_block.reloc_factor, AX    ; & procedure pointer
                                           ; structure. This computation
                                           ; tion takes into account
                                           ; the maximum number of
                                           ; procedure entry points
                                           ; which the pseudo-overlay
                                           ; is going to return to
                                           ; this program.

      MOV   WORD PTR entry_point[0], 100h  ; Build .COM file entry
      MOV   WORD PTR entry_point[2], AX    ; point

      MOV   AH, 4Bh                        ; Load the pseudo-overlay
      MOV   AL, 03h                        ; using the DOS "load
      LEA   DX, pseudo_over_name           ; overlay" function
      PUSH  DS
      POP   ES
      LEA   BX, parm_block
      INT   21h
      JC    emm_err_exit

      PUSH  DS                             ; Transfer control to the
      PUSH  ES                             ; loaded pseudo-overlays
      CALL  DWORD PTR entry_point          ; initialization code
      POP   ES
      POP   DS
      OR    AH, AH
      JZ    call_over_procedures
      JMP   emm_err_exit

call_over_procedures:
      MOV   ES, exp_mem_segment            ; As an example of passing
      MOV   BX, 0                          ; control to a procedure
      MOV   DI, 0                          ; existing in expanded
      MOV   CX, ES:[BX].proc_entry_count   ; memory, each procedure
      JCXZ  deallocate_exp_memory          ; contained in the overlay will
                                           ; be called in sequence.
                                           ; Obviously, a single procedure
                                           ; could be called just as
                                           ; easily.

pseudo_over_call_loop:
      PUSH  BX
      PUSH  CX
      PUSH  DI
      PUSH  ES
      PUSH  DS

      LDS   AX, ES:[BX+DI].proc_entry_ptr
      MOV   WORD PTR CS:tp_ent_ptr[0], AX
      MOV   WORD PTR CS:tp_ent_ptr[2], DS

      MOV   AX, 123                        ; Pass 2 numbers to
      MOV   DX, 23                         ; the procedures

      MOV   DS, ES:[BX].proc_data_segment  ; Set up pseudo-overlays
      MOV   ES, ES:[BX].proc_extra_segment ; segment environment
      CALL  DWORD PTR CS:tp_ent_ptr        ; Call each procedure

      POP   DS
      POP   ES
      POP   DI
      POP   CX
      POP   BX

   ADD   DI, 4                             ; Adjust index to the next
      LOOP  pseudo_over_call_loop          ; procedure (4 bytes long)
                                           ; pointer & loop till all
                                           ; have been called

deallocate_exp_memory:
      MOV   AH, 45h                        ; Return the allocated
      MOV   DX, handle                     ; pages to the expanded
      INT   67h                            ; memory manager
      OR    AH, AH
      JNZ   emm_err_exit

exit:
      MOV   AH, 4Ch                        ; Return a normal exit code
      MOV   AL, 0
      INT   21h

emm_err_exit:
      MOV   AL, AH                         ; Display the fact that
      MOV   AH, 09h                        ; an EMM error occurred
      LEA   DX, emm_err_msg                ; Go to the normal exit
      INT   21h
      JMP   exit

      tp_ent_ptr              DD        ?  ; CS relative far pointer
                                           ; used for transfer to the
main  ENDP                                 ; procedures in the
                                           ; pseudo_overlay


Figure 9:  Procedure to Test for the Presence of EMM

This procedure tests for the presence of the EMM in the system. The carry
flag is set if the EMM is present. The carry flag is clear if the EMM is not
present.

test_for_EMM    PROC NEAR

       MOV     AX, 3567h              ; Issue "get interrupt vector"
       INT     21h

       MOV     DI, 000Ah              ; Use the SEGMENT in ES
                                      ; returned by DOS, place
                                      ; the "device name field"
                                      ; OFFSET in DI.

       LEA     SI, EMM_device_name    ; Place the OFFSET of the EMM
                                      ; device name string in SI,
                                      ; the SEGMENT is already in DS.

       MOV     CX, 8                  ; Compare the name strings
       CLD                            ; Return the status of the
       REPE    CMPSB                  ; compare in the ZERO flag
       RET

test_for_EMM ENDP

CODE        ENDS


Figure 10:  Pseudo-overlay Module

The kernel loads the pseudo-overlay into expanded memory.The kernel calls
the initialization procedure within the pseudo-overlay. The initialization
procedure returns a data structure to the kernel. The data structure
describes the first object that will be located in expanded memory starting
at the page frame segment address. It contains the data and extra segments
of the pseudo-overlay, the number of subprocedure entry points in the
pseudo-overlay, and a list of far pointers to each of the subprocedures
contained in the pseudo-overlay.

CODE    SEGMENT PARA PUBLIC 'CODE'
ASSUME  CS:CODE, DS:DATA
ORG     100h

actual_proc_entries         EQU         2

overlay_entry_struct        STRUC
        proc_data_segment   DW          ?
        proc_extra_segment  DW          ?
        proc_entry_count    DW          ?
        proc_entry_ptr      DD          actual_proc_entries DUP (?)
overlay_entry_struct        ENDS


Figure 11:  Procedure to Identify Overlay

This procedure merely informs a user that this is the overlay and cannot be
executed from the command line.

command_line_entry_point        PROC        NEAR

        MOV     AX, DATA                    ; Set up local data
        MOV     DS, AX                      ; segment

        LEA     DX, overlay_err_msg         ; Display overlay error
        MOV     AH, 09h                     ; message
        INT     21h

        MOV     AX, 4C00h                   ; Exit back to DOS
        INT     21h

command_line_entry_point        ENDP


Figure 12:  Data Segment for the Pseudo-overlay Module

This is the data segment for the pseudo-overlay program.

DATA  SEGMENT PARA PUBLIC 'DATA'

sum_msg         DB   0Dh, 0Ah, 'Sum of numbers = ', '$'
diff_msg        DB   0Dh, 0Ah, 'Difference of numbers = ', '$'
overlay_err_msg DB   'Overlay cannot be executed via the command line$'
powers_of_ten   DW   10000, 1000, 100, 10, 1

DATA  ENDS

END   command_line_entry_point


Figure 13:  Pseudo-overlay Data Structure Initialization Procedure

The initialization subprocedure is called by the kernel after the program is
loaded. It passes to the kernel the data segment environment, a count of the
number of callable subprocedures in the overlay, and a far pointer to each
subprocedure.

initialization  PROC    FAR

MOV  AX, DATA                                        ; Set up a local
MOV  DS, AX                                          ; data segment

MOV  AH, 41h                                         ; Get the page
INT  67h                                             ; frame segment
OR   AH, AH                                          ; address from EMM
JNZ  error

MOV  ES, BX                                          ; Create pointer
MOV  DI, 0                                           ; to the page frame
                                                     ; segment address

MOV  ES:[DI].proc_data_segment, DS                   ; Return local data
MOV  ES:[DI].proc_extra_segment, DS                  ; & extra segment
                                                     ; back to the kernel

MOV  WORD PTR ES:[DI].proc_entry_count, 2            ; Return the number
                                                     ; of callable
                                                     ; procedures
MOV  WORD PTR ES:[DI].proc_entry_ptr[0], OFFSET sum  ; Return
MOV  WORD PTR ES:[DI].proc_entry_ptr[2], SEG    sum  ; pointer to each
MOV  WORD PTR ES:[DI].proc_entry_ptr[4], OFFSET diff ; local callable
MOV  WORD PTR ES:[DI].proc_entry_ptr[6], SEG    diff ; procedure in the
                                                     ; pseudo-overlay
                                                     ; back to kernel

exit:  MOV   AH, 0                                   ; Set status in AH
                                                     ; = passed
error: RET                                           ; Return status
                                                     ; in AH


Figure 14:  Procedure to Add AX and DX

This procedure adds AX and DX and displays the result.

sum     PROC    FAR
        ADD     AX, DX                 ; Add numbers
        PUSH    AX                     ; Display sum message
        LEA     DX, sum_msg
        MOV     AH, 09h
        INT     21h
        POP     AX
        CALL    display_result         ; Display sum
        RET
sum     ENDP


Figure 15:  Procedure to Subtract AX and DX

This procedure subtracts AX and DX and displays the result.

diff    PROC    FAR

        SUB     AX, DX               ; Subtract numbers
        PUSH    AX                   ; Display difference message
        LEA     DX, diff_msg
        MOV     AH, 09h
        INT     21h
        POP     AX
        CALL    display_result       ; Display difference
        RET

diff    ENDP


Figure 16:  Procedure to Display Number in AX in Decimal

This procedure displays the number in AX in decimal

display_result  PROC    NEAR

        LEA     DI, powers_of_ten
        MOV     CX, 5
display_loop:
        XOR     DX, DX            ; Divide the number passed
        DIV     WORD PTR [DI]     ; in AX by descending powers of ten
        ADD     AL, '0'           ; Convert digit to ASCII

        PUSH    DX                ; Output the digit
        MOV     DL, AL
        MOV     AH, 02h
        INT     21h
        POP     AX

        ADD     DI, 2
        LOOP    display_loop
        RET

display_result        ENDP


Figure 17:  Data and Stack Segment for the Kernel and the Pseudo-overlay

This is the common data area for the kernel and psuedo-overlay programs.

DATA            SEGMENT PARA PUBLIC 'DATA'

emm_err_msg        DB    'EMM error occurred$' ; EMM diagnostic message
pseudo_over_name   DB    'OVERLAY.EXE', 0      ; Name of pseudo-overlay
EMM_device_name    DB    'EMMXXXX0'            ; Standard EMM device name
exp_mem_segment    DW    ?                     ; Temp for expanded
                                               ; memory page frame
                                               ; segment address
handle             DW    ?                     ; Temp for handle allocated
                                               ; to the kernel
entry_point  DD          ?                     ; Far pointer to the
                                               ; entry point for a .COM
                                               ; file
parm_block_struct  STRUC                       ; Structure definition
    load_segment   DW    ?                     ; for a "load overlay"
    reloc_factor   DW    ?                     ; parameter block
parm_block_struct  ENDS
parm_block         parm_block_struct <>        ; The actual parameter
                                               ; block

DATA        ENDS


STACK   SEGMENT PARA STACK 'STACK'
        local_stack     DW 256 DUP ('^^')
STACK   ENDS

END        main

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

Keep Track of Your Windows Memory With FREEMEM

Charles Petzold☼

For the beginning Microsoft(R) Windows programmer, even simple, do-nothing
Windows programs seem to be forbiddingly long and complex. You may have
concluded that all Windows applications are monstrous collections of code.

This is not true. Take the program called FREEMEM, for example, a complete
and useful Microsoft Windows application in fewer than 100 lines of C code.
The FREEMEM program displays the amount of available free memory in an icon
at the bottom of the Windows screen. The free memory value is updated every
second and is consistent with the value that is shown in the MS-DOS
Executive "About" box.

The short length of FREEMEM helps to clarify the structure of Windows
applications and allows us to discuss extensively some details of Windows
programming. And since FREEMEM is certainly an unusual Windows program,
we'll explore a few of its tricks as well.


Overall Structure

FREEMEM.C contains only two functions: WinMain (see Figure 1) and WndProc
(see Figure 2). Similar functions are found in most Windows applications.

WinMain is the entry point to FREEMEM. WinMain is devoted mainly to
performing all of the preliminary initialization chores needed to create and
display a window.

The WndClass structure passed to the RegisterClass function defines the
window class. The most important item in the WndClass structure is
lpfnWndProc, which specifies the function within FREEMEM that processes
messages from Microsoft Windows. This is the WndProc function shown in
Figure 2.

The CreateWindow, ShowWindow, and UpdateWindow functions all cause Windows
to generate messages to the WndProc function. Within the WndProc function,
these messages are identified by names beginning with the letters WM. The
names are simply macro identifiers defined in WINDOWS.H that conveniently
hide the actual numbered codes.

WndProc sorts out the messages through a case statement. Only a few of the
many messages that Windows sends to WndProc are handled directly. The rest
are sent on to the DefWindowProc function within Windows for default
processing.

CreateWindow will cause Microsoft Windows to generate a WM_CREATE message.
ShowWindow usually generates a whole sequence of messages, among them
WM_SIZE and WM_ERASEBKGND. These messages cause the window to be displayed
on the screen and the background of the client area that is to be erased.
When FREEMEM calls the function UpdateWindow, Microsoft Windows generates a
WM_PAINT message, which instructs WndProc to paint the client area of the
window.

Following the UpdateWindow call, FREEMEM enters a message loop. The
GetMessage call retrieves a message from FREEMEM's message queue. If no
messages to FREEMEM are available, control can pass to another Windows
application. Currently, Windows' nonpreemptive multitasking guarantees that
a call to GetMessage is the only time that FREEMEM, in effect, stops
running. It regains control when FREEMEM's message queue contains some
messages and other applications' message queues are empty. It's really a
little more complex than this, but I'll explain it later.

TranslateMessage translates keystroke messages into character code messages.
Although FREEMEM doesn't care about the keyboard, this translation is
necessary to provide a keyboard interface with FREEMEM's system menu.
DispatchMessage then sends the message to the WndProc procedure.

FREEMEM will terminate when GetMessage retrieves a WM_QUIT message from the
queue. The GetMessage function returns a zero in this case, and FREEMEM
drops out of the message loop.

So far, this routine is normal for most Windows applications, but let's take
a look at the details.


Creative Use of Icons

Unlike most Windows programs, FREEMEM is intended to be displayed only as an
icon. Once FREEMEM is running, you can use the keyboard or mouse to open the
icon into a regular tiled window, but it won't give you any more information
in that form.

Most Windows applications have static pictorial icons. The programmer
usually creates these icons with the ICONEDIT utility supplied with the
Microsoft Windows Software Development Kit. The icon file is referenced by a
name in the resource script file. When the window class structure is
constructed, the icon is specified by the statement

  WndClass.hIcon = LoadIcon (hInstance, (LPSTR)szAppName) ;

The LoadIcon function loads the icon from the section of the .EXE file where
all the resources are stored and assigns a handle to it. (I'm assuming here
that the icon has the same name as the application and that szAppName is a
pointer to a character string with that name.)

If instead you use the following statement:

  WndClass.hIcon = NULL ;

then the application is responsible for drawing the icon. This lets your
application create a dynamic icon that you can alter. The CLOCK application
included with Microsoft Windows uses this same technique to display the
current time even when the window is an icon.

From the program's perspective, a NULL icon is just a tiny window that you
can draw on in the same way that you draw on the client area of a normal
window. If you need to know when your application is becoming an icon, you
can find out the information from the WM_SIZE message. For instance, CLOCK
takes note of this change so it can eliminate the second hand and update the
clock every minute in the iconic form.


Forcing the Icon

Normally, to execute a Windows application from the MS-DOS Executive, you
either press the Enter key while the cursor is on the program name, double-
click the program name with the mouse, or select File Run from the menu. If
you want to load a program as an icon instead, you just press Shift-Enter
when the cursor is on the program name or select File Load from the menu.
However, with FREEMEM, it doesn't matter what you do──it always loads as an
icon.

Most Windows applications execute the function

  ShowWindow (hWnd, nCmdShow) ;

shortly before entering the message loop. The nCmdShow variable is passed to
the program as a parameter to WinMain. If you RUN a program from the MS-DOS
Executive, nCmdShow is set as equal to the handle of the window that the
application is replacing on the display, which is usually the handle of the
MS-DOS Executive main window. If you LOAD an application as an icon,
nCmdShow is set equal to SHOW_ICONWINDOW. Your application usually doesn't
have to figure this out; it simply passes this parameter on to ShowWindow.

However, you don't have to use the nCmdShow variable with ShowWindow. In
FREEMEM, we use this line instead:

  ShowWindow (hWnd, SHOW_ICONWINDOW) ;

This forces the window to appear as an icon regardless of the value of
nCmdShow.

You can achieve other results with this technique. If you have an
application that you always want to appear first as a "zoomed" full-screen
display, you can use

  ShowWindow (hWnd, SHOW_FULLSCREEN) ;

You can even force FREEMEM to occupy a particular icon position at the
bottom of the display. If you replace the existing ShowWindow call in
FREEMEM with

  ShowWindow (hWnd, (int) 0xFF8F) ;

the icon will be positioned in icon slot 15, all the way over at the right
of the display. This syntax is somewhat obscure, but it is documented in the
Programmer's Reference manual included with the Software Development Kit.


Keeping It an Icon

As I mentioned before, you can easily use the keyboard or mouse to open up
FREEMEM into a regular tiled window. If you'd like to prevent that and
ensure that FREEMEM is always displayed as an icon, just add these two lines
to the WndProc function:

  case WM_QUERYOPEN:
     break ;

A reasonable place for these lines would be right above the line that reads
"case WM_DESTROY." Now when you try to use the mouse to open the icon, the
icon jumps back to the bottom of the display.

These two lines may not seem to be doing very much, but a closer look will
reveal what they're up to.

Microsoft Windows will send a WM_QUERYOPEN message to a program when it
wants to open an icon into a window. The documentation for the WM_QUERYOPEN
message states that a windows procedure such as WndProc must return zero to
prevent the icon from being opened. Under normal circumstances, the
WM_QUERYOPEN message is passed on to the DefWindowProc function, which
returns a nonzero value. (The DefWindowProc routine is provided with the
Windows Software Development Kit in the file WINDWP.C.) Windows then opens
the icon.

With the two lines shown above, however, WndProc will return a value of zero
for a WM_QUERYOPEN message. So, when Windows asks, "Do you want to be
opened?", WndProc answers, "Zero," which in this case means "No thanks."


The Timer Messages

FREEMEM continues to update its display of free memory even while other
applications are running. It performs this through the use of the Microsoft
Windows timer, which sends WM_TIMER messages to the window procedure. In
this respect, FREEMEM is similar to the CLOCK application. The timer permits
a form of multitasking without requiring the application to hog precious
processing time.

In its WinMain function, FREEMEM requests a timer from Windows with the
following statement:

  if (!SetTimer (hWnd, 1, 1000, NULL))
     return FALSE ;

The third parameter to SetTimer indicates that FREEMEM wants a WM_TIMER
message every 1,000 milliseconds, or once a second.

If SetTimer returns zero, it means that Windows cannot assign a timer to the
program. If you've ever tried to determine how many CLOCK applications can
be loaded and running in Windows at the same time, you know that the limit
is 15. Any additional SetTimer calls will return a zero. Actually, there is
a way to get more than 15 timers out of Windows, but it's more complex.

If FREEMEM cannot get a timer, the WinMain function must return a zero value
(the value of FALSE), which simply terminates the application. As you'll
note from the two other "return FALSE" lines in WinMain, FREEMEM also
terminates if a previous instance is already running or if FREEMEM cannot
register the window class. While prohibiting multiple instances of FREEMEM
from running isn't necessary, there is really no reason to run FREEMEM more
than once.

If you prefer that FREEMEM tell you why it must terminate when SetTimer
returns zero, you can instead use the code shown in Figure 3. This is more
informative, if not exactly friendly.

The second parameter in the SetTimer call is a "timer ID." When WndProc
receives the WM_TIMER message, the wParam parameter contains this value. By
using different timer IDs, a Windows application can set multiple timers and
do different processing for each one. FREEMEM must also use the timer ID to
relinquish the timer with the KillTimer call when WndProc receives a
WM_DESTROY message.

WM_TIMER messages are not asynchronous. Specifying 1,000 milliseconds in the
SetTimer call does not guarantee that the window procedure will receive a
WM_TIMER message precisely every second. The WM_TIMER messages are placed in
the normal message queue and synchronized with all the other messages.

If another application is busy for more than a second, FREEMEM does not get
any WM_TIMER messages during that time. FREEMEM receives its next WM_TIMER
message from the queue only when the other applications yield control by
calling GetMessage, PeekMessage, or WaitMessage.

In fact, like WM_PAINT messages, WM_TIMER messages are handled by Microsoft
Windows as low-priority items. I said before that control passes to other
applications only when a program's message queue is empty. Actually, if a
program's message queue contains only WM_PAINT or WM_TIMER messages, and the
message queue of another program contains any messages other than WM_PAINT
or WM_TIMER, control will pass to the other application anyway.

Moreover, Windows does not continue loading up the message queue with
multiple WM_TIMER messages if another application is running during this
time. In that case, Windows combines several WM_TIMER messages in the
message queue into a single message so that the application doesn't get a
bunch of them all at once.

While Windows' handling of WM_TIMER messages is adequate for a program like
FREEMEM, keep these points in mind if you ever do a clock application like
the Windows CLOCK. A 1,000-millisecond timer value does not guarantee 3,600
WM_TIMER messages over the course of an hour. When you get a WM_TIMER
message, you'll want to determine the real time either with a C function or
through MS-DOS directly. You can't keep time yourself solely with WM_TIMER
messages.


Calculating Memory

When FREEMEM receives a WM_TIMER message, it must determine the amount of
free memory. This little chore was the most difficult part of programming
FREEMEM. I knew the information was available because the MS-DOS Executive
"About" box showed a free memory value. But Microsoft Windows has around 400
function calls, and finding the one you need is sometimes difficult.

It turns out that the MS-DOS Executive gets a free memory value by calling
GlobalCompact with a parameter of zero. Windows applications normally use
GlobalCompact to generate some free memory from the global heap. If
necessary, Windows compacts memory and frees up segments marked as
discardable when GlobalCompact is called. (The global heap is comprised of
memory outside of the program's local data segment.)

A close reading of the documentation of GlobalCompact reveals that "if [the
parameter] is zero, the function returns a value but does not compact
memory." Thus, the value that FREEMEM displays is really a potential free
memory size rather than actual free memory. It represents how much memory a
Windows application can get from the global heap if it needs it.

If you're familiar with the HEAPWALK utility included with the Software
Development Kit, you can spend lots of time attempting to reconcile the
value displayed by FREEMEM with value displayed by HEAPWALK. It's definitely
not obvious. In general, you'll find that GlobalCompact returns
approximately the size of the large chunk of free memory in the middle of
the memory space maintained by Microsoft Windows, plus some discardable
segments above that block of free memory. However, it stops short of the
discardable code segment that contains FREEMEM's code. Obviously,
GlobalCompact cannot discard FREEMEM's code if FREEMEM is calling the
function.


Drawing the Icon

In FREEMEM, the section of WndProc that handles WM_TIMER messages does not
itself update the icon display. Instead, the line

  InvalidateRect (hWnd, NULL, TRUE) ;

notifies Windows that the contents of FREEMEM's window are now invalid and
must be repainted. The InvalidateRect function causes Windows to put a
WM_PAINT message in the message queue. WndProc actually updates the window
only when it receives this WM_PAINT message.

There are alternatives to this method. Instead of calling InvalidateRect,
WM_TIMER can update the window directly. It would need to retrieve a
display context with the statement

  hDC = GetDC (hWnd) ;

and would then call GetClientRect and DrawText, just as in the WM_PAINT
logic, but using hDC as the display context rather than ps.hdc. WM_TIMER
would finally release the display context with

  ReleaseDC (hWnd, hDC) ;

While this is certainly valid, I chose not to do it this way.

WndProc must also process WM_PAINT messages which occur for reasons other
than an updated free memory value. For instance, if you use the mouse to
move the FREEMEM icon around the screen, Windows will eventually send
FREEMEM a WM_PAINT message so that FREEMEM can repaint the icon.

So, we'd either have to duplicate the paint code or move it to a separate
subroutine. The InvalidateRect call really does this for us by generating
the WM_PAINT message. It allows us to use the same paint code for all
painting jobs.

WM_PAINT messages are considered low priority by Microsoft Windows. They
always go to the back of the message queue and are fetched from the queue
only when no other message is present. There is a way around this, however.
After calling InvalidateRect, WM_TIMER could then call

  UpdateWindow (hWnd) ;

just as we did in WinMain. This instructs Windows to call WndProc directly
with a WM_PAINT message, without going through the message queue.

But that's really not necessary here. I wanted to keep FREEMEM operating as
a low-priority task. If something else was going on in another application,
I didn't want FREEMEM to hog time continually by repainting the icon.

The DrawText function used by FREEMEM when processing the WM_PAINT message
is very convenient for simple word-wrapped text. Note that the line within
the DrawText code that reads

  strlen (strcat (itoa (mem, buffer, 10), "K Free"))

does the same thing as the more-common construction

  sprintf (buffer, "%dK Free", mem)

However, sprintf is a big function. By using strlen, strcat, and itoa
instead, we can reduce the size of FREEMEM.EXE by about 3K──after all,
every little bit helps.


Creating FREEMEM

Aside from the FREEMEM.C source code, you need two other files to create
FREEMEM. The first is the module definition file, which is called
FREEMEM.DEF (see Figure 4). The FREEMEM.DEF file contains the standard
information that you'll see in definition files for most small Windows
applications.

The WINSTUB.EXE program specified in the STUB line of FREEMEM.DEF is
included with the Software Development Kit. The program runs when you
execute FREEMEM outside of Windows. It simply displays the message "This
program requires Microsoft Windows."

If you want to, you can create a normal MS-DOS program that displays a free
memory value comparable to the value that CHKDSK calculates. If you decide
to name this normal MS-DOS program DOSFREE.EXE, for instance, you can
specify

  STUB 'DOSFREE.EXE'

in the FREEMEM.DEF file. This little trick permits FREEMEM the use of both
on the MS-DOS command level and inside Windows. FREEMEM.EXE would thus
contain two related, but quite dissimilar, programs in one file.

The FREEMEM "make-file" (which is called simply FREEMEM, without an
extension) is shown in Figure 5. When you execute

  MAKE FREEMEM

the FREEMEM.C source code will be compiled and linked with the appropriate
Windows and C libraries, along with additional information from the
FREEMEM.DEF module definition file.


No Resource Script?

Unlike many sample Windows applications, FREEMEM has no resource script
file, which is a file with the extension .RC. FREEMEM doesn't need one.
FREEMEM has no menu, no pictorial icon, and no dialog boxes. The only use we
would have for a resource script is for storing text strings, but the only
text string FREEMEM uses is the one with the word "Free" in it.

Of course, for longer programs, the use of the resource script for all text
strings is highly recommended since it will eventually make translation of
the program into another language easier. But to tell the truth, loading
strings stored as resources is a real nuisance for small programs. FREEMEM
is short enough that translation is not a big problem, and we'd risk being
meretricious by using a resource script for that single text string.


Uses in Development

I like to keep FREEMEM loaded and running in normal Microsoft Windows use
just to give me an idea of how close Windows is getting to a low-memory
situation. But it has more value in program development.

If you're developing a Windows application, keep a watch on the FREEMEM icon
during testing. If the free memory size keeps shrinking, you may be
neglecting to free up some allocated memory within the program. You may then
want to call on the HEAPWALK utility, also known as Luke Heapwalker, for
some assistance in tracking down those orphaned memory blocks.

However, don't assume that FREEMEM will display identical values before an
application is run and after it terminates. The complexity of Windows'
memory management makes this unlikely for most large programs.


Figure 1:  WinMain Function of FREEMEM.C

The first half of FREEMEM.C contains the WinMain function, which performs
necessary initialization and contains the message loop.

/*  FreeMem.C -- Windows application that displays free memory */

#include <windows.h>    /* all Windows functions */
#include <stdlib.h>     /* itoa */
#include <string.h>     /* strcat & strlen */

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 [] = "FreeMem" ;
    WNDCLASS WndClass ;
    HWND    hWnd ;
    MSG     msg ;

    if (hPrevInstance)
        return FALSE ;

    WndClass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
    WndClass.hIcon         = NULL ;
    WndClass.cbClsExtra    = 0 ;
    WndClass.cbWndExtra    = 0 ;
    WndClass.lpszMenuName  = NULL ;
    WndClass.lpszClassName = (LPSTR) szAppName ;
    WndClass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
    WndClass.hInstance     = hInstance ;
    WndClass.style         = CS_HREDRAW | CS_VREDRAW;
    WndClass.lpfnWndProc   = WndProc ;

    if (!RegisterClass ((LPWNDCLASS) &WndClass))
        return FALSE ;

    hWnd = CreateWindow ((LPSTR) szAppName,
            (LPSTR) szAppName,
            WS_TILEDWINDOW,
            0, 0, 0, 0,
            (HWND)   NULL,
            (HMENU)  NULL,
            (HANDLE) hInstance,
            (LPSTR)  NULL) ;

    if (!SetTimer (hWnd, 1, 1000, NULL))
        return FALSE ;

    ShowWindow (hWnd, SHOW_ICONWINDOW) ;
    UpdateWindow (hWnd) ;

    while (GetMessage ((LPMSG) &msg, NULL, 0, 0))
        {
        TranslateMessage ((LPMSG) &msg) ;
        DispatchMessage ((LPMSG) &msg) ;
        }
    return (int) msg.wParam ;
    }


Figure 2:  WindProc Function of FREEMEM.C

long FAR PASCAL WndProc (hWnd, message, wParam, lParam)
    HWND    hWnd ;
    unsigned message ;
    WORD    wParam ;
    LONG    lParam ;
    {
    static  int mem, lastmem ;
    char    buffer [20] ;
    PAINTSTRUCT ps ;
    RECT    rect ;

    switch (message)
        {
        case WM_TIMER:
            mem = (int) (GlobalCompact (0L) / 1024) ;
            if (mem != lastmem)
                InvalidateRect (hWnd, NULL, TRUE) ;
            lastmem = mem ;
            break ;

        case WM_PAINT:
            BeginPaint (hWnd, (LPPAINTSTRUCT) &ps) ;
            GetClientRect (hWnd, (LPRECT) &rect) ;
            DrawText (ps.hdc, (LPSTR) buffer,
                strlen (strcat (itoa (mem, buffer, 10), "K Free")),
                (LPRECT) &rect, DT_WORDBREAK) ;
            EndPaint (hWnd, (LPPAINTSTRUCT) &ps) ;
            break ;

        case WM_DESTROY:
            KillTimer (hWnd, 1) ;
            PostQuitMessage (0) ;
            break ;

        default:
            return DefWindowProc (hWnd, message, wParam, lParam) ;
        }
    return (long) 0 ;
    }


Figure 3:  Alternate SetTimer Logic

This code causes FREEMEM to tell you why it must terminate when SetTimer
returns zero.

if (!SetTimer (hWnd, 1, 1000, NULL))
     {
     MessageBox (hWnd, "Hey! Too many timers!", NULL, MB_OK) ;
     return FALSE ;
     }


Figure 4:  Module Definition File FREEMEM.DEF

The module definition file FREEMEM.DEF is used by LINK4 for information
about the program not implied by the source code.

NAME    FreeMem

DESCRIPTION 'Free Memory Display by Charles Petzold'

STUB    'WINSTUB.EXE'

CODE    MOVEABLE
DATA    MOVEABLE MULTIPLE

HEAPSIZE  1024
STACKSIZE 4096

EXPORTS
    WndProc @1


Figure 5:  FREEMEM Make-File

This file, called simply FREEMEM, without an extension, is the "make-file"
for generating FREEMEM.EXE from the source code.

# Make file for FREEMEM -- assumes Microsoft C 4.0
# ------------------------------------------------

freemem.obj : freemem.c
    cl -c -d -Gsw -Os -W2 -Zdp freemem.c

freemem.exe : freemem.obj freemem.def
    link4 freemem, /align:16, /map/line, slibw, freemem
    mapsym freemem

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

CodeView:Debugging Philosophy And Step-by-Step Technique

David Norris and Michael J. O'Leary

Most MS-DOS programmers at one time or another have had to fire up the MS-
DOS debugger DEBUG to patch another program or peek into memory. Some have
actually debugged programs with it.

All kidding aside, DEBUG has its uses, but it is usually woefully inadequate
for doing any serious debugging, as shown by the many MS-DOS debuggers now
on the market──we've counted more than a dozen.

While debuggers are second only to editors as a computer "religious" issue,
arguments often tend to revolve around user-interface issues or "my debugger
has more features than your debugger" comparisons. Yet these are secondary
to the main consideration: providing facilities that lend themselves well to
established debugging techniques. If DEBUG fails, it is because it has no
commands that adapt themselves to user queries such as "what is the value of
iter?" and no commands like "stop when iter reaches 10."

During the design of the Microsoft CodeView(TM) debugger, we surveyed these
debugging techniques and created a requirements document for the "ultimate"
debugger. By iterating these techniques and requirements, we hope to give
you a fuller understanding of the complex world of debugging and aid the
future evaluation of debugging tools, thus providing a debugger yardstick.
We'll also introduce CodeView's philosophy and evolution, demonstrating its
strengths and weaknesses with examples. Our goal is not to teach CodeView
commands and syntax (that's what the manual is for), but rather to give you
an understanding of the concepts behind the commands.


Techniques

The different debugging techniques can be categorized into two major groups:
internal and external. External debugging implies an inspection of some
sort, without specific interaction with the program being debugged. Internal
debugging, on the other hand, requires the use of debugging tools that
exercise control over the debugged program's environment.

External debugging via code reviews by peers can uncover almost three-
quarters of all software bugs for some kinds of errors. While the old adage
about an ounce of prevention applies here, it has been our experience that
scheduling peer code reviews usually plays second fiddle to meeting other
product deadlines.

Another external technique is simple post-mortem debugging. What did the
program output? What should it have output? Simple formatting errors are
usually found this way.

Embedding debugging aids directly in the program is an anachronism from a
time when adequate debugging aids were not available. In this case, the
programmer decides where in the code he wants something done, such as
testing an assertion (is this pointer valid?) and conditional compilation.

None of these external methods requires any specialized debugging tools. On
the other hand, internal debugging techniques involve the use of tools that
give the user varying degrees of control over his program as it executes.
This usually means a debugger, but includes a class of code-flow analysis
tools such as profilers, event monitors, and path generators. A debugger can
be internal to the program or it can be separate. Generally, the latter
approach is used, as it is more desirable to debug the target program by
itself, with minimal interference from the debugger.


Requirements

Let's examine some of the requirements of a debugger. This list contains
general ideas about debugging, not specific commands.

Size and Speed Efficiency: Obviously, you want a debugger to take up as
little space as possible (especially when memory space is at a premium) and
to be as fast as possible.

Minimum impact on program being debugged: The debugger should not affect the
behavior of the program it is debugging. As some have discovered (especially
on unprotected systems, such as MS-DOS), the program may crash by itself but
run fine when being debugged. This has been called the Heisenberg
uncertainty principle when applied to debugging──the result being a
"Heisenbug."

Execution Control: The user should be provided with a wide range of commands
for controlling the execution of his program, mostly in the form of "trap on
x." Figure 1 lists the possible event traps.

State Manipulation: Users of debuggers need to be able to meaningfully
examine the state of a running, suspended, or dead program, including the
determination of the cause of suspension, procedure call history,
examination and alteration of code and data in its source form, and
execution history.

User interface: The debugger should be invisible to the user. The use of the
debugger should be so comfortable and intuitive that users feel they are
"in" the program they are debugging. It should be like driving a car──you
don't drive the car around the corner, you just go around the corner.

Portability: Programmers have met with various degrees of success in making
debuggers portable. Still, it's always nice not to have to learn a new
debugger when you move to a new computer system.


CodeView Philosophy

Microsoft needed a unified debugging tool strategy. The SYMDEB debugger, a
symbolic extension of the MS-DOS DEBUG debugger, was partially able to
fulfill the need for language independence, but attempts to make it portable
or to separate user-interface code from system-dependent code would have
been extremely difficult. We realized an entirely new debugger was needed.

We knew we wanted the debugger to be language-independent; one hallmark of
the Microsoft language series is that it is object-compatible. Therefore, a
Microsoft debugger should be able to debug whatever programs users could
come up with. We also needed the debugger to have a portable user interface
and system-dependent code so that an SDB-compatible debugger could be
created for the XENIX operating system. (See "ISLAND Design" diagram below)

Above all, we wanted a user interface that everyone could be comfortable
with. This proved to be the most difficult design task, as every individual
had his own idea about user interfaces. We went through many iterations of
the basic screen snapshots before we arrived at the present version.

The main problem was presenting the proper information to the user given the
limited amount of screen real estate. Thus, there are only two horizontal
border bars on the basic screen (the original screen had borders around
every window). In fact, the second horizontal bar, called the dialog bar,
can be eliminated completely, allowing 22 lines of source information on the
screen, and more on the EGA. The registers were placed to minimize the
impact on the source window. Also, a pull-down menu system was used because
it is a friendly, intuitive interface, and it conserves screen space when it
is not used.

Finally, we decided that rather than creating a new debugger syntax for
people to learn, we would adhere to the syntax of the SYMDEB and DEBUG
debuggers in the dialog window. This caused, and still causes, confusion for
a number of reasons──mainly because the default radix can change. Our
difficulty was that while we wanted compatibility with DEBUG commands
(default radix of hex), we also wanted to use an expression evaluator that
mirrored C as closely as possible (default decimal radix). To complicate
matters, the default radix doesn't affect the dump command, which has its
own set of types (byte, word, unsigned, etc.). We felt that programmers who
dealt strictly in C might never use the lower-level commands and that
assembly-language programmers wouldn't be using C expressions.


Introducing CodeView

CodeView is currently only available with the Microsoft C Compiler,
Version 4.00 and Microsoft FORTRAN Optimizing Compiler, Version 4.0,
although versions are planned for other languages and operating systems.
After a brief overview of its features we'll go into detail on a few
specific functions in an actual debugging session.

A sample CodeView screen is shown in Figure 3☼. Four windows are available
for display of information: the source window, the dialog window, the
register window, and the watch window.

The central and most important window is the source window and can be
thought of as a read-only editor attached to debugging functions. For
example, the user can scroll and search through source files, and the
current line and active breakpoints are shown by a background blue bar and
intense video that are imposed on the source text. Navigation through the
program, which can be composed of more than one source file, is possible
through a number of commands. The user can locate a program label, and the
source window loads and displays the source file containing that label.

The dialog window was our answer to commands that did not lend themselves to
a simpler windowing scheme, such as variable-length data dumps. The dialog
window can be thought of as the "glass-teletype" interface to CodeView;
indeed, a CodeView switch (/T) forces CodeView to act as its predecessor
SYMDEB did, in that a nonwindowing user interface is presented. The dialog
window views only a portion of dialog text; scrolling commands that are
similar to those in the source window can be used to examine data.

The register window simply presents the machine registers. The only command
available here is the ability to alter a flag value by clicking on it with
the mouse.

Finally, the watch window is available for viewing variables and data of
interest to the user, so that it doesn't have to typed over and over. The
format of the watch window entries is identical to the way they would be
displayed in the dialog window; also, the command syntax for variable/data
display between these two windows is orthogonal. For example, "?i" displays
the variable i in the dialog window, while "w?i" displays it in the watch
window.

The pull-down menu was designed to mimic the Windows pull-down menu system,
including the mouse and the keyboard.

Let's look at a debugging session with CodeView in order to demonstrate some
debugging concepts.


Debugging WHERE

WHERE is a utility program written in C (see Figure 2) and compiled with the
Microsoft C 4.00 compiler, using the -Zi switch to enable CodeView
information. It takes as arguments one or more program names and tries to
find them as MS-DOS would try to execute them. Thus, the tool is useful in
finding out whether the command "foo" would mean "C:\BIN\FOO.EXE" or
"E:\FOO.BAT."

Knowing that Real Programmers get it right the first time, we'll run WHERE
and hope it doesn't trash our hard disk.

  A>WHERE WHERE.EXE
  WHERE.EXE 6224 10-30-86 11:23a

Let's verify our program's output by using DIR:

  A>DIR WHERE.EXE
  WHERE.EXE 6224 10-30-86 11:23p

Obviously, a.m./p.m. is incorrect. A quick glance at the printstats routine
verifies that there is a typo; "<=" should have been ">=". Again, since
we're Real Programmers, we'll remember to fix the bug later and forget the
number of times we have forgotten to fix the bug later.

But we haven't fully exercised WHERE. It should be able to find ambiguous
names such as "DIR," and perform the name search in the same manner that MS-
DOS would: .COM, .EXE, then .BAT. Let's try a simple case:

  A>WHERE WHERE
  A>

WHERE can't find itself. How about an ambiguous or unambiguous filename
somewhere in the path (but not in the current directory)?

  A>WHERE COMMAND
  A>
  A>WHERE COMMAND.COM
  A>

It looks like we have two bugs; we can try to find them at the same time.
Let's debug:

  A>CV WHERE COMMAND

Figure 4☼ shows the initial CodeView screen. The register window is initially
off when debugging C programs, and the watch window is initially empty.
Typing "t" or clicking on the menu item "Trace!" with the mouse causes a
blue highlighted line to appear in the source window on line 17, indicating
the current instruction. Tracing one more time makes line 23 the current
instruction.

You're probably wondering why the current instruction started one line after
"main" and why it skipped 5 lines. The C Compiler will only output line
numbers for those lines that contain executable code. Lines 18 through 22
are declarations that produce no code, while the declaration in line 23 has
an assignment. We can show the relationship between source lines and emitted
code (see Figure 5☼) by typing "u main", which instructs CodeView to display
both source-code and assembly-language instructions in the source window.
Type F3 to return CodeView to source-only mode.


Single-stepping

As the program is so brief, let's single-step through the program and see
what happens. We can put variables of interest in the watch window so we
don't have to retype them every time we want to see their values. The first
interesting line is line 32; we can execute up to that line by typing "g
.32" or by clicking the right mouse button on that line. Single-step by
pressing the F10 key. To see the result of the getenv function, type
"?envptr". CodeView displays the value 17552:4636; CodeView evaluated
"envptr" as a C expression and so returned the value of the pointer (an
address). The value of the expressions "&envptr", "envptr", and "*envptr"
are different, and CodeView evaluates them properly. We are interested in
looking at the null-terminated string pointed to by envptr, so our
expression should be

  >w?envptr,s

to put the string in the watch window (see Figure 6☼). Pointers and pointer
expressions in C can be difficult to learn. Inexperienced users may find
CodeView confusing in this respect, but no more so than writing in C itself;
CodeView is a great learning tool for this purpose.

The next two lines alter the variables ptr and namebuf, so let's trace the
code and put the variables in the watch window. This can be done all in one
line by separating the commands with semicolons:

  >t 2;w?ptr,s;w?namebuf,s

The current line will execute the search_for_file function. We can use the
"binary search," or divide-and-conquer, approach to debugging by stepping
over the function using the Pstep command:

  >p

The screen now looks like Figure 7☼. Note that both envptr and ptr are
incorrect. The search_for_file function should not have been able to alter
these variables, which are local to main. What we need is a way to stop
execution when the variables change. CodeView accommodates us with the
watchpoint and tracepoint commands.

We don't know whether the string pointed to by envptr or envptr itself
changed, so we need to trap on both conditions. First, we can reset the
state of the program to just before the occurrence of the bug by typing

  >l;g .40

To stop execution when envptr itself changes, we can use the watchpoint
command:

  >wp?main.envptr!=4636

We had to specify the function to which envptr belonged since we would be
accessing the variable outside its normal scope (that of main).
Alternatively, we could have used the Tracepoint command and typed "tpw
envptr". In either case, CodeView is somewhat deficient because the user has
to know either the initial value of envptr or the size of the pointer. We
can break on the second condition by typing

  >tpb *envptr

This command causes a trap when the first character pointed to by envptr
changes. Now you continue by clicking on "Go!".

After a couple of seconds──CodeView watchpoints and tracepoints can be
quite slow, as they are emulated in software, although CodeView can utilize
debugging hardware──the CodeView screen returns at line 82. The code just
executed was a call to the strcat library routine. Before blaming the run-
time library, though, let's take a look at its return value in fullpath:

  >?fullpath,s

Oops (see Figure 8☼). The search_for_file function has been appending file
extensions via strcat ad nauseum; we are overrunning the allocated length of
the namebuf array and destroying the contents of other local variables of
main. We know that envptr is being changed; unfortunately, the Microsoft C
Compiler did not order main's local variables in the order given. In this
case they are ordered envptr, ptr, found, namebuf, envpath, then
pathbuf──completely different from the declaration order.

The bug is easily fixed by removing the previous file extension using the
strchr function just before the end of the file extension loop:

  *strchr(fullpath, '.') = '\0';


Summary

CodeView is not new. Many of its commands and features have already been
seen in other debuggers. What is new about CodeView is the number of
commands and degree of integration that it has provided. What is most
important is that programmers have a full understanding of the nature of
debugging and how they can best utilize the language and debugging tools
available to them.


ISLAND Design

The ISLAND source code is divided into three major sections: user interface,
core code, and system-dependent routines. By linking the core code with a
given user interface and system-dependent code, a variety of debuggers can
be created.

                                      ┌──────────────────────────┐
                                      │         CodeView         │
            User Interface ►      ┌───┴──────────────────────┬───┘
                                  │            SDB           │
                              ┌───┴──────────────────────┬───┘
                              │                          │
                              │                          │
                 Core Code ►  │          ISLAND          │
                              │                          │
                              │                          │
                              ├──────────────────────────┤
                              │       DOS 2.x/3.x        │
                              └───┬──────────────────────┴───┐
                                  │         XENIX-286        │
          System Dependent ►      └───┬──────────────────────┴───┐
                                      │        XENIX-386         │
                                      └───┬──────────────────────┴───┐
                                          │          Others          │
                                          └──────────────────────────┘


Figure 1:  Debugger Event Traps

Before/After execution of a specific instruction
Before/After successful branch
Single-step
Data/Code Read/Write
Procedure and Function Prologue/Epilogue
Cross-Process


Figure 2:  WHERE Utility Program

     /* WHERE - shows where DOS is finding your executable */
#include <stdio.h>
#include <time.h>
#include <sys\types.h>
#include <sys\stat.h>
#define NUMEXTS     3
#define MAXFILELEN   12
#define MAXPATHLEN   65
struct stat file_stat;
char *getenv(char *);
char *strchr();
main (argc, argv)
int argc;
char **argv;
{
 int found;
 char pathbuf[MAXPATHLEN];
 char namebuf[MAXFILELEN];
 char *ptr;
 char *envpath;
 char *envptr;
  if (argc < 2) {
    printf("Usage: where filename[.ext]\n");
    exit(2);
  }
  if (!(envptr = getenv("PATH")))
    envptr = "";
  /* Loop thru filenames given */
  while (ptr = *++argv) {
    strcpy(namebuf, ptr);
    /* Try current directory first */
    if (!(found = search_for_file(namebuf, strchr(namebuf, '.')))) {
      /* Loop thru paths in path environment variable */
      envpath = envptr;
      while (*envpath) {
        ptr = pathbuf;
        /* Copy path into buffer */
        while (*envpath && *envpath != ';')
          *ptr++ = *envpath++;
        /* skip path separators */
        while (*envpath == ';')
          ++envpath;
        /* Don't append a '\' if looking thru a root directory */
        if (*(ptr-1) != '\\')
          *ptr++ = '\\';
        *ptr = '\0';
        strcat(pathbuf, namebuf);
        if (search_for_file(pathbuf, strchr(namebuf, '.'))) {
          printstats(pathbuf);
          break;
        }
        }
      }
    else printstats(namebuf);
    }
  exit(found == 0);
}
search_for_file (fullpath, specific_file)
char *fullpath;
int specific_file;
{
 int found, next_ext;
 static char *extension[NUMEXTS] = {".COM", ".EXE", ".BAT"};
  if (specific_file)
    found = !stat(fullpath, &file_stat);
  else {
     for (next_ext = 0; next_ext < NUMEXTS; ++next_ext) {
        /* try .com .exe .bat extensions */
        strcat(fullpath, extension[next_ext]);
        if (found = !stat(fullpath, &file_stat))
          break;
       }
     }
  return(found);
}
/*
 * print out fullname from passed string, and stats (size, date..)
 * from global struct file_stat.
 */
printstats (fullname)
char *fullname;
{
  struct tm *tmptr;
  tmptr = localtime(&file_stat.st_atime);
  printf("%s \t%ld\t%2d-%2d-%2d\t%d:%02d%c\n",
      strupr(fullname),
      file_stat.st_size,
      tmptr->tm_mon+1,
      tmptr->tm_mday,
      tmptr->tm_year,
      (tmptr->tm_hour < 13 ? tmptr->tm_hour : tmptr->tm_hour-12),
      tmptr->tm_min,
      (tmptr->tm_hour <= 12 ? 'p' : 'a'));
}

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

Page Description Languages: High-Level Languages for Printer Independence

───────────────────────────────────────────────────────────────────────────
Also see the following illustrations of Page Description Languages:
  Adobe System's PostScript
  A Comparison of Three Page Description Languages
───────────────────────────────────────────────────────────────────────────

Steve Rosenthal☼

As printers get steadily smarter, more and more of them are supporting
command languages similarly enhanced in power and flexibility. In
particular, page description languages (PDLs) are gaining popularity as a
preferred way of telling printers how to put images on paper. More than just
a simple list of what elements to be printed next, PDLs offer a precise and
formalized means of controlling printer output. PDL language statements,
which are normally created and sent to the output device invisibly to the
end user, can include a combination of operations, objects, dot locations,
and references to typographic elements such as fonts.

You can, in fact, think of PDLs as high-level languages for printer output,
compared to the machine language or assembly language approaches that have
until recently been the rule. Not surprisingly, many of the advantages and
disadvantages of using a PDL parallel those for using a high-level language
in other types of programming, and many of the arguments about which PDL is
best echo similar discussions about the relative virtues of computationally-
oriented programming languages.

Unfortunately, you can't run out and get all those promised advantages on
very many devices or from many programs just yet. Of the three major page
description languages that are likely to become major forces in the personal
computer market in the immediate future──PostScript, Interpress, and
Document Description Language (DDL)──only PostScript and Interpress have
been implemented on any production output devices so far, and only
PostScript has been implemented on hardware and software for personal and
desktop computers. So for now, we'll have to settle for an overall
discussion of why PDLs might be attractive, what's needed in a PDL and its
implementation, and a description of the three major languages that appear
to be serious market contenders.


Device Independence

Backers of PDLs say that the foremost advantage we can expect from them is
device independence. Theoretically, an application or system can produce one
single set of statements in a page language for output on any output device
that supports that page language, rather than having to produce a different
version for each brand, resolution level, and technology of output.

Using a PDL is like using a single language, such as BASIC, that works on a
wide variety of different machines. Even though the internal operation of
the various printing devices may be different, that's taken care of during
the interpretation of the PDL statements by each individual printer. From
the outside, each device seems to be logically equivalent.

Yet having a single logical connection between programs and printers affects
interfacing in a dramatic way: the number of drivers that must be written is
reduced to the sum of the programs and printers at issue rather than their
product. Furthermore, an application written to PDL standards continues to
support new devices that were not available at the time that the software
was created──a feat that is at best difficult with more-traditional
approaches.

While that's the theory, in practice the results aren't quite as pristine.
In all the announced PDLs, there are some device dependencies that sometimes
must be considered, just as there often are in programming BASIC or Pascal.
You can usually take a PDL file meant for one device and output it on
another, but you may have to clean up a few differences or accept some
slight artifacts of the retargeting process.

Furthermore, every page description language may not be optimized for all
new print technologies, so future compatibility cannot really be totally
guaranteed. Given that most users replace computer equipment when the
available alternative becomes economically more worthwhile, not when the
previous generation fails to work any longer, inefficiency can be as much of
a limiting factor as inability.

Ease of connectability could also vanish as a major advantage unless a
single page description language, or at least a small set of languages,
becomes the de facto standard. Right now, several different PDLs are
competing for market favor, along with several typographic description
languages that take a more character-oriented and smaller-region view of
the page.

In addition, all of these languages compete against the virtual device
approach, which has all applications produce their output as a series of
calls to functions on a theoretical device. The environment then translates
these calls to the actual operations needed by each printer. This last
method, by the way, is the one implemented in most operating environments,
such as Microsoft's own Windows.

Still, given all these cautions, all it takes to convince most people that
the PDL approach to universality is worthwhile is one typesetting project on
the Macintosh. Because many applications on the Mac (including Microsoft
Word and Microsoft Works) can produce output in PostScript, the same files
can be run off first inexpensively on the Apple LaserWriter, then, when
proofed and ready, can be run in finished form on a true high-quality
typesetting machine from Allied Linotype.


Low Overhead

Economy of description is the second major advantage that page description
languages offer. In all current PDLs, an application can describe intended
output, where appropriate, as a series of objects rather than characters or
individual dots and lines. An object-oriented description often takes much
less time to communicate, as well as less memory buffer space and other
system resources.

For example, if an application wants to draw a circle in the output with a
PDL, it generally orders up a circle object of a certain center and radius,
with the outline done in the current line width setting. In contrast, if it
wants to draw a circle with ordinary dot graphics, the application has to
send every point on the circumference to the printer.

The same idea applies to boxes, lines, shading, and even typographic fonts.
Because the PDL-equipped printer starts with a large store of knowledge
about elements of graphics and type, an application usually needs to send
only the pointers rather than the objects themselves.

In most PDLs, fonts are considered a special class of graphics. All the
regular PDL commands that apply to graphics──such as those governing
movement, rotation, enlargement, tint, and so on──apply to typographic
characters as well. In addition, all PDLs have font-handling commands that
take into account the special nature of type.

On the flip side, being able to work with regular objects is no help at all
for photographic information, scanned images, or other graphic elements
that, unlike line drawings, have no simple tonal structure. In fact, if a
PDL has to communicate such a graphic as a series of dots that require a lot
of overhead for their expression, it can take the PDL more time and space to
communicate this type of picture than would a simpler approach.

Division of labor is another benefit that PDLs offer. Since page languages
describe output in terms of objects, an originating system that contains
those objects internally does not have to translate from object to ink
dot──a process called rasterizing. On most printer technologies, rasterizing
means translating an object into a series of on/off dots placed in a raster
pattern of successive lines sweeping across the page. Rasterizing is also a
necessary step in printing various sizes, styles, and weights of type.

However, rasterizing is computationally intensive, and──if the page creation
sequence and timing are to be decoupled from the printing technology──it
needs at least one bit of memory for every possible dot placed on the page.
Using a PDL permits the main processor to generate the prerasterized image;
the printer can then rasterize and store the resulting bit map.

PDLs that are full programming languages can also let the printer do some of
the computations that precede rasterization. Since PDL statements can
describe operations as well as objects, programs have the ability to send
complex expressions to the output device; these expressions, once they are
reduced to simpler form, will generate either the described object or the
parameters of the described object. The output device will then be able to
do the computations while the main processor proceeds with other work.


Three Main Ingredients

Basically, you need three components to implement a PDL in some workable
form.

The first component is the quality of language itself, which allows it to
meet a number of criteria pertaining to function. For example, a page
description language must be clearly and completely defined if it is to be
used as a consistent standard across multiple machines and systems. It must
be sufficiently fast and efficient to make the overhead involved in using it
affordable. If it isn't, developers will simply bypass the PDL to write
native code drivers for each device.

A workable PDL should also have some sort of hierarchical or layered
structure. Although not every printer has every feature, users who buy the
more capable devices want to put any extra functionality to use; hence a
good PDL must be able to take care of most business with a set of core
commands and functions, yet still retain the potential to make use of
additional capabilities.

To be device independent, a PDL must also maintain a universal coordinate
system visible to the applications program that is translating the points to
the printer's actual coordinate system for output. The coordinate system
must be sufficiently large to cover the maximum defined page size at the
maximum workable resolution of any supported output device.

A successful PDL must also be perceived as acceptable. The primary value of
any such language depends in large part on its pervasiveness, so any
language must appeal to a large number of software developers and output
device manufacturers. Products that are not seen as acceptable, no matter
how technically proficient, won't reach the critical mass needed to make
their use worthwhile.

In most cases, the bid for popularity has led to having the PDL languages
themselves placed into the public domain──that is, the actual verbs and
structures that comprise the language can be used by anyone, even though the
programs that create the code or translate it into printed images are
proprietary.


The Interpreter

The second component of the PDL system is the interpreter and printer
controller, which translate the PDL statements into the actual dot
information that the printer's marking engine needs to produce the output
image.

Translation is usually done on the fly by an interpreter (compiling usually
isn't worthwhile because documents frequently change between every
printing). Strictly speaking, the output of the interpretation process is
some result sent back to the host system, but its "side effect" is the
creation of the actual sequence of bits needed to control the printer
marking engine.

The major PDLs for the PC market are all based on threaded stack languages,
so the interpreters and languages all have a very Forth-like flavor. They
use reverse Polish notation (RPN) and store almost all their data and
working values on a push-down stack.

Because the computational power needed to make this translation quickly is
so great, most of the PDL-equipped printers are more high-powered than the
main system for which they are ostensibly peripherals.

They also need a lot of memory. All current implementations are for page
printers (where the complete page is translated and stored in dot form
before the actual marking process begins), and all expect to find a complete
bit map in which to store the resulting page image. For the current standard
of 300 dots per inch on an 8 1/2- by 11-inch page, that means slightly more
than a megabyte.

PDL-based printers also need a large ROM space or a great deal of extra
memory for downloading the interpreter. A typical PDL interpreter for a
laser or similar xerographic page printer takes up several hundred
kilobytes.

The third component, producing the translation code, is also a huge
challenge. Page description languages are intended as a form of
communication between program and printer and are not generally designed
for ease of human use. Instead, applications are expected to produce the
code to be sent to the printer without any direct human intervention.

The PDL code emitter that most applications require is somewhat more complex
than a standard printer driver, but still within the reach of most serious
programmers. For maximum efficiency, the application must specify its output
as certain types of objects, and there are similar incentives toward
handling type in more compatible ways.

It is possible to write your own PDL code emitter, and maybe a dozen or more
firms have already included such a facility in current software products.
The backers of most PDLs will also write the needed code sections on a
contract basis or will recommend a firm that does so.

A common code generator can also be shared by multiple applications that run
in a common operating environment using a virtual device interface. On the
Macintosh, for example, system and applications programs write to the
Quickdraw ROM routines, and a single code generator that is loaded as a
"printing resource" then changes the ROM calls to PostScript code.

Similarly, in Windows and Digital Research's GEM, programs that want to
write PostScript don't have to have their own embedded PostScript code. A
system driver turns virtual device interface calls into the code needed for
a PostScript-based printer.

Given all these requirements, it's not surprising that the major page
description languages exhibit many similarities. But in the case of these
three languages, the resemblance is more than coincidental. PostScript,
Interpress, and DDL are all outgrowths of work done at the Xerox Palo Alto
Research Center (PARC). So, although the exact syntax and precise list of
features characterizing each language have diverged because each language
was developed at a different corporate home, the overall spirit and approach
are very much the same.


PostScript

PostScript, from Adobe Corp., was the first page description language to be
implemented for personal computer software and peripherals and is still the
only one that is being delivered with actual commercial products. Although
its first widely available applications were on the Apple Macintosh and the
Apple LaserWriter printer, PostScript is now backed by scores of software
packages on various machines and operating systems, as well as close to a
half dozen or more different printers.

PostScript is in the public domain, and there is even a functionally
compatible language/interpreter set combination that can be mixed and
matched with PostScript sold by Control-C Software of Portland, Oregon.
Most implementations so far have been done by Adobe.

As a language, PostScript is particularly rich in general programming
capabilities as well as in graphic and typographic support elements; more
than 250 PostScript verbs cover arithmetic, logical, and control categories,
as well as graphics. PostScript is written entirely in ASCII (printable)
characters, making it easier to debug final code and to send output
descriptions across simple communications links. On the other hand, this
slows the transmission of nongeometric images, since they must be translated
from bit maps to hex representation and back to be sent as printable
characters.

PostScript supports outline (vector) fonts and has provision for both
built-in fonts and downloadable supplements. All fonts are defined as one
single point unit in height and are then scaled to any selected size. The
PostScript language definition supports color as well as shading, but so far
all the delivered devices have been monochrome. For creating halftone
(shaded) images, such as those found in photographs, PostScript includes a
number of facilities for creating small regular patterns.

PostScript applications can be written to handle documents of most any
length, but the language won't supply much built-in help for the more
complex projects. Formatting beyond the structure of a single page, not
included in the first language definition, is currently implemented as a
series of structured comment lines.

For debugging or constructing composite projects, PostScript contains a fair
number of file and input control statements. The AppleWriter PostScript
interpreter, for example, can be used interactively to let you improve
various formulations of your procedures.


Interpress

Interpress is Xerox Corp.'s nomination as a standard page description
language. So far, it has been implemented on several larger laser printers
and on many Xerox minicomputer applications as part of the Xerox Network
Architecture (XNS) system.

However, applications in the personal computer field should begin to appear
very shortly. Xerox's Ventura Publisher will support Interpress, as will
Microsoft Windows. Xerox says that several personal computer printer
manufacturers plan to announce Interpress support, starting in the first
quarter of 1987. The language itself is also in the public domain.

Compared with PostScript, Interpress has more facilities for controlling
overall document structure and distribution but slightly less power for
general computation. The language can be written in a Forth-like ASCII
representation, but the actual code sent to Interpress printers is a binary
representation. Bidirectional translators are available for debugging,
testing, and learning.

Using binary for Interpress files obviously saves space, but perhaps more
importantly, it cuts transmission time for complex documents. Because
Interpress was explicitly designed for use with networks, transmitting a
file more than once was expected to be the rule rather than the exception.

The most important document control feature is page independence. In an
Interpress output file, each page is dependent only on an initial header and
information local to that single page. This feature guarantees that printers
that actually image in some order other than first through last will produce
the right output, and many of the higher performance printers that do two-
sided copying or binding do indeed print documents in various orders.

Interpress also features an explicit mechanism for setting imaging priority.
On many output devices the order with which overlapping images are laid down
matters to the final result; on others, printing can be done faster by using
an order that may be different from the order in which the instructions
arrive. Hence, Interpress allows the user to specify strict sequencing when
needed.

As part of an overall network strategy, Interpress also links to a large
number of complementary output-related Xerox standards. Many of these
define a standard solution to issues that have yet to be addressed in the
DOS and Macintosh environments. That includes standards for font handling
and naming, encoding of scanned and other raster images, and character
coding for extended character sets.


DDL

Document Description Language (DDL) is Imagen Corp.'s entry into the
standard page description language derby. As the latest of the three
contenders, and as Imagen's second generation of page description languages,
it naturally combines features from the prior languages as well as adding
some innovations of its own.

DDL devices and programs will not be publicly available until sometime this
year, but agreements between Imagen, Hewlett-Packard, and Microsoft ensure
the language a substantial launch. It will be Hewlett-Packard's language of
choice for sophisticated applications speaking to the HP Laserjet printer
and will be supported as an output driver in Microsoft Windows.

DDL shares with Interpress an enhanced emphasis on overall document
structure as well as on the geometry of each page and perhaps goes even
further in this direction. It also makes more-extensive use of caching to
reduce the time required in repetitively translating objects from
description to dot form.

Document control includes page independence and an explicit mechanism for
specifying page order for output. The latter feature is particularly
important when multiple logical pages are imaged onto each physical page,
and the resulting sheets have to be folded and assembled into a complete
document.

Caching attempts to use all available memory, which is constantly getting
cheaper, to increase the speed of performance. Objects, including both fonts
and geometric shapes, are held in memory in translated form as long as there
is room; they can be reused if called again. That cuts down translation time
substantially for repetitive elements.

DDL files are transmitted in binary, which makes them more compact and
speedier to transmit than an ASCII representation. Compactness is
particularly important for scanned images and halftones (photographs), which
are normally sent as arrays of dots. For debugging purposes, DDL printers
will also accept an ASCII equivalent.

DDL can also support both bit-mapped and outline fonts. When scaling bit-
mapped fonts, which are made of lists of actual dot positions, the system
applies various typographic design rules to produce a more intelligent
result than does simple geometric multiplication. Besides the standard fonts
found on both computerized and traditional systems, Imagen has licensed
several special computer-oriented type families, including a face called
Lucida that its originators, Bigelow & Holmes, claim is the first designed
explicitly for electronic printing.

For times when exact dot placement is important, such as in tiny fonts or in
plotter-type output, DDL guarantees that if a target resolution is defined
before image creation, dot placement at that same output resolution will
follow the original exactly without any errors due to translation back and
forth into universal coordinates. Close positioning control will increase
greatly in importance when more color devices become available, especially
if they are able to intermix colors to provide a wide range of hues and
shades ("process color"). Tight dot positioning control also makes it
possible to guarantee a close level of correspondence between screen and
printed images.

Like PostScript, DDL includes a full complement of control structures and is
extensible, which makes it possible to write routines that handle both
common and special requirements. While a wide variety of programs can be
written in DDL, the expected use of this facility is for printer drivers or
output filters and formatters.


Making a Choice

Although end users normally choose a page description language only
implicitly through their choice of printers, developers and programmers face
a tougher dilemma. They can pick a single PDL and support it as their output
language of choice, support multiple PDLs, or rely on an operating
environment for PDL support.

The problem is made still more difficult by the rising expectations of end
users. While the first laser printers that supported graphics and
typographic-style fonts initially seemed sufficient, users are now asking
for higher performance, faster output, greater detail, and increased ease of
use.

The makers of PDLs recognize this and are all working on improvements and
refinements. But because the interpreters are normally implemented in ROM
firmware, updating a PDL is a major market headache. PDLs are not changed
very lightly.

Furthermore, optimizing the choice of language itself is not the total
solution. PDLs are part of a complex web that includes experienced
programmers, supporting applications, tools, and output devices. The
availability and popularity of resources and collateral material also
matter.

Which page description language is the best? Perhaps that question won't be
settled any more than which programming language should be everyone's
choice. At some point, it's not technology but theology.

The good news is that developers writing programs for Microsoft Windows do
not have to choose. Since Windows provides a device-independent interface, a
developer can write an application that can output to a device using a PDL
simply by having the appropriate device driver installed. In doing so,
Windows permits innovation in PDLs while maintaining a standard application
interface. A PostScript driver is already available, and DDL and Interpress
drivers are expected to be ready in the near future.

───────────────────────────────────────────────────────────────────────────
Adobe System's PostScript
───────────────────────────────────────────────────────────────────────────

The "B" cube on page 50 of the printed version was created from the
sample of Adobe's PostScript Language which follows.

  %! PS-Adobe- Adobe Systems Incorporated-Colophon 3 ShowPage Graphics 1986
  %% DocumentFonts: Palatino-Roman
  %% Title: B.cube.ps

  %  B.cube.ps produces a cube drawn with a character on each face.
  %  To change the font subtitute a chosen font for Palatino-Roman in the
  %  definition of "masterfont".

  /cube {
     /masterfont /Palatino-Roman findfont def

% Letter strings that allow you to assign particular letters to each cube
  face.
     /front exch def
     /leftside exch def
     /back exch def
     /rightside exch def
     /top exch def
     /bottom exch def

     % The height of the letter is reduced with respect to its angle. (stan)
     % Height is multiplied by sin/cos.
     % This helps for perspective.

     /stan {a sin a cos div sy mul} def
     /getfont {masterfont [sx 0 stan sy 0 0]
           makefont setfont}  def

     % type (s)ize and (a)ngle of oblique

     gsave
          /sy 100 def
          /sx 100 def
          /a 0A def
          getfont
          40 50 moveto
          .75 setgray
          back show
     grestore

     gsave
          /a 45 def
          /sy sy 2 sqrt div def
          /sx 70 def
          getfont
          0 0 moveto
          .60 setgray
          bottom show
     grestore

     gsave
          getfont
          70 0 moveto
          45 rotate
          .1 setgray
          rightside show
     grestore

     gsave
          getfont
          0 7 moveto
          45 rotate
          .40 setgray
          leftside show
     grestore

     gsave
          /a 0 def
          /sy 100 def
          /sx 100 def
          getfont
          0 0 moveto
          .20 setgray
          front show
     grestore

     gsave
          /sy sy 2 sqrt div def
          /a 45 def
          /sx 70 def
          getfont
          0 70 moveto
          top show
     grestore

  } def %cube

  %% EndProlog

  % To set cube:
  % gsave
  % x y translate
  % x y scale       ...no scale here gives a cube of 100 pts
  % (bottom) (top) (rightside) (back) (leftside) (front) cube
  % grestore

  gsave
  100 200 translate
  3 3 scale
  (B) (B) (B) (B) (B) (B) cube
  grestore

───────────────────────────────────────────────────────────────────────────
A Comparison of Three Page Description Languages
───────────────────────────────────────────────────────────────────────────

We asked three PDL vendors to create a sample output consisting of the words
"Microsoft Systems Journal" in a box.

  ■  Postscript Sample
  ■  DDL Sample
  ■  Interpress Sample

Postscript Sample

The PostScript programming from Adobe produces "Microsoft Systems Journal"
shown on page 53 of the printed version.(The misspelling of the corporate
name was due to creative license on the part of the programmers.)

!PS-Adobe-2.0
  %% Creator: pasteup
  %% CreationDate: Tue Dec 10 1986
  %% For: Microsoft Systems Journal
  %% Pages: 1
  %% DocumentFonts: Optima
  %% BeginProcSet: text_tools v1.0 revl.0
  /box {  % takes relativeX relativeY on stack, draws a box
      dup 0 rlineto
      0 3 -1 roll rlineto
      neg 0 rlineto
      closepath
      stroke
  } bind def
  % short names for frequently used PostScript operators:
  /s  /show load def
  /m  /moveto load def
  /S  /save load def
  /RS  { restore save } bind def
  /R  /restore load def
%% EndProcSet
%% END Prolog
%% Page: 1 1
  S
  100 100 moveto
  35 306 box
  RS
  120 110 m
  /Optima findfont 24 scalefont setfont
  (MicroSoft Systems Journal) show
  R
  showpage
%% Trailer


DDL Sample

The DDL output from Imagen Corporation appears on page 54 of the printed
version.

  Uopbind.ddl (this program uses the standard DDL include file "opbind.ddl")
  "opbind.ddl" (specify the name of a file for the binding of operators and
                constants)
  :40 (open the specified file and push its source descriptor code on the
       stack)
  \srcn ; 3C (assign a name to the source descriptor code of specified file)
  srcn :3 (interpret the contents of specified source file)
  srcn :42 (close the specified source file)

  ; define an operator to convert points to image units
  { 24 POINTSPERINCH / ImageMetrics 5 index * }
    \pointstounits =
  @S (end of preamble and the start of section 1)

  @Uhelvr (this program uses the font file Helvetica Regular)

  "helvr" \SymbolStyle = ; set the state variable SymbolStyle to the name of
                           the font file
  24 pointstounits \SymbolDesignSize = ; convert 24 points into image units

  128 array \CompositeMap = ; specify the size of the CompositeMap array

  33 \i = ; set the starting index for the Composite map array to 33
  95 {
          i symbol area\ CompositeMap i = ; store the graphic object for an

          i 1 + \i =  ; ASCII character and increment the array index
  } repeat

  SymbolDesignSize 1 + 3 / relx \ CompositeMap 32 =
     ;  calculate the width of the ASCII character SP (space)

  656 \Xvalue = ; set the starting X coordinate for printing specified words

  600 \Yvalue = ; set the starting Y coordinate for printing specified words

  Xvalue Yvalue absxy ! ; go to the coordinates Xvalue Yvalue

  "Microsoft Systems Journal" composite \text =
      ;  create a  composite object called text

  text ! ; print the words "Microsoft Systems Journal"

  text bbox ; calculate the smallest box that can enclose
              the specified three words

  \maxy = ; get the maximum Y coordinate of the bounding box from the DDL
            stack
  \miny = ; get the minimum Y coordinate of the bounding box from the DDL
            stack
  \maxx = ; get the maximum X coordinate of the bounding box from the DDL
            stack
  \minx = ; get the minimum X coordinate of the bounding box from the DDL
            stack

  maxy miny - \height = ; height of the bounding box
  maxx minx - \width =  ; width of the bounding box

  height 2 / \offset = ; calculate the offset from the starting position

  width height = \width = ; increase the box width by box height

  Xvalue minx + offset - \Xvalue = ; calculate the starting X and Y
                                       coordinates
  Yvalue miny + offset - \Yvalue = ; the printing of a box

  Xvalue Yvalue absxy ! ; go to the starting coordinates

  width height height + rectangle line !
    ; print a box twice as high as the bounding box

  endimage
  @E

Interpress Sample

The Interpress output from Xerox Corporation appears on page 57 of the
printed version.

  ──Date: 8-Dec-86  14:42:23  PST - Object file Name: MICROSOFT2.ip

  ── Object to Source Conversion parameters:
  ──    Object File Name: MICROSOFT2.ip,
  ──    Source File Name: MICROSOFT2.ial.
  ──    Op codes: Source Only.
  ──    File Results: SourceOnly,   Sequence Data:
  Decimal/ASCII.
  ──    Conversion: Normalized,     Items Per Line:
   Multiple.
  ──    Large Arrays: NotSupressed.

  Interpress/Xerox/2.2
  BEGIN
    {
    Identifier "Xerox" Identifier "XC1-1-1" Identifier
  "Modern" 3 MAKEVEC
    FINDFONT
    99.576 SCALE
    MODIFYFONT
    O FSET
    }

    {
    1/11811 SCALE
    CONCATT
    375 2950 SETXY
    O SETFONT
    String "Microsoft"
    SHOW
    40 SETXREL
    String "Systems"
    SHOW
    40 SETXREL
    String "Journal"
    SHOW
    5 15 ISET
    350 2900 350 3050 MASKVECTOR
    350 3050 1630 3050 MASKVECTOR
    1630 3050 1630 2900 MASKVECTOR
    1630 2900 350 2900 MASKVECTOR
    }

  END

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

DIAL 2.0 Provides Software Developers with Integrated Support System

Barbara Krasnoff☼

Microsoft's on-line DIAL 2.0 support service, which supplies assistance for
applications developers, has been recently upgraded and expanded. We asked
Sunny Baker, Director of Planning and Marketing, to explain the new
developments.


MSJ: What exactly is DIAL?

SUNNY BAKER: DIAL is an integrated set of on-line support services that use
the PC as the interface to our own software. There are three components to
DIAL. First, there is the on-line bulletin board system, a knowledge bank
that is one of the most sophisticated bulletin board systems in the entire
industry. It uses artificial intelligence techniques for keyword search
capabilities. It's very fast and powerful.

Second, we have an on-line Forum, similar to the ones you get through
CompuServe, especially for the ISV (independent software vendor) community.

Finally, we offer technical assistance through our TAR (technical assistance
request processing) system. We supply both on-line TAR processing and
callback for high-priority TARs in order to give people immediate assistance
over the telephone. DIAL also transfers both binary and ASCII files, and we
do furnish some DIAL software to subscribers.


MSJ: When did DIAL first go on-line?

BAKER: The first version went on-line at the end of 1984, and, quite
honestly, it was not very capable. It was basically a prototype version. At
that time, we limited access to DIAL to our OEM customers and some of the
specialized ISVs. It wasn't until last year that we really opened DIAL up to
the ISV community, and now we have a completely new version.

DIAL 2.0 is very similar to what people have been using from the interface
standpoint, but functionally and operationally, it's a completely new
product. For example, we used to run DIAL on very small machines because of
its prototype nature, and now we've moved the actual server for DIAL to a
full-sized VAX.

MSJ: What are some of the other differences between 2.0 and previous
versions?

BAKER: The Bulletin Board has been entirely restructured. It has keyword
search capabilities, it contains much more information, and the organization
and speed of the board are entirely different. The Forum is a totally new
feature, and transmission error-checking capabilities have been added as
well. The product now uses an ANSI driver rather than a VT52 driver, which
is something that our customers asked for. The VT52 was a Microsoft standard
that we used in-house, but nobody else uses it, so it was a big problem.

MSJ: What kind of service does DIAL contribute to the community? Why is
there a need for it?

BAKER: There have been complaints in the user community for some time that
Windows development has not been supported properly. DIAL is our effort to
supply the high level of support that people who are doing software
development need. Access to the Bulletin Board gives software developers
immediate information for any known problem. We summarize every TAR that's
presented to us from whatever source, and place it on the Bulletin Board,
giving developers and programmers everywhere full access to bug lists for
Microsoft products. There is immediate access to any new software updates;
it's the most efficient way to get new tools to users. We also offer example
software that they can download and then use in their own applications.

From our point of view, the Bulletin Board is the fastest and most efficient
way for subscribers to get support from Microsoft, since it contains answers
to every question that somebody has asked before. The Bulletin Board is so
quick that it's faster than calling somebody or sending in a technical
assistance request. We try to get subscribers to DIAL to really use the
Bulletin Board actively, especially when they first start developing
applications with a Microsoft product.


MSJ: Who is the typical DIAL subscriber?

BAKER: Right now, our typical users are software developers doing Windows
development, OEMs doing adaptations of both DOS and Windows to their own
machines, and anybody who uses a Microsoft systems product──especially to
create Windows applications. We are in the process of making DIAL available
to any person who does systems development using any Microsoft programming
language or programming tool kit, with emphasis on our most strategic
products: Windows and DOS.

MSJ: Approximately how many users do you have?

BAKER: We have about 1,200 users today; and we're getting about 120 new
users a month. Certainly we would like to penetrate the community even
deeper than that. Right now, we know we have about 6,000 Windows Toolkit
users out there; our goal is to have at least 75 percent of the active
program developers on-line. And there are thousands of C programmers out
there who are another target market for us. As our user base expands, the
Forums will get better, because there are more and more people exchanging
information.


MSJ: How much information does Microsoft post on the Bulletin Board?

BAKER: We delete things that are obsolete, but otherwise the Bulletin Board
is constantly expanding. We now post about 150 items a week. Our goal for
next month is to post 1,000 items, and our goal over the next 6-month period
is to get up to the point where we will be posting 7,000 items a month,
because we're going to support Microsoft's complete product line.

MSJ: The Bulletin Board contains information only from Microsoft. Is there
any on-line interaction between users?

BAKER: That's what the Forum is all about. We just started it in January,
and it looks like we're going to have a good quorum.

Our Forums are just like the forums on GENIE or CompuServe. Let's say that
somebody is looking for a local area network card. That person can ask the
community, "Hey, do any of you know a good local area network card?" DIAL
people can respond to the Forum and say, "Hey, I know so-and-so." Their
anonymity is maintained throughout the Forum.

Forum items that we think are noteworthy are also translated into Bulletin
Board items, so that everybody can have access to them.

The whole point is to make DIAL a one-stop support system that combines
everything a user basically needs as far as support from Microsoft, support
from his own community, and support from our knowledge base──all integrated
into one system.


MSJ: When does a subscriber use TAR?

BAKER: Any question that relates to the operation of a product or the
building of an application with a Microsoft product can be asked over the
TAR system.

However, to support the complex questions that we get, we really need to see
the customer's code. TAR offers an immediate mechanism for transferring that
code back and forth. Coupled with our callback on priority questions, we
think we've developed the optimum support system for our ISV customers. It
lets them have direct contact over the telephone with Microsoft, and gives
them the chance to explain the problem in detail in writing, which is
usually the best way to handle it.

Every TAR is summarized and rewritten to become a Bulletin Board item after
it's answered. Of course, the anonymity of the person who sent in the TAR is
always maintained.


MSJ: How many people do you have on staff right now?

BAKER: In the system support area in Microsoft, there are 35 people
supporting languages and operating environments. We have just gotten
approval to double our operations staff from 10 to 20. We've also introduced
a very interesting job rotation plan that allows engineers to work on DIAL
and then rotate back into engineering.


MSJ: How long does it usually take for a question on TAR to be answered?

BAKER: In the past, because of the limited staff, the typical time was 2 or
3 days. Right now, our goal is to get it to within 24 hours on all TAR
questions.

MSJ: How much does DIAL cost?

BAKER: $450 a year for unlimited service covering all of the MS-DOS system's
products; XENIX Support is $600 per year.


MSJ: Are there any arrangements for long-distance callers?

BAKER: We have negotiated an arrangement for low-cost service with Telenet
so that DIAL users who call long distance can reduce that charge.


MSJ: How many lines do you have now?

BAKER: We have 8 Telenet lines and 8 long-distance lines. We also monitor
the usage at any particular time and add lines as they are needed.


MSJ: What is your own frank evaluation of DIAL 2.0?

BAKER: It's a good, solid support vehicle that still needs work. In the
past, there weren't enough senior people staffed on DIAL. Of course, we're
meeting that need now by adding more people.

The knowledge base is probably the most outstanding keyword search bulletin
board system in the entire computer industry──and we've looked at the
systems that DEC and IBM are using as well, so we really have a
technological lead with our system.

I think that in certain areas, such as actual support delivery, it does need
some enhancement, but Microsoft management is being very responsive in
giving us the resources to make it a really great world-class system.

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

Rich Text Format Standard Makes Transferring Text Easier

───────────────────────────────────────────────────────────────────────────
Also see the following related articles:
  An RTF Sample
  Preliminary Rich Text Format Specification
───────────────────────────────────────────────────────────────────────────

Nancy Andrews☼

Back in the bad old days of punch cards, formatted text was never an issue.
There were no formats; you couldn't even use lowercase. But we've come a
long way since then. With bit-mapped screens, state-of-the-art word
processors can handle numerous character, paragraph, and section formats.
Not only can you get lowercase, you can have 16-point Helvetica in a
paragraph with a 1/2-inch hanging indent. However, problems arise when you
transfer text and formats to other applications. Rich Text Format (RTF)
provides a solution to these problems.

RTF is a way to encode formatted text. Microsoft is proposing RTF as a
standard to make moving text between applications easier. With RTF, users
can transfer text from one application to another and not lose any
formatting. And developers can save documents in one format, RTF, and know
that other applications can read this information without worrying about
translating to each application's unique format.


Why RTF

Microsoft is pushing RTF because of Microsoft Windows. Currently, if you use
Window's clipboard to transfer text between applications, you lose all
formatting. For example, if you were transferring formatted text from
Windows Write into Micrografx Draw, you'd have to reformat the text within
Draw. A way to retain the format from one application when it is transferred
to another is needed. If both Write and Draw used RTF, the clipboard could
transfer the formats along with the text. Microsoft recognizes the need to
exchange documents easily among its own products. It sees RTF as a solution
for existing and future products.

Current format standards such as IBM's Document Content Architecture (DCA),
although widely supported, are not adequate. DCA, for example, lacks a good
font strategy. It works with pitch only, doesn't consider point sizes, and
has only one code to specify proportional spacing. DCA is most efficient for
transferring entire documents, as opposed to short strings.

Microsoft wanted a standard that could handle all existing formats and both
entire documents and the rapid exchange of short formatted strings. This
kind of information is usually transferred via the Windows clipboard. When
you transfer short strings, you don't want to include the document name,
creator, and date of last modification. But when you transfer entire
documents, this information is essential. RTF has the flexibility to handle
both types of transfer. Also, the standard had to be what Charles Simonyi,
author of RTF and Microsoft's chief architect of applications, calls
"forward-looking," that is, one that can handle existing applications and
leaves room for future enhancements as well.


What RTF Is

RTF provides a standard format for interchanging text regardless of the
output device, operating system, or operating environment. Text and format
instructions are saved in 7-bit ASCII format so that they are easy to read,
and you can send them over virtually any communications link.

RTF uses "control words" to encode the formats. Control words provide the
space so that RTF can be expanded later to include macros or additional
formats. Control words use this form:

  \lettersequence
  <delimeter>

If the delimeter is a digit or a hyphen (-), another parameter follows. A
nonletter or digit terminates the control word.

In addition to control words, RTF uses braces. A left brace ({) marks the
beginning of a group of text, and a right brace (}) marks the end. Braces
can be used to delineate footnotes, headers, and titles. In RTF, the control
words, the control symbol (\), and the braces constitute control
information. All other characters in RTF are plain text.

A bit of RTF code might look like this:

  \rtf0\mac{\fonttbl\f1\ fromanBookman;}
  {\stylesheet {\s0 Normal;}
  {\s1\i\qj\snext2\f1 Question;}{\s2\qj\f1 Answer;}}
  {\s0\f1\b\qc Questions and Answers\par }
  {\s1\i\qj 1. What is the left margin of this document?\par}
  {\s2\qj\li720\f1 Since no document parameters were specified, the default
     of 1800 twips (1.25") is used.\par}}

Here's what this information means. \rtf0\ indicates that this is an RTF
document and the version number is 0. It uses the Macintosh, rather than the
PC or ANSI, character set. Next comes information about the current font,
which is from font table 1, the roman family, the Times font. After that is
style sheet information. Three different styles are assigned. Style 0 is the
normal character and paragraph style. Style 1 is italic, the paragraph is
justified, it is always followed by a style 2 paragraph, uses font 1, and is
called Question. Style 2 is justified, uses font 1, and is called Answer.

Next is information about the first paragraph, followed by the text of that
paragraph. It is normal text (style 0), uses font 1, and is bold and
centered. The second paragraph is a question, uses the question style, and
is italic and justified. The last section is the answer, which is based on
style 2, is justified, and uses a left indent of 720 twips or 0.5". (Twips
are 1/20th of a point; there are 1,440 twips per inch.)

To read a stream of RTF you first need to separate the control information
from the plain text and then act on the control information. When you
encounter a left brace ({), you have to stack the current state and then
continue. When you encounter a control symbol (\), collect the control word
and parameter, if any, look up the word in the symbol table (a constant
table), and follow the directions outlined there. For example, change a
formatting property such as adding the bold format. Then continue writing
characters to the current destination by using the current formatting
properties. When you come to the right brace (}), unstack the current state.
Working with RTF is straightforward──much simpler than decoding proprietary
file formats of individual applications.


Who's Playing

RTF, like any standard, is as valuable as the people who support it.
Microsoft believes that most Microsoft Windows  applications developers will
support RTF, since it is advantageous for them to be able to easily exchange
formatted text. George Grayson, president of Micrografx, one of the first
Windows applications developers, says that its products, In*a*Vision, Draw,
and Graph, will all support RTF by June. Also, Microsoft currently has
several Windows applications in development that will definitely support
RTF.

Aldus, developer of PageMaker for the Mac and PC, has not yet committed to
RTF. But Ted Johnson, an Aldus engineer, says that although RTF was too late
to be included in Version 1.0 of Aldus's PC PageMaker, versions after 1.0
will definitely support RTF for clipboard transfers. PageMaker will also
continue to accept formatted text from several word processors using the
proprietary file format of each of these word processors. So it looks like
RTF is well on its way to acceptance in the Microsoft Windows  world.


What Next

Greg Slyngstad, program manager in Microsoft's word processing area and
current keeper of the RTF standard, claims "this version is complete, but we
expect to extend it in the future to meet the needs of Windows application
developers (including Microsoft). Any future version will be superset of the
current RTF." The goal of RTF is to retain all formatting in transfer. And
Slyngstad's goal is to ensure that the RTF standard responds to the needs of
developers.


───────────────────────────────────────────────────────────────────────────
An RTF Sample
───────────────────────────────────────────────────────────────────────────

{\rtf0\mac {\fonttbl{\f0\fswiss Chicago;}{\f3\fswiss Geneva;}{\f4\fmodern
Monaco;}{\f13\fnil Zapf Dingbats;}{\f14\fnil Bookman;}{\f15\fnil N Helvetica
Narrow;}{\f20\froman Times;}{\f21\fswiss Helvetica;}{\f22\fmodern
Courier;}{\f23\ftech Symbol;}}
{\stylesheet{\s243\tqc\tx4320\tqr\tx8640 \sbasedon0\snext243
footer;}{\sbasedon222\snext0
Normal;}}\margl1613\margr1080\margt720\widowctrl\ftnrestart {\headerl
\pard\plain 1-4 see auditors notes{\|}\par
\par
}\sectd \linemod0\linex0\cols1\colsx0\endnhere {\footer \pard\plain
{\i\f20\fs18 This document was prepared with Microsoft(R)
Word}{\i\scaps\f20\fs18  }{\i\f20\fs18 Version }{\i\scaps\f20\fs18
3.0}{\i\f20\fs18  for Apple(R) Macintosh(TM) systems, \par
and printed with an Apple LaserWriter(TM).}{\i\scaps\f20\fs18 \par
}\pard\plain \s243\tqc\tx4320\tqr\tx8640 {\f150\fs18 \par
}}\pard\plain \ri360 {{\pict\macpict\picw73\pich66
02b2012500af016700f81101a00082a0008c01000a0000000002d0024051013500b4016700f1
5809000000000000000071001e013500c5016200de016000dd013900c501
                                    ∙
                                    ∙
                                    ∙
00f3a0621670002a015300eb016000f7015400f3015700ee015c00eb016000ef015f00f60158
00f7015300f3015400f3a0008da00083ff}}{\b\f140\fs28 \par
\par
}\pard \ri187 {\b\f140\fs32 West Associates, Inc.\par
}\pard \ri187\sb40\sl320\brdrt\tqr\tx9360 {\f139 Consolidated Balance
Sheet}{\f21\fs20 \tab }{\i\f21\fs20 Current as of: December 31st, 1986\par
}\pard \ri5947\sa40\brdrb {\f21\fs20 \par
}\pard \ri187\brdrth\tx360\tx1080\tx6480\tqr\tx7560\tx8280\tqr\tx9360
{\b\f140\fs20 Assets}{\b\f21\fs20 \tab \tab \tab }{\b\i\f21\fs20 1986\tab
}{\i\f21\fs20 \tab 1985\par
}\pard \ri187\sb40\brdrt\brdrth\tx360\tx1080\tx8280 {\i\f21\fs20 \tab
}{\f21\fs20 Current Assets:\par
}\pard \ri187\tx360\tx1080\tqr\tx7560\tqr\tx9360 {\f21\fs20 \tab \tab Cash
and temporary cash investments}{\b\f21\fs20 \tab $114,888}{\f21\fs20 \tab
$143,284\par
\tab \tab Accounts receivable}{\b\f21\fs20 \tab 258,238}{\f21\fs20 \tab
136,420\par
\tab \tab Inventories (at cost)}{\scaps\f21\fs16\up6 1}{\b\f21\fs20 \tab
264,619}{\f21\fs20 \tab 142,457\par
}\pard \ri187\sb40\tx360\tx1080\tqr\tx7560\tqr\tx9360 {\f21\fs20 \tab \tab
Prepaid Income Taxes}{\b\f21\fs20 \tab 26,751}{\f21\fs20 \tab 27,949\par
}\pard \ri187\brdrb\tx360\tx1080\tqr\tx7560\tqr\tx9360 {\f21\fs20 \tab \tab
Other current assets}{\b\f21\fs20 \tab 23,055}{\f21\fs20 \tab 18,883\par
}\pard \ri187\sb40\brdrb\tx360\tx1080\tx1620\tqr\tx7560\tqr\tx9360
{\f21\fs20 \tab \tab \tab }{\f138\fs20 Total Current Assets:}{\b\f21\fs20
\tab $687,551}{\f21\fs20 \tab $468,993\par
}\pard \ri187\tx360\tx1080\tx1620\tqr\tx7560\tqr\tx9360 {\f21\fs20 \tab
Property, plant and equipment\tab \par
\tab \tab Land and Buildings}{\b\f21\fs20 \tab 24,892}{\f21\fs20 \tab
19,993\par
\tab \tab Machinery and equipment}{\b\f21\fs20 \tab 68,099}{\f21\fs20 \tab
51,445\par
\tab \tab Office furniture and equipment}{\b\f21\fs20 \tab 30,575}{\f21\fs20
\tab 22,628\par
}\pard \ri187\sa60\tx360\tx1080\tx1620\tqr\tx7560\tqr\tx9360 {\f21\fs20 \tab
\tab Leasehold improvements}{\scaps\f21\fs16\up6 2}{\b\f21\fs20 \tab
26,008}{\f21\fs20 \tab 15,894\par
}\pard \ri187\brdrt\tx360\tx1080\tx1620\tqr\tx7560\tqr\tx9360 {\f21\fs20
\tab \tab \tab }{\b\f21\fs20 \tab $149,574}{\f21\fs20 \tab $109,960\par
}\pard \ri187\tx360\tx1080\tx1620\tqr\tx7560\tqr\tx9360 {\f21\fs20 \tab \tab
Accumulated depreciation and amortization}{\b\f21\fs20 \tab
(73,706)}{\f21\fs20 \tab (42,910)\par
                                    ∙
                                    ∙
                                    ∙
\ri187\sb40\brdrb\brdrdb\tx360\tx1080\tx1620\tqr\tx7560\tqr\tx9360
{\f21\fs20 \tab \tab \tab }{\b\f21\fs20 \tab $788,786}{\f21\fs20 \tab
$556,579\par
}\pard \ri187\brdrdb\tx360\tx1080\tx1620\tqr\tx7560\tqr\tx9360 {\f21\fs20
\par
}{\f138\fs18 All amounts shown in thousands (000)\par
}}

───────────────────────────────────────────────────────────────────────────
Preliminary Rich Text Format Specification
───────────────────────────────────────────────────────────────────────────

RTF text is a form of encoding for various text formatting properties,
document structures, and document properties, using the printable ASCII
character set. The main encoding mechanism of "control words" provides a
naming convention to expand the realm of RTF to include macros, programming,
and so on. Special characters can also be thus encoded, although RTF does
not prevent the utilization of character codes outside the ASCII printable
set.


BASIC INGREDIENTS

A file or stream of RTF consists of "plain text" interspersed with control
symbols, control words, and braces. Control words follow the format:

  \<alphabetic string> <delimiter>

The delimiter can be a space or any other nonalphanumeric character. When a
numeric parameter follows the control word, the first digit of the parameter
(or the minus sign, in the case of a negative number) functions as the
delimiter of the control word. The parameter is then in turn delimited by a
space or any other nonalphanumeric character.

Control symbols consist of a \ character followed by a single nonalphabetic
character. They require no additional delimiting characters. Control symbols
are compact, but there are not too many of them, whereas the number of
possible control words is unlimited. The parameter is partially incorporated
in control symbols, so a program that does not understand a control symbol
can recognize and ignore the corresponding parameter as well.

Each group begins with a control word that describes the contents of the
group. { indicates the beginning of a text group and } indicates its end.
The text grouping is used for formatting and to delineate structural
elements of the document, such as the footnotes, headers, title, and so on.
All other characters in RTF text constitute "plain text." Since the
characters \, {, and } have specific uses in RTF, the control symbols \\,
\{, and \} are provided to express the corresponding plain characters.


WHAT RTF TEXT MEANS

The reader of an RTF stream will be concerned with separating control
information from plain text and acting on control information. This is
designed to be a relatively simple process, as described below. Some control
information just contributes special characters to the plain-text stream.
Other control information changes the "program state," which includes
properties of the document as a whole and also a stack of "group states"
that apply to parts of the document.

The file is structured as groups and subgroups. Each group state is defined
by a text group (text enclosed in braces). The group state specifies the
following:

  a.  The "destination" or the part of the document that the plain text is
      building up.

  b.  The character formatting properties, such as bold or italic.

  c.  The paragraph formatting properties, such as justification.

  d.  The section formatting properties, such as number of columns.


GROUPS AND SUBGROUPS

The overall grouping is the document file as a whole, since the entire
document is enclosed in braces, beginning with the control word
\rtf<parameter> with the parameter being the version number of the writer.
This control word must begin every RTF file, and the entire file must end
with the corresponding closing brace.

Before any text in the RTF file is entered, the character set for the entire
destination may be declared using one of these control words:

  \ansi            Text is the ansi character used by Microsoft Windows
                   (default).
  \mac             Text is the Macintosh character set.
  \pc              Text is the IBM PC character set.


COLOR TABLE

The control word \colortbl is used to define the color table, with numeric
indexes (starting at 0) for red, green, and blue. These indexes are the same
as those used in Windows (the italicized portion represents the variable
numeric parameter):

  \red000          Red index
  \green000        Green index
  \blue000         Blue index
  \cf000           Foreground color
  \cb000           Background color

Each set of color definitions delimited by semicolons defines the next
sequential color number. The following example defines color 0 and color 2:

  {\colortbl\red128\green0\blue64;;\red64\green128\blue0;}


FONT TABLE

The control word \fonttbl designates the group that is the font table, which
assigns the font name and family to the font numbers used. The text is the
font name delimited by semicolons. Default is used if no font was assigned
and the recipient should use whatever font is considered the default for
that particular output device. The font table, if it exists, must occur
before the style sheet defintion and any text in the file. Possible font
families are:

  \fnil            Family unknown (default)
  \froman          Roman family. Proportionally spaced, serif (Times Roman,
                   Century, Schoolbook, etc.)
  \fswiss          Swiss family. Proportionally spaced, sans serif
                   (Helvetica, Swiss, etc.)
  \fmodern         Fixed pitch, serif or sans serif (Pica, Elite, Courier,
                   etc.)
  \fscript         Script family (Cursive, etc.)
  \fdecor          Decorative fonts (Old English, etc.)
  \ftech           Technical or symbol fonts

Example:

  {\fonttbl\f0\froman Tms Rmn;\f1\fswiss Helv;\f2\fnil default;}


STYLE SHEETS

The style sheet for the document is defined by a group beginning with the
control word \stylesheet.

More precisely, plain text in the group between semicolons is interpreted as
style names, which will be defined to stand for formatting properties that
are in effect.

Example:

  {\stylesheet{\s0\f3\fs20\qj Normal;}
  {\s1\f3\fs24\b\qc Heading Level 3;}}

This defines Style 0 (given the name "Normal") as 10-point justified type in
font 3 (the font is defined in the font table). Style 1 (Heading Level 3),
is 12-point font 3, boldface and centered.

The following fields may be present if the destination is \stylesheet.

  \sbasedon000     This defines the style number which the current style is
                   based on. If this control word is omitted, the style is
                   not based on any style.

  \snext000        This defines the next style associated with the current
                   style. If this control word is omitted, the next style
                   is itself.


PICTURES

If the group begins with the control word \pict the plain text within the
group is a hex dump of a picture. The following optional parameters may also
exist if the group is a picture. If they are not present, the default frame
size equals the picture size. A picture must be preceded by the \chpict
special character that serves as the anchor point for the picture that
follows.

  \pich000         Defines picture frame height in pixels. The picture
                   frame is the area set aside for the image. The picture
                   itself does not necessarily fill the frame (see
                   \picscaled).

  \picw000         Picture frame width in pixels

  \picscaled       Scales the picture up or down to fit within the
                   specified size of the frame

  \wmetafile       The picture is a Windows metafile

  \macpict         The picture is in Mac Quick Draw format

  \bin000          Special field used to include binary information within
                   the file (in lieu of hex as expected). The parameter
                   defines the number of bytes of binary information that
                   follow. This is useful for the "clipboard" data
                   exchange where the file is not intended to be
                   transferred over a communications line.


FOOTNOTES

If the group begins with the control word \footnote the group contains
footnote text. The footnote is anchored to the character that immediately
preceds the footnote group. The group may be preceded by the footnote
reference characters if automatic footnote numbering is defined. A footnote
group may also have the following complements:

  \ftnsep          Text is a footnote separator.
  \ftnsepc         Text is a separator for continued footnotes.
  \ftncn           Text is continued footnote notice.


HEADERS AND FOOTERS

If the group begins with the control words\header or \footer the group is
the header or footer for the current section. The group must precede the
first plain text character in the section. The following variant forms may
also be used:

  \headerl         Header on left-hand pages only
  \headerr         Header on right-hand pages only
  \headerf         Header on first page only
  \footerl         Footer on left-hand pages only
  \footerr         Footer on right-hand pages only
  \footerf         Footer on first page only


INFORMATION

The file may also contain an information group, specified by the control
word \info. This group is used to store such information as the title. The
name of the author, and the subject of the document. The appropriate
information is entered as plain text following these control words:

  \title
  \subject
  \author
  \operator
  \keywords
  \comment         Text will be ignored.
  \version
  \doccomm         This last control word is for storing comments. It
                   should not be confused with \comment  which specifies
                   that the text is to be ignored. Here is an example of
                   an information block:

  {\info{\title Unified Field Theory Analysis}{\author A. Finestyne}

Other control words allow values to be automatically entered into the
information block:

  \vern000         Internal version number
  \creatim         Creation time
  \yr000           Year
  \mo000           Month
  \dy000           Day
  \hr000           Hour
  \revtim          Revision time
  \printim         Last print time
  \buptim          Backup time
  \edmins          Minutes of editing
  \nofpages000     Number of pages
  \nofwords000     Number of words
  \nofchars000     Number of characters
  \id000           Internal id number


TEXT PROPERTIES

Note that a change of destination will reset all text properties to their
default values and that changes are only legal at the beginning of a text
group.


DOCUMENT FORMATTING PROPERTIES

The default value (given in square brackets) will occur if the control word
is omitted. When the control words are used,000 is replaced by numeric
parameters. All measurements are in twips which are twentieths of a point or
1/1440th of an inch.

Examples:
  \paperw000       Paper width in twips [12240]
  \paperh000       Paper height [15840]
  \margl000        Left margin [1800]
  \margr000        Right margin [1800]
  \margt000        Top margin [1440]
  \margb000        Bottom margin [1440]
  \facingp         Facing pages (enables gutters and odd/even headers)
  \gutter000       Gutter width (inside of facing pages) [0]
  \ogutter000      Outside gutter width [0]
  \deftab000       Default tab width [720]
  \widowctrl       Enable widow control
  \headery000      Header y position from top of page [1080]
  \footery000      Footer y position from bottom of page [1080]
  \ftnbj           Footnotes at bottom of page (default)
  \ftnsep          Text is a footnote separator
  \ftnsepc         Text is a separator for continued footnotes
  \endnotes        Footnotes at end of section
  \ftntj           Footnotes beneath text (top justified)
  \ftnstart000     Starting footnote number [1]
  \ftnrestart      Restart footnote numbers each page
  \pgnstart000     Starting page number [1]
  \linstart000     Starting line number [1]
  \landscape000    Printed in landscape format


SECTION FORMATTING PROPERTIES

Examples:
  \sectd           Reset to default section properties
  \sbknone         Section break continuous (no break)
  \sbkcol          Section break starts a new column
  \sbkpage         Section break starts a new page (default)
  \sbkeven         Section break starts an even page
  \sbkodd          Section break starts odd page
  \pgnrestart      Restart page numbers at 1
  \pgndec          Page number format decimal
  \pgnucrm         Page number format upper case roman numeral
  \pgnlcrm         Page number lower case roman numeral
  \pgnucltr        Page number format upper case letter
  \pgnlcltr        Page number format lower case letter
  \pgnx000         Page number x pos [720]
  \pgny000         Page number y pos [720]
  \linemod000      Line number modulus [1]
  \linex000        Line number text distance [360]
  \linerestart     Line number restart at 1 (default)
  \lineppage       Line number restart on each page
  \linecont        Line number continued from previous section
  \vertalt         Vertically align starting at top of page (default)
  \vertalc         Vertically align in center of page
  \vertalj         Vertically justify top and bottom
  \vertal          Vertically align starting at the bottom
  \cols000         Number of columns (snaking) [1]
  \colsx000        Space between columns [720]
  \endhere         Include endnotes in this section
  \titlepg         Title page is special


PARAGRAPH FORMATTING PROPERTIES

Examples:
  \pard            Reset to default paragraph properties
  \s000            Style; if a style is specified, the paragraph formatting
                   implied by that style must still be specified with the
                   paragraph
  \ql              Quad left (default)
  \qr              Quad right
  \qj              Justified
  \qc              Centered
  \fi000           First line indent [0]
  \li000           Left indent [0]
  \ri000           Right indent [0]
  \sb000           Space before [0]
  \sa000           Space after [0]
  \sl000           Space between lines (If no \sl is specified, default is
                   12 points. If \sl000 is specified, line spacing is
                   automatically determined by the tallest font on the
                   line).
  \keep            Keep
  \keepn           Keep with next paragraph
  \sbys            Side by side
  \pagebb          Page break before
  \noline          No line numbering
  \tx000           Tab position (this places a vertical bar at the
                   specified position for the height of the paragraph)
  \tqr             Flush right tab (last specified position)
  \tqc             Centered tab
  \tqdec           Decimal aligned tab
  \brdrt           Border top
  \brdrb           Border bottom
  \brdrl           Border left
  \brdrr           Border right
  \box             Border all around
  \brdrs           Single thickness border
  \brdrth          Thick border
  \brdrsh          Shadow border
  \brdrdb          Double border
  \tldot           Leader dots
  \tlhyph          Leader hyphens
  \tlul            Leader underscore
  \tlth            Leader thick line


CHARACTER FORMATTING PROPERTIES

Examples:
  \plain           Reset to default text properties
  \b               Boldface
  \i               Italic
  \strike          Strikethrough
  \outl            Outline
  \shad            Shadow
  \scaps           Small caps
  \caps            All caps
  \v               Invisible text
  \f000            Font number
  \fs000           Font size in half points [24]
  \expnd000        Expansion or compression of space between characters in
                   quarter points (negative value indicates compression).
  \ul              Continuous Underline
  \ulw             Word underline
  \uld             Dotted underline
  \uldb            Double underline
  \ulnone          Cancels all underlining
  \up000           Superscript in half points [6]
  \dn000           Subscript in half points [6]

A zero following the character formatting instruction turns off the format.

For example:
  \b0              Turns off the boldface
  \i0              Turns off the italic


SPECIAL CHARACTERS

If a character name is not recognized by a reader, it will simply be
ignored. Other characters may be added for interchange with other programs.

These are examples of special characters:
  \chpgn           Current page number (as in headers)
  \chftn           Auto footnote references (footnote to follow in a
                   group.)
  \chdate          Current date (as in headers)
  \chtime          Current time (as in headers)
  \|               Formula character
  \~               Nonbreaking space
  \-               Discretionary, or soft, hyphen
  \_               Nonbreaking hyphen
  \'hh             Any hex value (can be used to identify 8-bit value)
  \page            Required page break
  \line            Required line break (no paragraph break)
  \par             End of paragraph
  \sect            End of section and end of paragraph
  \tab             Same as ASCII 9

For simplicity of operation, ASCII 9 will be accepted as tab. The control
code \<10> (backslash followed by ASCII 10) and \<13> will both be accepted
as \par. ASCII 10 and ASCII 13 without the backslashes will be ignored. They
may be used to include carriage returns for easier readability but will have
no effect on the interpretation as long as they do not occur within a
control word. It's a good idea to insert carriage returns every 255
characters or less to facilitate transmission via E-mail systems.

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

Ask Dr. Bob!

DOS Path

Dear Dr. Bob,

I need to use the MS-DOS path to search for the overlays, initialization
files, and data files my program needs. I can't figure out how to get this
from MS-DOS. Can you give me a clue about where to get this information?
──Searching

Dear Searching,

It's true that MS-DOS has no usable support for path information. But you
can use the C library getenv ("PATH") function. It returns a pointer to the
current path string. At that point it's up to your program to decode and use
that information, which is in the same format you see when you type "path"
after the MS-DOS prompt. You need to parse the string and then scan
individual directories for the files that you will need.


Redundan-C

Dear Dr. Bob,

My programs (like most everyone's these days) use some C modules and some
MASM modules. It seems so redundant to type the same declarations in C
format and then in MASM format. Often, I make a change in one and then
forget to incorporate it into the other. Is there a way to get around this?
──Forgetful

Dear Forgetful,

Actually, there is. The trick is to use C's preprocessor phase. What you can
do is make a header file that produces the definitions required for both C
and MASM. Then you write one declaration file using generic rather than C-
or MASM-specific declarations. When you make a change, all you need to do is
change the generic header file, and it will automatically change both the C
and MASM declarations.

Here's how it works. First you make a header file like the one in Figure 1,
called C_OR_ASM.HDR. It takes information from a generic program declaration
file and turns it into the format required by either the C compiler or MASM.

Next, put the declarations for your program in a file like the one shown in
Figure 2. In this case, we have called our file SHARED.HDR. As you can see,
instead of using either C or MASM's declaration formats, what you use are
generic formats. C_OR_ASM.HDR then takes this file and turns it into the
required format.

The last piece you need to finish the puzzle is a way of telling the
compiler or assembler about these files. Here's where you use C's
preprocessor. You use both the /D and /EP switches. The /D switch tells the
preprocessor which set of definitions to use in C_OR_ASM. /EP tells the
compiler to stop after this preprocessor stage. If you're assembling a MASM
module, use this line:

  msc shared.hdr
  /DMASM_STYLE /EP;
  >shared.inc

It takes SHARED.HDR and produces the declarations required in MASM format in
a file called SHARED.INC.

For C declarations use the following line:

  msc shared.hdr
  /DC_STYLE /EP;
  >shared.h

It produces the declarations in C format in a file called SHARED.H.

Now you have declaration files in both formats. All you need to do is
include them with your .C or .ASM files when you compile. If you make any
changes, the only file you need to change is SHARED.HDR.


Taking Out Mouse Garbage

Dear Dr. Bob,

I am currently writing a QuickBASIC program that uses some of the new hi-res
EGA modes──SCREEN 8, 9, and 10. But whenever I move the mouse around while
my program is writing to the screen, I get garbage. Is there a fix for this?
──Mystified


Dear Mystified,

You're in trouble. Because the EGA has write-only registers, in order for
two programs to write to the screen at the same time, they need to
cooperate. Microsoft QuickBASIC doesn't cooperate with anyone. So you can
just avoid using the mouse, or, if the mouse is critical, disable it while
you're drawing on the screen. For additional information you can call
Microsoft Technical Support at (206) 882-8089 and ask for "Writing EGA
Software for the IBM PC."


Hexing Windows' Calculator

Dear Dr. Bob,

I used to have a terminate-and-stay-resident calculator I used all the time.
When I started using Microsoft Windows, I found I really needed all my
memory for Windows and the applications I run with it. I've tried using the
Windows calculator. Although it has the basics, what I need is the ability
to display the hex equivalent of decimal numbers. Is there any way I can get
the Windows calculator to do this?──Refiguring

Dear Refiguring,

You're in luck. It's quick and easy. Just hold down the H key and the number
currently displayed will temporarily change to its hexidecimal equivalent.


Standard Memory Error

Dear Dr. Bob,

I'm running Microsoft Windows and I'm using standard applications. When I
quit one of the standard applications, Windows doesn't always seem to
relinquish all of its memory. Occasionally I get a "Not enough memory..."
error when I think I shouldn't.──Puzzled

Dear Puzzled,

This can happen because of the way Windows manages memory for standard
applications. You need to follow a couple of basic guidelines when working
with several standard applications and Windows. First, load the largest
standard application. When Windows starts the second standard application,
it swaps the first out and the second in. Loading the largest first ensures
that there will be enough room. Then if you quit the largest application the
second will still have as much memory as was allotted for the largest one.
You can fix this by choosing the About... command in the MS-DOS Executive
System menu. In addition to displaying the amount of memory, it also does
garbage collection and will reclaim and reorganize the unused memory.


BitBlt Limits

Dear Dr. Bob,

I'm working on a Windows app. I tried to use BitBlt to copy a large portion
of the screen, but it didn't work. What's wrong?──Frustrated

Dear Frustrated,

An undocumented limitation of existing versions of Windows is that bitmaps
can't be larger than 64K. One way you can get around this is to use
"banding"──splitting the block that you want to move into rectangles and
then calling BitBlt on each of the smaller rectangles.


Stock Bitmaps

Dear Dr. Bob,

The Windows Software Development Kit documentation for CreateCompatible
display context says that "GDI automatically selects a monochrome stock
bitmap" for the new memory display context. What does this mean? Is there
something special about a "stock bitmap"?──Wondering

Dear Wondering,

There is nothing special about a monochrome stock bitmap. It is simply a
bitmap with one plane and one bit per pixel. By using the CreateCompatibleDC
function a display context is set up whose "display surface" is simply a
bitmap allocated from system memory──it is not actually displayed anywhere.
Any GDI function may be used to draw into this bitmap. BitBlt can quickly
move images between itself and another DC. When a memory DC is created,
Windows allocates a monochrome bitmap for it as a default, even if your
original had color. If you have all color information, you can switch to a
color bitmap by using CreateBitmap and SelectObject.


Scaling Text

Dear Dr. Bob,

I'm confused by Windows' mapping modes. My graphics get scaled because I
specify anisotropic mode, but my text doesn't. Is there a way to scale text?
──Confused

Dear Confused,

Maybe. You can tell whether a given device supports character scaling with
GetDeviceCaps (hDC,TEXTCAPS). Certain bits of the return value tell what
sorts of text display tricks are supported. If the device does support some
text scaling, be sure to use DrawText, instead of TextOut, because TextOut
doesn't scale.


Interrupt Routines

Dear Dr. Bob,

I'm writing a Windows app that uses a special interrupt-driven I/O board.
Obviously, it's not supported by Windows. Ideally, I would like my interrupt
service routine to send messages to the Windows app. It would be notified by
GetMessage, just as when you press a key or move the mouse. Am I dreaming?
──Dreamer

Dear Dreamer,

Yes. First of all, you can't call any Windows functions from within your
interrupt service routine because right now Windows is not re-entrant. You
might have better luck when DOS and Windows are truly multitasking. Windows
handles the keyboard and mouse in a special way and does not allow you to
define your own messages that would work like the keyboard and mouse. The
best you can do is use SetTimer to generate WM_TIMER messages for your
Windows application periodically. You can have your interrupt routine set
flags, which are polled in the WM_TIMER code. Obviously, this means that you
can't count on a quick response since you'll be at the mercy of the Windows
scheduler and the other applications to return your program control
occasionally.


Windows Color Support

Dear Dr. Bob,

The EGA hardware supports 16 colors. To my surprise, I discovered Windows
displays only eight. Say it ain't so, Bob.──Surprised

Dear Surprised,

When Microsoft wrote the EGA driver, it traded colors for speed. The EGA
card has four planes, allowing a possibility of 16 colors. Microsoft chose
to use only three planes for color and reserved the fourth plane for the
mouse cursor and the other cursor. If Microsoft had given this plane to
color rather than the cursor, each time the mouse moved you would need to
restore the screen in the old cursor position, save the screen in the new
cursor position, and draw the cursor. All three operations would have to
process all four planes. This would be too slow, especially on an 8088 PC.

By dedicating one plane to the cursor, you don't have to save a copy of the
screen under the cursor. Moving the cursor is simply erasing it from the old
position and drawing it at the new position. This tradeoff seems reasonable.
After all, Windows can dither the dots and get 64 shades for each of the
eight colors that are supported.


Using Windows and SYMDEB Together

Dear Dr. Bob,

I do a lot of traveling. I like to use my time on the airplane to work on my
hobby──debugging Windows apps. How can I have my Windows app and SYMDEB
running on the screen at the same time?──Jetsetter

Dear Jetsetter,

First, fasten your seatbelt, and then relax. It has been reported that you
can run Windows and SYMDEB on the same screen some of the time. But this
doesn't work reliably, and in fact, Dr. Bob hasn't been able to get it to
work at all.

In any event you'll most likely find it easier between landings and takeoffs
to use a second monitor or a serial terminal. That way you can see your
Windows app running on one screen and SYMDEB on the other.


Getting the Same Image

Dear Dr. Bob,

I'm using the same series of steps for output to the screen and the printer,
but the images are different. Is there a way to guarantee that the output
will be the same on both devices?──Curious

Dear Curious,

You can't always guarantee the images will be the same because the images
that are produced depend on the capability of the output device. For
example, monitors differ in number of colors and resolution as well as
support from the device driver. To get the same image on different devices,
you must use SelectObject to give each device the same pen, brush, and font.
Then it's up to Windows to match what you send to the capabilities of the
device.


Self-Modifying Code

Dear Dr. Bob,

Let's say that you have a program that manipulates its own code. How then
can it function with only one code segment? Also, is there an objective
reality?──Philosophical


Dear Philosophical,

Well, if you must write self-modifying code... be sure that the modified
code can be relocated, and specify that it is not discardable. If Windows
discards and reloads, then some can be lost. To prevent this, state that it
isn't moveable in the definition file. Windows uses the same code segment
when you load multiple instances of the same program. To force Windows to
load a fresh copy of the code specify:

  CODE MULTIPLE MOVEABLE

in the Module Definition File, passed to the linker.

As for objective reality, Dr. Bob defers to his colleague Dr. Science of San
Francisco, Calif.


Figure 1:

#ifdef    MASM_STYLE
   #define CONSTANT(name,value)     name = value
   #define INTVAR(name)             name DW ?
   #define INTARRAY(name,size)      name DW size dup(?)
   #define STRUCVAR(name,strc)      name DB (size strc)dup(?)
   #define DEFSTRUC(name)           name STRUC
   #define ENDSTRUC(name)           name ENDS
#endif

#ifdef    C_STYLE
   #define CONSTANT(name,value)    #define name (value)
   #define INTVAR(name)            int  name;
   #define INTARRAY(name,size)     int  name[size];
   #define STRUCVAR(name,strc)     struct strc name;
   #define DEFSTRUC(name)          struct  name {
   #define ENDSTRUC(name) };
#endif


Figure 2:

#include "c_or_asm.hdr"

CONSTANT (ScreenHt,    200)
CONSTANT (ScreenWidth, 640)
CONSTANT (MaxNumPts,   1024)

DEFSTRUC (Point)
INTVAR (x)
INTVAR (y)
ENDSTRUC (Point)

STRUCVAR (startPt,Point)
INTVAR     (counter)
INTARRAY (data,MaxNumPts)


Figure 3:

$cursor dd  0b8000000h  ;for MONO adapter use 0b0000000h
$hexdig db  '0123456789ABCDEF'

;------ Display character in AL and advance cursor:
$showc: push  es,di
         les di,cs:$cursor     ; Get current cursor
                               ; (segment & offset)
         mov ah,07h            ; Set attribute
         cld
         stosw                 ; Write char to screen
         mov word ptr cs:$cursor,di ; Save csr (offset only)
         pop di,es
         ret

;------ Display value in DX as a hex number:
$showx: push  f,ax,bx,cx
        mov cx,12             ; Shift by 12,8,4, and 0 bits
$sx1:   mov bx,dx             ; Get number from DX
        shr bx,cl             ; Get next nybble to display
        and bx,000fh
        mov al,cs:$hexdig[bx] ; Map nybble to hex digit
        call  $showc          ; Show hex digit
        sub cl,4
        jns $sx1              ; Keep going until shift count
                              ; is neg.
        pop cx,bx,ax,f
        ret

;------ Display string stored in code segment just after CALL to
;------ this routine:
$shows: push  f,ax,bx,bp
        mov bp,sp
        mov bx,[bp+8]         ; Get return addr -
                              ; it points to string
$ss1:   mov al,cs:[bx]        ; Get next char from string
        inc bx                ; Advance string pointer
        or  al,al             ; Reached 0 end of string?
        jz  $ss2              ; Exit if so
        call  $showc          ; Display char
        jmp $ss1              ; Back for more
$ss2:   mov [bp+8],bx         ; reset return address
        pop bp,bx,ax,f
        ret

;------ Macro to display a string.
;------ Example: SHOWS 'Booga booga!'
SHOWS   macro str
        call  $shows
        db  str,0
endm
;------ Macro to display a register. Example:  SHOWR BX
SHOWR   macro reg
        SHOWS ' &reg='
        push dx
        mov dx,reg
        call  $showx
        pop dx
endm

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

Carl's Toolbox

Carl Warren

Starting a new column in a new magazine can be a tricky business. Often you
aren't sure who the reader is or what the interest level might be. Luck and
a great deal of experience seem to be with MSJ. We have a pretty good idea
of who you are and what you want.

Even though the column is called "Carl's Toolbox," the fact is, it's your
toolbox. In amateur radio days we called this our junkbox. That was the old
wooden or paperboard box under the bench that contained odds and ends, a few
chassis and boards that we picked up at a junkyard or swap meet and from
which we all vowed to create a masterpiece of ham radio design.

One old friend of mine used this ploy to smuggle in his brand-new Collins S
line──old-time hams know this was the good stuff. He had convinced his wife
that all those old military chassis, tubes, and other collected junk would
soon be transformed into a work of art──which, some six months and about
$5,600 (equipment was much cheaper in 1959) later, it was.

Times have changed and so has the equipment. Today, prices are a little
better, and yes, you may decide that building is far better than buying,
especially when the tool you need doesn't really exist.

I'm going to tell you not only about the tools you can buy, but about those
that you can build yourself. Since I'll be taking a hands-on approach, I'll
tell you how well various pieces of equipment work, and perhaps some
interesting ways to use them.

Of course, not all the tools and equipment that I'll be talking about come
from manufacturers. Every now and then a few friends and I sit down and try
to noodle our way through a problem, and occasionally we develop something
useful. For example, not too long ago my friend Keith dropped by my office
for a chat. I was complaining about all the problems you run into when you
add a new board to the system or try to get a new device driver to work.
"It's like a continual series of reboots," I said.

Keith, who spends most of his time writing specialized code for various
vendors, asked, "Why don't you do it in software?"

He explained that by using the "A" mini-assembler command in DEBUG, you can
enter the following four instructions that will reboot the system:

  MOV AX,40
  MOV DS,AX
  MOV WORD PTR [0072],1234
  JMP FFFF:0000

The first two instructions (MOV AX,40 and MOV DS,AX) put the hex value 0040
into the 8086 DS register. This points the 8086 at the area of memory in
which the IBM PC's ROM I/O, reset, and power-on routines are located.

The next instruction (MOV WORD PTR [0072],1234) stores the hex value 1234 to
the IBM PC ROM's Reset flag data field. The ROM's power-on routines use this
flag to determine when a Ctrl-Alt-Del reset is in progress or whether a
power-on reset is occurring. The 1234 value transfers control directly to
the power-on routines, and the memory and I/O checks are skipped. If the
Reset flag contains garbage, as it would during power-up, all the checks are
performed. Of course, if you want the checks, change the 1234 to 0000. When
the program is executed, a full I/O and memory check takes place.

That's not a bad little piece of utility software for those of you who
collect routines. I have a few others that will be published in future
issues.

Keeping Track of Changes

IBM Personally Developed Software: SuperC
Donald M. Ludlow and Randy Black, IBM Personally Developed Software; P.O.
Box 3280; Wallingford, CT 06494; (800) 426-7279
Price: $29.95

Whenever I review a product from IBM, I'm always afraid that if I don't like
it, someone will accuse me of being biased against IBM. So, when I was asked
to test-drive IBM's Personally Developed Software package, SuperC, I heard
an anxious voice in the back of my mind say, "What if it's junk?" After all,
the software in this series has ranged from excellent to mediocre, so I
wasn't exactly sure what to expect.

First of all, SuperC is neither a C-language development package nor a
tutor. It is, however, a powerful and useful utility that allows
programmers, writers, editors, and anyone who deals with stored information
that changes to keep track of those changes.

In operation, you can find changes in source code, binary code, or just
plain ASCII words. For example, suppose in updating a document you
accidentally insert a large space or new word in a sentence, something that
just shouldn't be there or that changes your meaning. SuperC uses its word-
compare feature to notify you that a change has occurred.

Of course, in order to compare changes, both an old and new file must exist.
In the case of WordStar users, where a backup copy is kept, the new copy is
compared with the old through the use of simple syntax:

  A>SuperC
    [d:][path]newfile
    [d:][path]oldfile

In some cases, the only thing you would be interested in is whether or not
there are changes in files. When a change is detected by using the file-
compare feature, the program reports that there are indeed differences. At
this time, you can determine whether to search for word, line, or byte
differences.

Within SuperC is the tagged-as-is delta-file (Tasdf) file format, which is
intended for experienced programmers. It generates a special output file
similar to a delta listing with a prefix tagged onto each line, including T)
for title record, I) for inserted line, D) for deleted line, RN for
reformatted line, S) for summary record, and E) for end record.

Although it appears to be just a simple compare program, SuperC includes
capabilities such as bypassing BASIC line numbers, processing assembler
comments and blank lines, and even substitution for nondisplayable
characters. Also, you can specify what line(s) should be ignored in the
process.

Don't be concerned about space, either; SuperC manages just about any length
of file. Recently, I compared a long ASCII file in a standard document of
over 70K in length with no difficulty.

SuperC offers on-line help at the touch of a function key (F1). All
documentation is on-line and can be printed out.

Since the next several columns will address various programming methods,
I'll be using the listing function in SuperC, which prints up to 132 columns
wide.

Getting All the Fax

GammaLink
GammaFAX; 2452 Embarcadero Way; Palo Alto, CA 94303; (415) 856-7421
Price: $995
Includes a synchronous modem board that fits in one slot on an IBM PC, XT,
or AT, or a similar machine, and the GammaFAX software package. The GammaFAX
board provides bidirectional facsimile transmission CCITT group III to and
from PCs, as well as PC-to-PC communications with full data compression and
error checking up to 9,600 bits per second.

As more and more technology comes into the office, the desktop of the future
is getting quite cluttered. The stacks of paper and in-and-out baskets have
been pushed to one side to make room for IBM PCs, printers and facsimile
machines, mice, and whatever else that manufacturers can dream up. At least
one of these desktop items, the facsimile machine, can be moved off the desk
and back to the store room. GammaLink's GammaFAX board and software can take
over the chores once relegated to the standalone facsimile.

Besides fitting into an open slot on the PC backplane, the GammaFAX board
can also be plugged into any available phone jack with an RJ-11 receptacle.
By doing this you can probably reclaim at least 2 square feet of your desk.

The GammaFAX system is capable of taking existing text files or even saved
picture files and shooting them out to a facsimile machine anywhere in the
world. And since facsimile is a two-way street, so is GammaFAX: the machine
can be set to work in an answer facsimile mode or to communicate with other
PCs for very rapid file transfer.

The GammaFAX software is flexible and smart. By entering the DEBUG option,
you can have the program report the status of the operation, thus ensuring a
great deal of confidence in the operation of the line as well as the modem.

A series of menus facilitates use of the program. For example, on start-up
you are greeted with a menu that asks what you want to do: send, receive,
convert programs for facsimile transmission, or set up a string of files for
sending or receiving. You make your choices with the function keys, which
direct you to another level of menus. F10 always takes you back to the
previous level.

The GammaFAX requires the undivided attention of the machine, which is fine
unless you have to get some other work done while sending. The store and
forward function does allow you to set times for a transfer to begin, but
does not let you use the machine.

I set up a Windows definition file for GammaFAX, so it could be called from
Microsoft Windows, and there were no ill effects on either application. It
would be nice, though, if the GammaFAX people would do some Windows
compatibility rewriting so that it can be called as a windowed task and work
in the background.

You can use GammaFAX with a PC, XT, or AT. I strongly recommend the AT
because it lets you use all the speed that the board has. The manual,
although poorly written and in need of a good layout job, contains lots of
information──you just have to search hard for it. Clever programmers will be
able to build a better software mousetrap for communications by using the
board and the information contained in the manual.

The GammaFAX board is essentially a facsimile machine, so software
developers will find it ideal for creating desktop publishing images.
GammaFAX makes it possible to obtain pictures from sources other than
scanners. Scanners are becoming an important tool for desktop publishing, as
they allow photographs or other complex pictures to be scanned in.

There is a problem, though. GammaFAX stores pictures in CCITT group III
format, which is good for facsimile, but not so good for most of the
formats used with bit-image file formats. GammaFAX does provide the
utilities necessary to scan images in with a Canon scanner and convert
them for facsimile transmission.

In the group III format, the least significant bit of the first byte in the
file is the first bit transmitted during the group III phase of the in
message of a facsimile call. The most significant bit of the last byte of
the file is the last bit transmitted. A standard-resolution facsimile page
has 1,728 dots across and 1,075 lines in the vertical dimension. A high-
resolution picture, such as a half-tone, is 1,728 by 2,150. The FAX virtual
page program, supplied with the board, lets you convert the binary image
format into a bit-image format for use with such programs as Media
Cybernetics's Dr. Halo II.

Since the image does have specific characteristics, it can be manipulated
into other forms. The Tag Image File Format (TIFF), a newly proposed format
developed by Aldus Corp., Dest Corp., and Microsoft Corp., provides a
structured format for digital data. TIFF is based on CCITT group III and IV
formats, but it adds additional information for the digital data, including
color and detail about individual pixels, so that a full picture can be
saved.


════════════════════════════════════════════════════════════════════════════


Vol. 2 No. 2 Table of Contents


Microsoft Operating System/2: A Foundation for the Next Generation

Microsoft's joint effort with IBM to develop an operating system for Intel
80286- and 80386-based computers resulted in Operating System/2. Using the
286's protected mode, OS/2 not only breaks the 640K memory barrier, it also
provides multitasking capability and inter-process communication.


OS/2 Windows Presentation Manager: Microsoft Windows on the Future

Included as a standard part of the new Operating System/2, the Windows
presentation manager will provide OS/2 with the same advantages that
Microsoft Windows brings to MS-DOS, such as a standard user interface and a
rich independent graphic application environment.


OS/2 DOS Environment: Compatibility And Transition for MS-DOS Programs

An important OS/2 feature is its MS-DOS compatibility environment, a mode
that emulates DOS 3.x and runs most existing programs. Discussed are the
restrictions of the compatibility mode and the considerations in the
construction of programs designed to run with both MS-DOS and OS/2.


OS/2 Multitasking: Exploiting the Protected Mode of the 286

The Microsoft OS/2 scheduler, the dispatcher, and the protected mode of the
286 are the keys to OS/2's multitasking operation. This article considers
the system developers view of the fundamental elements of multitasking:
processes, threads, and screen groups.


OS/2 Memory Management


OS/2 Inter-Process Communication: Semaphores, Pipes, and Queues

Multitasking operations need fast and reliable communication between
multiple processes and threads. To offer the system developer a wide range
of facilities to choose from, OS/2 depends on shared memory as well as
system semaphores, pipes, and queues for rapid exchange of information.


The MS OS/2 LAN Manager


A Complete Guide to Writing Your First OS/2 Program

Discussions of program development for OS/2 introduce a new vocabulary.
Dynamic linking or module definition files may at first seem forbidding,
but they, like OS/2, are variants of familiar concepts. We offer a sample
program complete with multitasking and inter-process communication.


Turning Off the Car to Change Gears: An Interview with Gordon Letwin

With more than 350,000 lines of code, OS/2 is unquestionably the most
complex software that Microsoft has designed. Microsoft Systems Journal
spoke with Gordon Letwin, the chief architect of OS/2, to learn more about
the challenges of designing Operating System/2.


A Simple Windows Application For Custom Color Mixing

Our regular programming article presents COLORSCR, a deceptively simple
program. On the surface, it provides three scroll bars to adjust the red,
green, and blue components of GDI color specification. The elegance of the
design is its use of child windows to manage the screen area.


Ask Dr. Bob


EDITOR'S NOTE

The recently announced IBM Personal Series/2 computers and the first product
of Microsoft's joint development agreement with IBM, Microsoft Operating
System/2, advance personal computing to new levels of sophistication and
improved performance. For the end user, this translates into greater
ease of use. For the software developer, however, improvements in
technology often mean increasingly complex procedures that bring new
challenges and probably a few frustrations as well.

To help the software developer unravel the intricacies of OS/2, this issue
of Microsoft Systems Journal provides an overview of the new operating
system as well as a detailed look at OS/2's basic features──multitasking,
inter-process communication, and memory management. And MSJ doesn't stop
there──the issue continues to explore OS/2 with articles on the MS-DOS
compatibility environment, the Windows presentation manager, and an
interview with Microsoft's chief architect of OS/2, Gordon Letwin.

What will OS/2 mean to the PC world? Although no one disagrees that the
impact will be deep, standards are not made overnight. Because MSJ is
committed to the concerns and interests of the software developer, we are
planning to bring you a great deal more on OS/2, but certainly not to the
exclusion of MS-DOS. MS-DOS will continue to be important for a long time
to come.

A reminder to DIAL subscribers: Code samples in this and any previous issue
are available on-line. Keep an eye on the DIAL Bulletin Board for more
information on this.

We've said it before, but it's worth repeating. We want MSJ to be your
journal, your forum for exchanging ideas. Write to us with your suggestions
for articles, your reactions to this and previous issues.──Ed.


Masthead

JONATHAN D. LAZARUS
Editor and Publisher

EDITORIAL

TONY RIZZO
Technical Editor

BARRY OWEN
Managing Editor

CHRISTINA G. DYAR
Associate Editor

JOANNE STEINHART
Production Editor

GERALD CARNEY
Staff Editor

DIANA E. PERKEL
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

BETSY KAUFER
Administrative Assistant

Copyright(C) 1987 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, MS-DOS, and XENIX are registered trademarks
and CodeView is a trademark of Microsoft Corporation. Macintosh is a
trademark of Apple Computer, Inc. IBM is a registered trademark and
Personal System/2 is a trademark of the International Business Machines
Corporation. PageMaker is a registered trademark of Aldus Corporation.
UNIX is a registered trademark of AT&T. Compaq is a registered trademark
of Compaq Computer Corporation. Intel is a registered trademark of Intel
Corporation. Paintbrush is a registered trademark of ZSoft Corporation.

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

Microsoft Operating System/2: A Foundation For the Next Generation

───────────────────────────────────────────────────────────────────────────
Also see the related articles:
  Evolution and History of MS-DOS
  CMD.EXE-OS/2 Command Processor
  System Configuration (CONFIG.SYS)
───────────────────────────────────────────────────────────────────────────

Tony Rizzo

After much waiting, speculating, and hoping, the word from Microsoft and
IBM is out, and that word is Operating System/2 (OS/2), their new single-
user, multitasking operating environment. OS/2 is the successor to MS-DOS
(and PC-DOS) and is the first product of Microsoft and IBM's Joint
Development Agreement, which is especially relevant to MSJ's readers.

At the same time that the software announcements were made, IBM launched
its next generation of personal computers, the Personal System/2 series.
On April 2, 1987, a host of new IBM hardware and software offerings were
introduced, but the focus was decidedly on the new IBM machines: an entry-
level 8086 PC and new 80286- and 80386-based machines, built mainly around
in-house VLSI technology and offering substantial price/performance
improvements over the old hardware.

Very briefly, the new systems boast switchless planar (or self-configuring
system) boards with on-board video and disk controllers, greatly expanded
RAM capabilities, 3-1/2" floppies, and new IBM hard files with a 1-to-1
interleave factor. The new 286 and 386 machines also usher in IBM's 16/32-
bit Micro Channel architecture, a new bus structure that greatly improves
data throughput, and a new Video Graphics Array (VGA) display subsystem.
VGA offers built-in, 640-by-480 resolution with 16 colors, which is
necessary for advanced graphics─based software.

For software developers, however, the big news is the new Microsoft OS/2
environment, which will run on the new 286 and 386 machines, as well as on
the older PC AT and compatibles.

───────────────────────────────────────────────────────────────────────────
The Parallel Growth of Hardware and Software. As hardware continues to
provide enhanced and more sophisticated services, the MS-DOS world will
slowly give way to OS/2 and new multi-applications systems.

┌───────────┬─────────────┬────────────────┬───────────────┬──────────────┐
│           │    8088     │      8086      │     80286     │      80386   │
├───────────┼─────────────┼────────────────┼───────────────┼──────────────┤
│MS-DOS 1.x │≡≡≡≡≡≡≡≡≡≡≡≡≡│≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡│═══════════════│──────────────│
│           │             │                │               │              │
│           │             │                │               │              │
│MS-DOS 2.x │≡≡≡≡≡≡≡≡≡≡≡≡≡│≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡│═══════════════│──────────────│
│           │             │                │               │              │
│           │             │                │                              │
│MS-DOS 3.x │≡≡≡≡≡≡≡≡≡≡≡≡≡│≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡│════════REAL MODE ONLY═══════>│
│           │             │                │                              │
│           │             │                │               │              │
│MS-OS/2    │             │                │≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡│≡≡≡≡≡≡≡≡≡≡≡≡≡>│
│           │             │                │               │              │
│           │             │                │               │              │
│MS-OS/2 386│             │                │               │≡≡≡≡≡≡≡≡≡≡≡≡≡>│
└───────────┴─────────────┴────────────────┴───────────────┴──────────────┘
───────────────────────────────────────────────────────────────────────────

The Connection

In contrast to the current situation in which IBM offers PC-DOS and other
manufacturers offer the slightly different MS-DOS, what IBM calls
"Operating System/2 Standard Edition" and what Microsoft calls "Operating
System/2" are in fact one and the same operating system.

Perhaps more important is that the Windows presentation manager, the MS
OS/2 version of Microsoft Windows, is an integral part of the new
operating system. Both MS OS/2 and the Windows presentation manager are in
compliance with IBM's recently announced Systems Application Architecture
(SAA), a set of common specifications created to promote "consistent"
applications development across the entire line of IBM System/370,
System/3x, and personal computer systems.

To date, every version of MS-DOS, including the newly announced Version
3.3, has been a single-process operating system. That is, they can only
execute one application at a time. This restriction is a consequence of
the 8086/8088 architecture, which is limited to 1Mb of memory and offers
no hardware support for protecting memory──any application can access at
will any memory location at any time.

Unrestricted memory and device access by software may very well be a boon
to those creative programmers who look to squeeze every bit of performance
out of the MS-DOS, 8086/8088 environment, but it poses serious problems
for those trying to run more than one application at the same time.
Furthermore, the memory map of the MS-DOS, 8086/8088 environment leaves
only 640Kb of main memory available for both applications and the
operating system.

Though MS-DOS has been continually refined─especially in its file
management, device support, background processing (e.g., print spooling),
network support, and very rudimentary attempts at multitasking──it has
remained fundamentally a single-user, single-application design.


Almost the Promised Land

Without hardware-based protection mechanisms, an operating system would
have a very difficult time managing more than one application; preventing
applications from accessing conflicting memory locations becomes almost
impossible. Intel addressed this problem by including a "protected mode"
of operation in the design of its 80286 processor. On account of this,
expectations for an enhanced operating system were high when IBM
introduced the 286-based PC. The 286 processor's protected mode of
operation, its ability to address up to 16Mb of memory, and the fact that
the 286 also provided a "real" mode fully compatible with the 8086/8088
suggested the potential for a more powerful operating system.

Unfortunately, the 286 was designed before the importance of the IBM PC
(8088) standard was established. As a result, the 286 cannot
simultaneously support both real and protected modes─a major drawback.
Adding to the problem, the 286 real mode has every deficiency of the 8086
environment: the 640Kb barrier, no memory protection, and, as a result, no
multiple-application operation.

Because of these 286 shortcomings, it was not feasible to build a
protected-mode operating system with MS-DOS compatibility before the
release of the PC AT. So, rather than take a half-step, Microsoft and IBM
decided to invest in a new, extensive development effort that would focus
on a protected-mode operating system (see "Turning Off the Car to Change
Gears: An Interview with Gordon Letwin,"). The new operating
system would take advantage of the hardware, break the 640Kb memory
barrier, allow a multi-application, multitasking environment, and still
permit total compatibility with MS-DOS applications.

The result of this effort is MS OS/2.

───────────────────────────────────────────────────────────────────────────
Block Diagram of the MS OS/2 Environment. MS OS/2 is a complicated set of
interrelated modules working together to provide a complete multitasking,
virtual memory environment.

                                         Application
            Application               (or Library Routine)
            │   │     │  Dynamic Link   │      │   │
            │   │     │    Function     │      │   │
            │   │     │ Call Interface  │      │   │
            │   │     ▼                 ▼      │   │
            │   │  ┌─System Session Manager─┐  │   │
            │   │  │           ▼            │  │   │
            │   │  │Console Device Interface│  │   │
            ▼   ▼  ▼           ▼            ▼  ▼   ▼
        ╔══════════════════════════════════════════════╗
        ║                  Kernel                      ║
        ║ ┌───────────┐       Process Management       ║
        ║ │File system│       Memory Management        ║
        ║ └───────────┘       Program Management       ║
        ║                     Time Management          ║
        ║                     I/O Management           ║
        ╚══════════════════════════════════════════════╝
                               ↕
        ░░░░░░░░░░░░░░░░ Device Drivers ░░░░░░░░░░░░░░░░
                               ↕
        ░░░░░░░░░░░░░░░░░░░ Hardware ░░░░░░░░░░░░░░░░░░░

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

The Real Thing

MS OS/2 is a priority-based, pre-emptive, multitasking environment which
includes a new application interface that isolates applications from low-
level hardware differences between machines running MS OS/2. OS/2 utilizes
the protected mode of the 286 chip to allocate an exclusive address space
to each application. OS/2 controls access to memory, so that different
applications cannot corrupt each other's data and program areas.

In protected mode, the 286 uses the contents of the segment register to
access a local descriptor table (LDT) containing information about each
application. The contents of the segment register serve as an index into
the descriptor table. The operating system also accesses a global
descriptor table (GDT), which contains information on code and data that
is shared by all executing applications (see Figure 1). Both of these
tables are exclusively managed by the operating system, and together they
allow up to the entire 16Mb of real memory to be mapped to up to 1
gigabyte of virtual memory.

Also, application programs are normally prevented from writing directly to
the underlying hardware. The 286 architecture provides for discrete I/O
privilege levels (IOPL) to be assigned to applications; this capability
enables the operating system to control access to hardware. Although it is
possible for an application to obtain hardware control if it can gain
access to an appropriate privilege level, the operating system generally
does not explicitly make that privilege available to an application.
Protected mode and IOPL are the basic, 286 hardware──based underpinnings of
MS OS/2 (for a detailed discussion of privilege levels and how OS/2
performs multitasking, see "OS/2 Multitasking: Exploiting the Protected
Mode of the 286,").


Memory Management

In protected mode OS/2 can simultaneously run many applications, any one
of which may cause the amount of physical memory available to the system
to be exceeded. When an application's memory requirements exceed real
memory (referred to as "memory overcommit"), OS/2 will swap memory
segments to disk as required (see Figure 3). OS/2 dynamically expands and
contracts allocated memory and collects fragmented memory allocations
(known as "memory compaction"), which provides efficient control of
available memory.

Microsoft has gone to great lengths to protect the enormous investment in
pre-MS OS/2 software. To achieve this goal, OS/2 provides a compatibility
mode in which real-mode MS-DOS applications can run (see Figure 3). The
compatibility mode offers an 8086 environment in which most MS-DOS
programs will run. However, a small number of programs, especially those
that directly access hardware or with timing dependence, won't run in
compatibility mode.

Compatibility mode allows for one MS-DOS program to execute in the
foreground; background, light protected-mode applications are suspended
when in this mode. A program executing in the compatibility mode can only
run in the foreground─such a program is suspended if one of the background
protected-mode applications is selected for foreground operation.


Command Processors

Microsoft OS/2 comes with two command processors, COMMAND.COM and CMD.EXE.
The former is the real-mode command processor, similar to the MS-DOS
command processor, with its familiar initialization, resident, and
transient parts. As in MS-DOS, COMMAND.COM is used to start programs when
in compatibility mode.

CMD.EXE is the MS OS/2 command processor, which offers an extended command
syntax. Most important, however, is that CMD.EXE is both shareable and
swappable. This means that multiple copies of CMD.EXE can be executed, but
only one copy of CMD.EXE is actually kept in memory, and that copy is
shared by all protected-mode applications. The only additional memory used
is that which is unique to any given application.


Presentation Manager

OS/2 consists of two fundamental, interrelated parts. The first is the
kernel, which provides for the major internal workings of the operating
system. Integrated into the MS OS/2 system is the Windows presentation
manager. Like Microsoft Windows for MS-DOS, it is a consistent, graphic
user interface and a device-independent applications environment.

The MS OS/2 Windows presentation manager is derived from the current
Microsoft Windows, Version 1.03, software. The MS OS/2 Windows
presentation manager, however, has been significantly enhanced; it has
been designed to use multiple overlapping windows and to conform to the
Common User Access component of IBM's SAA.

The key word is consistency. Microsoft will soon release Windows, Version
2.0, for the MS-DOS environment with the same user interface as the
Windows presentation manager. The visual interface will be consistent in
both MS OS/2 and MS-DOS. Also, because the Windows presentation manager is
consistent with SAA, this new standard is expected to eventually find its
way into a large number of computer systems, from micros to mainframes.

For the end user, the Windows presentation manager offers

  ■  the ability to run graphics-oriented programs
  ■  a simple and consistent user interface
  ■  access to the session manager
  ■  enhanced mouse and keyboard control

And, for the software developer, the Windows presentation manager
provides

  ■  access to the MS OS/2 multitasking and memory management kernel
     functions

  ■  access to intertask communication and data interchange facilities

  ■  a device-independent output architecture

  ■  a rich and sophisticated graphics library


The Session Manager

The new Windows environment gives the user full control over the operating
environment. The windows environment accomplishes this by offering users a
session manager, which allows users to control the status of concurrently
running applications, as well as that of any application running in
compatibility mode. The user requests access to the session manager with
the SysReq key, and the session manager responds with a list of active
applications (see Figure 4☼). The user can then bring any process to the
foreground, start any number of new processes, or terminate any number of
existing processes. There is a lot more to the session manager, however,
than meets the eye.


The New Frontier

When the session manager is active, it switches between different screen
groups. To understand screen groups we need to take a close, if somewhat
brief, look at the activity underlying executing applications. MS OS/2
applications are made up of one or more processes, which are in turn made
up of one or more threads. A thread is an "atomic unit," a unique entity
that the MS OS/2 scheduler can allocate CPU cycles to, that is, it is
actually executable by the CPU. A process can have any number of
executable threads, each of which can be in one of three states: blocked,
ready to execute, or executing.

Processes are, in turn, assigned to screen groups. Screen groups are
collections of processes that write to a common virtual display and
receive input from a virtual keyboard. As an example, a screen group might
be a collection of related windows, each of which is running its own
process and each of which was started inside a parent window (screen).

The screen group writes to a virtual display and reads from a virtual
keyboard because it is possible to execute any number of screen groups,
each with its own screen and keyboard needs. Since there is only one
physical screen and keyboard, there must be some system to prevent
applications from conflicting with each other. MS OS/2's solution maps
each screen group to its own unique virtual screen, eliminating any
possible conflicts.

When a user requests access to an active application, the session manager
maps that application's screen group to the physical screen─more commonly
known as the foreground. MS OS/2 manages the mapping of virtual screens to
the physical screen with the session manager (see Figure 5). The user has
complete control over which applications MS OS/2 will bring to the
foreground, and over initiation or termination of new applications (and
their associated screen groups).

There is a lot going on here. Obviously, OS/2 has provided the user with a
great deal of flexibility. But what has the software developer gained?


API and FAPI

MS OS/2 offers a rich application program interface that allows software
developers to fully exploit the resources of the operating system. For
example, the Windows presentation manager provides a large set of
sophisticated graphics functions, giving the software developer the
building blocks necessary to create very sophisticated screen images.

MS OS/2 provides a substantial set of CALL-based functions──189 of them to
be exact──to assist in the development of programs that will run in a
multitasking environment. These functions, which handle file and device
management, memory management, and task management, are known as the
kernel, or operating system Application Programming Interface (API).

The API includes facilities to write device drivers, known as the Device
Driver Interface. Device drivers handle device initialization, transfer of
data to and from devices, and device error control, and they provide
access to low-level BIOS functions. High-level languages such as C can
call these functions directly. These callable functions completely
eliminate the need to depend on interrupts (for example, Interrupt 21
function calls) and the need to write directly to hardware.

API also provides functions to deal with the complexities of inter-
process communication (IPC). Processes and threads communicate through
semaphores, queues, pipes, and shared memory (see "OS/2 Inter-Process
Communication: Semaphores, Pipes, and Queues,"). API's functions
make such complex programming significantly easier to write.

Microsoft will support all of these functions as a standard. There will be
a substantial improvement in the ability to port software to future
releases of MS OS/2, as well as an enhanced ability for the products of
different software developers to interact with each other. Microsoft
suggests that applications conforming to the API standard will run within
the MS OS/2 environment and future releases of OS/2. This creates an
opportunity to standardize coding practices throughout the entire PC
environment.

Realistically, OS/2 won't become as pervasive as MS-DOS for some time yet.
It will most likely be at least 3 years before 286- and 386-based machines
take over the major share of the PC market. MS-DOS, Version 3.3, will
remain with us for the foreseeable future, so developers will want the
ability to write software that can move between MS-DOS and MS OS/2.

This need, however, has already been anticipated and resolved; Microsoft
guarantees to support a subset of the API function set that will execute
in every available mode of the two operating environments. Dubbed the
Family Application Programming Interface (FAPI), this set of 91
functions──25 of them are somewhat restricted──will permit programs to
execute under MS-DOS 3.3, within the compatibility mode of MS OS/2, and as
an actual, although limited, MS OS/2 application (see Figure 6).

API/FAPI represents a unique opportunity for software developers to create
applications that can deal with current limitations while incorporating
future advances, thus providing users to run state-of-the-art applications
on older machines.


Software Assistance

By August, Microsoft plans to release a Software Development Kit (SDK)
that will contain the full specifications of MS OS/2─as well as a beta-
release version of the operating system kernel. The toolkit will come with
C Version 4.5, MASM Version 4.5, and the CodeView debugger.

The new compiler and assembler will be able to generate code to be used
with the new Microsoft Dynamic Linker. The linker will allow code to be
linked with FAR calls, creating external references to various libraries.
These external references eliminate the need to link library routines into
each object (or load) module. The Dynamic Link Editor services program
needs by loading library routines only when necessary.


The LAN Manager

The LAN Manager will play an important role in the MS OS/2 environment and
can be fully integrated into MS OS/2. It promises an exciting new
applications environment in which different applications, such as
spreadsheets, word processors, and database systems, will be able to
communicate with each other not only application-to-application (e.g.,
spreadsheet to word processor) within one processor, but also among
processors connected to a network. The LAN API component allows developers
to take full advantage of the capabilities of the LAN Manager so as to
build truly distributed applications. It will also include site-to-site
electronic-mail capabilities and resource sharing. (For more information
on the LAN API, see "The MS OS/2 Lan Manager,").


Help from the 286

The 386 chip provides much faster speed, 1 gigabyte of addressable real
memory, and a huge virtual address space─64 terabytes! It is also capable
of running "virtual" 8086 machines, obviating the need for a compatibility
mode. Users can run multiple MS-DOS sessions within the multitasking
environment.

Microsoft estimates that only 10 percent of the MS OS/2 code will have to
be modified or rewritten, primarily in the memory management component, to
utilize the 386. This code modification will be completely transparent to
applications written under the MS OS/2 API on the 286 chip; MS OS/2 has
been standardized in this respect. With the next release of MS OS/2,
software developers may gain access to additional tools, but they won't
have to rewrite any code because of changes made to the existing API. This
means, of course, that software developers can begin now to create on the
286 applications that can easily be ported to the 386──a substantial
development advantage.


A New Philosophy

MS OS/2 is obviously a greatly improved and sophisticated piece of
software. Software development under this new environment will require a
considerable effort to understand just how all of the pieces fit together.
It will take some time before developers can claim mastery of all the
tools currently available.

There will also be a shift in emphasis from programming around the
operating system to programming with it. Instead of hunting for
undocumented interrupts that perform unsupported services and bypassing
the operating system to get to the hardware, developers will learn to
integrate all of the tools now at their disposal. With new hardware that
virtually eliminates performance bottlenecks and a new operating system
that takes full advantage of that hardware, developers can focus directly
on their applications.


Conclusion

Although they are not exactly right around the corner, it won't be long
before we will have applications "systems" that interact with each other,
work together across networks, and share resources efficiently in
environments spanning micros, minis, and mainframes.

What will become of the "under-the-hood" programmer? What about all of
those who still long to hack, to get their fingers dirty by directly
driving hardware, manipulating code and data segments, and fooling with
undocumented system calls? Well, MS OS/2 is undoubtedly full of mysterious
code to decipher, but the real challenge will be fully utilizing the many
tools and facilities that are now available to create state-of-the-art
applications. For the serious microcomputer software developer the future
is MS OS/2, and the future is now.


───────────────────────────────────────────────────────────────────────────
Evolution and History of MS-DOS
───────────────────────────────────────────────────────────────────────────

IN AUGUST 1981 Microsoft introduced MS-DOS, Version 1.0, a simple
operating system providing support for a new IBM machine and a fledgling
industry. Over the last six years the machine and the industry have grown
up, and MS-DOS has evolved into MS OS/2, a very sophisticated,
multitasking operating environment.

Presented here is a brief but complete history of MS-DOS. It is hoped that
the reader will find it entertaining, nostalgic, and perhaps revealing.
How many readers know, for example, that the original design of the File
Allocation Table (FAT) can be traced to Mr. Gates himself?

1974: Intel introduces the 8 bit 8080 processor.

January 1975: MITS introduces the $400 Altair computer; it has no
keyboard, no monitor, no disk, and no operating system.

February 1975: Paul Allen and Bill Gates develop and sell their own
version of BASIC to MITS for the Altair.

February 1976: Paul Allen, now working for MITS, asks Bill Gates to write
a disk-based version of BASIC for the Altair.

Bill Gates creates a working model of his disk BASIC in 10 days. He
designs a disk layout and file structure based on a centralized File
Directory and File Allocation Table (FAT). He also includes a rudimentary
set of file services in the disk BASIC he is developing.

1976-1978: Microsoft Disk BASIC is ported to all major 8 bit personal
computers. An assembler and linker are developed for 8080- and Z80- based
systems.

April 1978: Intel announces the 8086, a 16 bit processor.

January 1979: Tim Paterson of Seattle Computer Products begins work on a
plug-in 8086 processor card to bring the power of the 8086 to the S-100
bus.

June 1979: Microsoft and Tim Paterson show Microsoft's BASIC running on
Paterson's 8086 card at the National Computer Conference in New York.

April 1980: Delays hold up the delivery of CP/M 86. Tim Paterson decides
to write his own "Quick and Dirty" OS, which becomes known as 86-DOS. He
incorporates the FAT structure first designed by Bill Gates for Disk-
BASIC, and some features and techniques underlying MS-DOS.

August 1980: IBM takes its first step towards producing the IBM PC,
planning to use readily available, off-the-shelf 8 bit hardware. IBM
visits Microsoft, asking if Microsoft can write a ROM-based BASIC for the
computer IBM is developing.

Microsoft suggests that IBM consider the 16 bit architecture. IBM's
"Project Chess" goes on to become the 8088 (8086-based) IBM PC.

The first working version of 86-DOS runs on Tim Paterson's 8086 card. This
is essentially the birth of what will become known as MS-DOS.

September 1980: IBM asks Microsoft to provide COBOL, FORTRAN and Pascal
for their personal computer. Microsoft suggests to IBM that an operating
system would be necessary to develop the additional languages.

October 1980: Microsoft submits a proposal to IBM that includes MS-DOS.

November 1980: The proposal is accepted by IBM. A prototype machine
arrives at Microsoft and a small DOS team begins a concentrated period of
work.

February 1981: 86-DOS runs on the prototype for the first time. Over the
next half year the OS is refined and becomes MS-DOS, Version 1.0.

August 1981: IBM introduces the IBM PC, and announces three operating
systems: MS-DOS, CP/M 86, and the P System. For several months MS-DOS is
the only OS available. It is also priced substantially lower than CP/M.

MS-DOS incorporates a number of advanced features:

  ■  Greater device independence.
  ■  Greater data integrity.
  ■  Simplified handling of peripheral devices.
  ■  A replaceable COMMAND.COM processor.

February-April 1982: Microsoft introduces for the MS-DOS environment:

  ■  The Macro Assembler.
  ■  The FORTRAN Compiler.
  ■  The COBOL Compiler.

June 1982: MS-DOS, Version 1.1, is announced, providing support for
double-sided, eight sector diskettes on the IBM PC.

March 1983: IBM announces the PC XT.

Microsoft announces the release of MS-DOS, Version 2.0, which includes:

  ■  Supports for hard disks including a XENIX(c)-like hierarchical
     directory structure.

  ■  File Handles, allowing programs to reference files anywhere they
     have been loaded.

  ■  The ability to redirect the I/O of a program to any file or device,
     and implements pipes and filters.

  ■  Support  for nine-sectored diskettes, increasing storage to 360 Kb.

  ■  Provision for installable device drivers, promoting logical device
     independence.

  ■  Background printing through PRINT.COM.

  ■  ANSI.SYS device driver, providing support for serial "stream"
     processing to give cursor positioning and color control information to
     the monitor.

March 1984: IBM introduces the PCjr with half-height disk drives, and MS-
DOS, Version 2.11, is introduced, providing support for it.

August 1984: IBM announces the 80286 based PC AT, with a 20 Mb hard disk
and 1.2 megabyte high density diskette drive.

Microsoft introduces MS-DOS, Version 3.0, which includes:

  ■  A rewritten MS-DOS kernel and a new standard set of I/O calls.

  ■  ISO Open System Interconnect (ISO) based model for networking.

  ■  Network redirector and file sharing support for the IBM PC Network
     Adapter Card.

November 1984: Microsoft introduces MS-DOS, Version 3.1, and  Microsoft
Networks (MS-Net) including:

  ■  Redirector and file sharing services for non-IBM network cards.
  ■  Transport and server functions to all MS-DOS systems.
  ■  Network spooled printing.
  ■  The basic structure for Installable File Systems.

June 1985: Microsoft, Intel, and Lotus establish the Lotus-Intel-Microsoft
Extended Memory Specification, allowing programs to access memory beyond
the MS-DOS limit of 640 Kb of RAM.

August 1985: IBM and Microsoft enter into a Joint Development Agreement.

January 1986: Microsoft introduces MS-DOS, Version 3.2, which includes
support for 3.5" diskettes.

April 1987: IBM announces the Personal System/2 series of computers.

Microsoft announces MS OS/2, with an integrated Windows presentation
manager, MS-DOS, Version 3.3, Windows 2.0, and a new LAN Manager.


───────────────────────────────────────────────────────────────────────────
CMD.EXE-OS/2 Command Processor
───────────────────────────────────────────────────────────────────────────

OS/2 provides the user with a new command processor that extends the
functionality of the command language and provides enhanced batch file
support.

Command Line Operators

The following shows both the old operators as well as the new, from
highest to lowest precedence.

command.com           cmd.exe         Function

                         ^            Lexical Escape Charactor
                         ()           Command Grouper
    <                    <            I/O Redirector -+  Same
    >                    >            I/O Redirector  |->Precedence
    >>                   >>           I/O Redirector -+     Level
    |                    |            Pipe Operator
                         &&           And Operator
                         ||           Or Operator
                         &            Command Separator

The operators common to both environments work the same way they
previously did.

The Lexical Escape Character allows the precedence operators to be used as
regular charactors. Refer to the following,

  1. echo go home > kid
  2. echo go home ^> kid

In the first case a file called kid is created with the line "go home" as
its contents. In the second, "go home > kid" is echoed to the screen.

The Command Grouper works in exactly the same way parentheses work in
equations, to establish explicit precedence. Refer to the following,

directory a: contains the files temp1, temp2 and temp3

  temp1 contains "a"
  temp2 contains "b"
  temp3 contains "c"

  1. dir a:*.* >> temp1 & (temp1 >> temp2 & temp2 >> temp3)
  2. (dir a:*.* >> temp1 & temp1 >> temp2) & temp2 >> temp3

After execution of the first command line temp1, temp2 and temp3 will look
as follows

  temp1 contains a,temp1,temp2,temp3
  temp2 contains b,a
  temp3 contains c,b,a

If the second line had been executed instead, temp1, temp2 and temp3 would
look as follows

  temp1 contains a,temp1,temp2,temp3
  temp2 contains b,a,temp1,temp2,temp3
  temp3 contains c,b,a,temp1,temp2,temp3

The parentheses determine the order of execution. Additionally, lower
precedence operators can be executed prior to those of higher precedence.

The Pipe and Redirection operators work in the same way they previously
did.

The And operator performs the command to its right if and only if the
command to its left successfully executes. For example, in

  dir exist.txt && copy exist.txt lpt1

if the DIR command is successful, then exist.txt is printed. If DIR was
not successful the command on the right will not be executed.

The Or operator performs one of two commands depending on the success or
failure of the first command given. For example, in the following

  more < document.txt || more < document.doc

more will either output the contents of document.txt and then exit, or, if
document.txt doesn't exist, will output the contents of document.doc.

The Command Separator allows more than one command to be typed on a
command line. For example,

  dir a:\*.* > temp & dir b:\*.* >> temp

places the directory listing of the root directory of a: in a file called
temp (which is either overwritten if it exists or is created if it doesn't
exist) and then immediately executes the next command, which appends the
contents of the root directory of b: to temp.

BATCH FILE ENHANCEMENTS

OS/2 supports all of the old batch file commands and adds four new ones.
They are

  CALL
  EXTPROC
  SETLOCAL
  ENDLOCAL

The Call command finally provides a means for users to chain together
batch files so that when a called batch file ends it returns control to
the calling batch file.

Extproc allows a user to run batch files designed for command processors
other than CMD.EXE. The command is issued as the first line of the batch
file and defines the alternate command processor.

Setlocal allows the user to define local drive, directory and environment
variables for the current batch file. When the command is issued it saves
the existing drive, directory and environment information and replaces it
with the user specified information.

In conjunction with the setlocal command, the endlocal command restores
the drive, directory and environment information previously saved through
setlocal, and allows additional setlocal calls.

If a setlocal call is issued and the batch file issuing the setlocal call
terminates without issuing an endlocal call, the operating system restores
the original drive, directory and environment information, insuring that
the state previously known to CMD.EXE still exists.


───────────────────────────────────────────────────────────────────────────
System Configuration (CONFIG.SYS)
───────────────────────────────────────────────────────────────────────────

CONFIG.SYS is the file that contains the commands used to configure the
system. Only a single CONFIG.SYS file is needed to configure the system
for both real-mode and protected-mode operations.

During system initialization, OS/2 opens and reads the CONFIG.SYS file in
the root directory of the drive from which it was started and interprets
the commands within the file. If the file is not found, OS/2 assigns
default values for the configuration commands.

The following list summarizes the configuration commands for OS/2:

  buffers        Determine the number of buffers to allocate for disk I/O.

  country        Select the format for country-dependent information.

  device         Specify path and filename of a device driver to be
                 installed.

  iopl           Specify whether I/O privilege is to be granted or not.

  maxwait        Set the time limit for calculating CPU starvation.

  memman         Select memory management options.

  priority       Disable dynamic priority variation in scheduling regular
                 class processes.

  protectonly    Select the modes of operation.

  protshell      Specify path and filename of the OS/2 top-level command
                 processor.

  run            Start a system or daemon process during system
                 initialization.

  swappath       Specify the location of the swap file.

  timeslice      Set the time slice values for process scheduling.

  threads        Set the maximum number of threads in the system.

  trace          Select system trace.

  tracebuf       Set the system trace buffer size.

The following commands are ignored:

  files
  lastdrive

The following commands apply only to the configuration for real-mode and
only if the MODE command specifies that old applications will be run.
These real-mode-only commands are documented after the OS/2 configuration
commands.

  break          Check for Ctrl-Break.
  fcbs           Determine file control block management information.
  rmsize         Select the amount of memory for real-mode applications.
  shell          Load and start the top-level command processor.


Figure 1:  Virtual/Real Memory Mapping. The global descriptor table (GDT)
           and every local descriptor table (LDT) can each map 8191 64K
           segments or .5 Gb (8191 x 64K bytes= .5 Gb). There is one GDT for
           the entire MS OS/2 system, shared by every application. Each
           specific application has its own private LDT. Together, the
           shared GDT and the private LDT provide a virtual address space of
           up to one Gb for each application. The virtual addresses are
           managed by the operating system, and are mapped to a real memory
           space of up to 16Mb.

      Application Address Spaces

       Application n-┌──────────┐
                   ┌─┴─────────┐│
                 ┌─┴──────────┐││
                ┌┴──────────┐ │││    1Gb      Real
  Application 1-│ ─ ─ ─ ─ ─ │ │││     |      Memory
     ┌───────── │_ _Code_ _ │ │││     |     ┌───────┐16M      Secondary
     │          │   Data    │ │││     |     ≈       ≈          Storage
  Unique        ├ ─ ─ ─ ─ ─ ┤ │││     |     ├ ─ ─ ─ ┤         ┌────────┐
  to Each       ≈           ≈ │││    LDTs   │ Code  │╔═══════╗│   ..   │
  Application   │           │ │├┘     |     ├ ─ ─ ─ ┤║Virtual║├─ ─ ─ ─ ┤
     │          ├─ ─ ─ ─ ─ ─┤ ├┘      |     │ Code  │╣Memory ╠│  Code  │
     │          │   Data    ├─┘       |     ├ ─ ─ ─ ┤║ Mgt   ║├ ─ ─ ─ ─┤
     ╞════════  ├─ ─ ─ ─ ─ ─┤ ─ ─ ─ ─5Gb─ ─ │ Data  │╚═══════╝│  Data  │
     │          ≈           ≈         |     ├ ─ ─ ─ ┤         ├─ ─ ─ ─ ┤
  Shared        ├─ ─ ─ ─ ─ ─┤         |     ├ ─ ─ ─ ┤         │   ..   │
                │System Code│        GDT    │ System│         └────────┘
     │          │ and Data  │         |     └───────┘
     └──────────┴───────────┘         |


Figure 2:  Protected Mode Memory Map

                      ╔═════════════╗
                      ║ Real Memory ║
                      ╚═════════════╝
                 16M╔═══════════════════╗ ───┐
                    ║  New Application  ║    │ Moveable
                    ≈   code and data   ≈    ├─Swappable or
                    ║     segments      ║    │ Non-swappable
                  1M╟───────────────────╢ ───┘
                    ║                   ║
                    ║  BIOS and video   ║      Fixed
                    ║     buffers       ║
                640K╟───────────────────╢ ───┐
                    ║                   ║    │
                    ║  New Application  ║    │ Moveable
                    ≈   code and data   ≈    ├─Swappable or
                    ║     segments      ║    │ Non-swappable
                    ║                   ║    │
                    ╟─ ─ ─ ─ ─ ─ ─ ─ ─ ─╢ ───┘
                    ║       OS/2        ║      Fixed
                    ║                   ║
                   0╚═══════════════════╝


Figure 3:  Compatibility Mode Memory Map

                        ╔═════════════╗
                        ║ Real Memory ║
                        ╚═════════════╝
                  16M╔═══════════════════╗ ───┐
                     ║  New Application  ║    │ Moveable
                     ≈   code and data   ≈    ├─Swappable or
                     ║     segments      ║    │ Non-swappable
                     ╟─ ─ ─ ─ ─ ─ ─ ─ ─ ─╢ ───┘
                     ║       OS/2        ║  Fixed
                   1M╟───────────────────╢ ───
                     ║  BIOS and video   ║  Fixed
                     ║     buffers       ║
                 640K╟───────────────────╢
                     ╟─ ─ ─ ─ ─ ─ ─ ─ ─ ─╢ ───┐
                     ║      Single       ║    │
                     ≈ "old" Application ≈    │
                     ║                   ║    ├─Variable size
                     ╟───────────────────╢    │
                     ║   "MS-DOS 3.x"    ║    │
                     ╟───────────────────╢ ───┘
                     ║       OS/2        ║      Fixed
                     ║                   ║
                    0╚═══════════════════╝


Figure 5:  OS/2 maps multiple virtual I/O to the
           physical screen and keyboard.

                            Screen Groups
    ┌─────────┐             ┌─────────┐                 ┌─────────┐
    │         │             │▓▓▓ ┌──┐ │                 │         │
    │    C>   │             │▓☻▓ │/\│ │                 │    ?    │
    │         │             │    └──┘ │                 │         │
    └─────────┘             └─────────┘                 └─────────┘
   Character User        Presentation Manager          Special Purpose
     Interface        Graphics Desktop Publishing           │
      Light WP                    │                         │
      Database      ┌─────────────▼────────────────┐        │
     Spreadsheet ──►│ Visual Console I/O Management│◄───────┘
                    └──────────────────────────────┘
                          ╔════════════════╗   ┌────────────────┐
                          ║ ░░░░░░░░░░░░░░ ║█  │ Physical Video │
                          ║ ░░░░░░░░░░░░░░ ║█  │    Manager     │
                          ║ ░░░░░░░░░░░░░░ ║█  └────┬───────────┘
                          ║ ░░░░░░░░░░░░░░ ║█───────┘
                        ╔═╝────────────────╚═╗
                        ║ ‼‼‼‼‼‼‼‼‼‼‼‼‼‼‼‼‼  ║█
                        ╚════════════════════╝█
                          ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀


Figure 6:  FAPI assures the software developer that an application will
           run under both MS-DOS 3.x and MS OS/2.

                              16M ┌──────────────────────────┐
                                  │  ┌────────────┐          │
                                  │  │   OS/2     │          │
                                  │  │ ┌──────────┴───┐      │
                                  │  │ │      OS/2    │      │
                                  │  │ │  ┌───────────┴────┐ │
                                  │  │ │  │      OS/2      │ │
                                  │  └─┤  │    Protect     │ │
                                  │    │  │     Mode       │ │
                                  │    └──┤    Full New    │ │
                                  │       │      API       │ │
                                  │       └────────────────┘ │
              MS-DOS 3.n          │     ┌────────────────┐   │
      ┌──────────────────────┐    │     │  "Family API"  │   │
      │       Reserved       │◄───┼────►│   Applications │   │
      ├──────────────────────┤  1M├ ─ ─ └────────────────┘ ─ ┤
      │        DOS 3.n       │    │         Reserved         │
      │     Compatibility    │640K├ ─ ─ ┌────────────────┐ ─ ┤
      │      Environment     │────┼────►│     DOS 3.n    │   │
      └──────────────────────┘    │     │  Compatibility │   │
                                  │     │   Environment  │   │
                                  │     └────────────────┘   │
                                0 └──────────────────────────┘

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

The OS/2 Windows Presentation Manager: Microsoft Windows On the Future

Manny Vellon

Last April, Microsoft announced a series of new products, including a new
operating system, Microsoft Operating System/2 (OS/2). An important part
of this announcement was the inclusion of Microsoft Windows as a standard
part of the OS/2 operating system. Currently, Microsoft Windows must be
purchased separately and installed under MS-DOS.

The Windows presentation manager will be tightly integrated with OS/2 and
will provide the same benefits that Microsoft Windows provides to MS-DOS:
a windowed, graphical user interface and support for a variety of input
and output devices. Through the presentation manager, OS/2 will replace
the well-known A> prompt of MS-DOS with window-based screens.

This union of Windows with OS/2 strengthens the role of Windows in future
system and application products and also addresses the ease-of-use issues
that have been associated with MS-DOS and IBM PC software. Windows
provides a more intuitive interface that allows novice users to learn
products more quickly.

Also important is IBM's support of Microsoft Windows in its new products.
This support ensures that Windows will become a standard part of the
operating environment and encourages other hardware manufacturers to
support it as well.


Protected Mode

The new IBM PS/2 series, the IBM PC AT, the Compaq 386, and other Intel
286- and 386-based computers are capable of running in real mode or
protected mode. In real mode, they operate much like the Intel 8088-based
IBM PC: they are limited to using 1 megabyte (Mb) of memory, they can
usually run only one program at a time, and they are vulnerable to
crashing when a program goes awry. In protected mode, the 286- and 386-
based computers don't have those limits.

Under OS/2's protected-mode operating system, programs are no longer
limited to 640K; applications can take advantage of up to 16 Mb of real
memory and up to 1 gigabyte of virtual memory. OS/2 is able to perform
multitasking──it can run several programs simultaneously by quickly
switching among them. OS/2 is more robust than real-mode operating
systems; programs run independently of each other, and if one crashes
it is less likely to affect the others.

Currently, Microsoft Windows can also do many of these functions. By
performing a variety of sophisticated functions, Windows can take
advantage of extended memory if available and can provide rudimentary
multitasking. With the OS/2 Windows presentation manager, however, these
functions are much easier to implement since the underlying operating
system kernel includes these capabilities.


User Interface

In addition to this symbiosis "under the hood," OS/2 Windows is integral
to the operating system at the user level. The Windows presentation
manager will be the standard user interface for the new operating system.
Users won't have to learn about disk directories, filenames, and cryptic
commands; executing programs and managing the OS/2 file system with the
OS/2 Windows presentation manager will be intuitive and fast.

Although the new OS/2 Windows bears a strong resemblance to today's
Microsoft Windows, there are some substantial differences between the two.
Most significant are the differences in user interfaces. To eliminate
these differences so as to produce a common user interface, Microsoft has
announced a new version of Microsoft Windows──Version 2.0 (see Figure 2).
This version, while still a real-mode version for MS-DOS, employs the same
user interface to be used by the OS/2 Windows presentation manager.

When Microsoft Windows was first developed, it used tiled windows (see
Figure 1☼).
Overlapped windows were considered too slow and unusable with
low-resolution displays. In order to respond to customer requests for
overlapping windows, and because optimized graphics algorithms and
improved processing speeds have eliminated performance bottlenecks that
are found in older technology, the new Windows uses overlapped windows
instead of tiled windows.

New Windows products will also have improved keyboard interfaces. Although
Windows is best used with a mouse, it is possible, and sometimes
preferable, to use it solely through the keyboard. Changes in the user
interface will make this easier.

First, the keyboard interface has been improved in order to allow direct
access to items in dialog boxes. In Figure 4☼, for example, the user can
type Alt-F to quickly position the cursor in the filename field of the
dialog box. Access to menus from the keyboard has also been enhanced by
allowing the developer to select any letter from a menu command to execute
the item rather than being limited to the first letter, as in the case of
Windows Version 1.03 and Version 1.04. This helps to make it easier for
software developers to provide meaningful command names while still
providing fast keyboard access to commands.

The second significant change affects mouse operation. Currently, Windows
employs pull-down menus. You click down on the menu bar to make visible a
pop-up menu; then, while holding down the mouse button, you drag down to
the desired command in the pop-up menu and let go of the button. This
technique is fast, but it is prone to accidental selections and requires
considerable manual dexterity. The new interface allows you to click and
let go on the menu bar to make the pop-up menu visible and then click down
on the desired command within the given menu.

Finally, the new interface employs some new terminology (for example,
minimize and maximize, instead of icon and zoom) and some additional
keyboard operations.

Users who are accustomed to the old keyboard interface won't be forced to
learn the new one. Microsoft Windows, Version 2.0, will work in both the
old and new styles to help users get accustomed to the new interface.
However, the OS/2 Windows presentation manager will use this new interface
only.

───────────────────────────────────────────────────────────────────────────
Progression for Windows-based Applications

                          Windows 1.03
                                ▼

                         Optional Changes
        • Edit Resource Files       • Enhanced Printer Interfaces
        • Implement Mult Doc Facil  • Error Condition Support
                                ▼

                           Windows 2.0
        • Overlapped Applications   • EMS/EEMS Support
        • New User Interface        • Display Performance
                   • Multiple Document Facilities
                                ▼

        • Mechanical API Changes    • Mode Rules
        • GDI-to-GPI                • Access to OS/2 Facilities
                   • Conform to Protected
                                ▼

                          OS/2 Windows
        • Overlapped Applications   • Preemptive Multitasking
        • New User Interface        • Large Memory (16Mb)
                   • GPI and New Device Drivers

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

API

Although Microsoft Windows, Version 2.0, and the OS/2 Windows presentation
manager appear to be similar and have many common functions, programs
written for Microsoft Windows will have to be modified to work with the
OS/2 Windows presentation manager. OS/2 restricts several operations in
programs; for example, software interrupts are not allowed. Programs
written for MS-DOS will also have to be modified to work in OS/2.

Besides the changes required by the new operating system, such as
replacing software interrupts with operating system calls, other changes
have been made to the OS/2 Windows Application Program Interface (API) in
order to standardize coding practices, improve error handling, and exploit
new graphics capabilities. Fortunately, many of these changes are
"mechanical" in nature. To follow the new coding practices, for example,
all window manager calls are preceded with "Win": "WinCreateWindow",
"WinShowWindow", etc. Other changes entail reordering parameters.
Applications that make heavy use of graphics will require the most
changes; the OS/2 Windows presentation manager contains a new graphics
library that requires a variety of changes to be made to the API.


The Future

OS/2 is not intended to replace MS-DOS. Microsoft expects that real-mode
(MS-DOS) and protected-mode (OS/2) products will continue to co-exist
until Intel 286- and 386-based computers predominate. MS-DOS and Microsoft
Windows will continue to be developed and marketed for low-end machines,
while OS/2 addresses newer, more-powerful computers. The Windows user
interface will serve as a bridge for users, allowing them to operate each
of these classes of machines regardless of the underlying hardware.

In time, Microsoft expects Intel 386-based machines to become the standard
hardware environment for powerful new business and engineering
applications. Microsoft will extend OS/2 to exploit the additional
capabilities of the Intel 386 and will upgrade OS/2 in the future. Once
more, the Windows presentation manager interface will help to keep these
changes invisible to the user.


Figure 2:  Comparison of Current and Future Windows Versions

╓┌───────────────────────────────┌────────────────┌──────────────┌───────────╖
                                 Microsoft
                                 Windows         Microsoft      MS-OS/2
                                 1.03 & 1.04     Windows 2.0    Windows

Presentation Spaces              Tiled           Overlapped     Overlapped

More Consistent User                ---          Yes            Yes
and Keyboard Interface

Processor Environments           8088, 8086,     8088, 8086,    286, 386
                                 286, 386        286, 386

Large Memory Support                ---          EMS/EEMS       16Mb

Multitasking                     Non             Non            Fully
                                 Preemptive      Preemptive     Preemptive

Enhanced Display Performance        ---          Yes            Yes
                                 Microsoft
                                 Windows         Microsoft      MS-OS/2
                                 1.03 & 1.04     Windows 2.0    Windows
Enhanced Display Performance        ---          Yes            Yes

Runs Existing Windows (1.03)     Yes             Yes            Yes
Application

Graphics API                     GDI             GDI            GDI

Multiple Document Interface         ---          Yes            Yes

Device Drivers                      ---          Enhanced       New Model

Old Application Support             ---          Improved       Improved

Integral Part of OS                 ---          ---            Yes

Protected Mode Execution            ---          ---            Yes
Applications Execution

                                 Microsoft
                                 Windows         Microsoft      MS-OS/2
                                 1.03 & 1.04     Windows 2.0    Windows

New Development API                 ---          ---            Yes

New User "Shell"                    ---          ---            Yes
and Keyboard Interface


Figure 3a:  Microsoft Windows API. Programs written for MS-DOS will
            have to be modified to work in OS/2. Many of these changes
            are mechanical in nature──for example, all window manager
            calls are preceeded with "Win". Compare the above Windows 2.0
            code with that shown in Figure 3b below.

                                    ∙
                                    ∙
                                    ∙
int PASCAL WinMain( hInstance, hPrevInstance, lpszCmdLine, cmdShow )
HANDLE hInstance, hPrevInstance;
LPSTR lpszCmdLine;
int cmdShow;
{
        MSG   msg;
        HWND  hWnd;
        NPWNDCLASS       pHelloClass;
                                    ∙
                                    ∙
                                    ∙
     /* Allocate and initialize class data structure pHelloClass */
                                    ∙
                                    ∙
                                    ∙
         /* Create a window class */
        if (!RegisterClass( (LPWNDCLASS)pHelloClass ) )
                return FALSE;

        /* Create an application window */
        hWnd = CreateWindow((LPSTR)"Class", (LPSTR) "Title",
                            WS_TILEDWINDOW, 0, 0, 0, 0, (HWND)NULL,
                            (HMENU)NULL, (HANDLE)hInstance,
                            (LPSTR)NULL );
                                    ∙
                                    ∙
                                    ∙
        /* Process messages*/
        while (GetMessage((LPMSG)&msg, NULL, 0, 0)) {
                TranslateMessage((LPMSG)&msg);
                DispatchMessage((LPMSG)&msg);
                }

                return (int)msg.wParam;
}

CAPTION: OS/2 Windows Presentation Manager Program
                                    ∙
                                    ∙
                                    ∙


Figure 3b: OS/2 Windows presentation manager API (see figure 3a)

int cdecl main(argc, argv)
int argc;
char *argv[];
{
        QMSG qmsg;
        HAB    hab;
        HMQ    hmq;
        HWND   hwnd, hwndFrame;

        /* get an anchor block */
        hab = WinInitialize();

        /* create a message queue for the application */
        hmq = WinCreateMsgQueue(hab, 0);

        /* create a window class */
        if (!WinRegisterClass(  (LPCH) "Class",
                                WndProc,
                                WS_SYNCPAINT | WS_CLIPSIBLINGS |
                                WS_SIZEREDRAW,
                                0,
                                NULL)) return FALSE;

        /* create an application window */
        hwndFrame =
                WinCreateStdWindow(NULL,
                                   FS_MENU | FS_TITLEBAR | FS_MINMAX |
                                   FS_SIZEBORDER | FS_SYSMENU,
                                   (LPSTR) "Class",
                                   (LPSTR) "Title", 0L, NULL,
                                   IDM_APPMENU,
                                   (HWND far *)&hwnd);
                                    ∙
                                    ∙
                                    ∙
        /* process messages */
        while (WinGetMsg(hab, (LPQMSG)&qmsg, NULL, 0, 0))
                WinDispatchMsg(hab, (LPQMSG)&qmsg);

        /* destroy the resources used by the application */
        WinDestroyWindow(hwndFrame);
        WinDestroyMsgQueue(hmq);
        WinFinalize(hab);
}

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

The OS/2 MS-DOS Environment:Compatibility and Transition for MS_DOS Programs

───────────────────────────────────────────────────────────────────────────
Also see the related article:
  Some Programming Don'ts for OS/2
───────────────────────────────────────────────────────────────────────────

Joel Gillman

Microsoft's multitasking, virtual memory operating system has finally been
unveiled. Now that MS OS/2 is a reality, programmers may be wondering with
some trepidation whether they'll have to rewrite all their MS-DOS
programs.

The good news is that nearly all of your existing code, both commercial
products and custom-made utilities, will run just fine in OS/2's MS-DOS
compatibility environment, an operating mode that emulates MS-DOS 3.3.
Programs that need low-level access will not run in the compatibility
mode, but these are exceptions. Most spreadsheets, word processors, and
many other applications programs will work just fine.

However, once you start writing code to run in the multitasking protected
mode of OS/2, you will have to do some things differently. There's a
silver lining in this, because you will end up with better code once
you've unlearned some of the bad habits you picked up writing in MS-DOS.
Multitasking implies resource sharing, so you can't be overly greedy with
system resources anymore. But what you may lose in resource access you
gain in the power of multitasking.

After booting OS/2, the user is greeted by the session manager, from which
the user can start any number of protected-mode applications or enter
compatibility mode. For each protected-mode program, the session manager
creates a new protected mode execution environment, called a screen group.
Each screen group has a command processor (CMD.EXE, which corresponds to
the MS-DOS 3.x COMMAND.COM), a virtual screen buffer, a virtual keyboard,
and a virtual mouse, along with a virtual memory space of up to 16
megabytes (Mb) depending on the total number of screen groups.


Compatibility Mode

The MS-DOS compatibility mode in OS/2 uses the 80286 chip's real mode,
which is engaged by a technique called mode switching (see "OS/2: Turning
Off the Car to Change Gears,"). Mode switching emulates a full
system reset without disrupting operation, allowing the processor to
switch from protected to real mode. The compatibility mode gives the
system an 8086 interface with 1 Mb of address space and emulates MS-DOS
3.x with the MS-DOS SHARE utility installed. OS/2 needs up to 500 Kb of
system memory in a typical configuration──substantially more than the 50 Kb
required by MS-DOS 3.x.

The compatibility mode recognizes all of the documented MS-DOS services. A
number of undocumented interrupt 21H services also exist under MS-DOS 3.x,
but since OS/2 does not recognize most of the undocumented MS-DOS
services, programs that use them won't run in the compatibility mode.

The compatibility mode supports a ROM data area and accepts service
interrupts, but you cannot call ROM services directly by address. You must
use the interrupt 10 through interrupt 1A services instead (see Figure 1).
Applications can call any hardware interrupt except the CMOS
clock/calendar interrupt or an interrupt already taken over by any OS/2
device driver other than the keyboard. The compatibility mode will issue
interrupt 28h (spooler interrupt)──so you can still run SideKick(R)──and
interrupt 24h (critical error handler).

A program running in the MS-DOS compatibility mode freezes up if the
system places it in the background by switching to the protected mode.
Programs frozen in the background receive no CPU service or interrupts. A
program that determines the time of day by counting clock ticks, for
example, will generate inaccurate times if it goes into the background.

Version-specific applications won't run in the compatibility mode, because
the MS-DOS version number of OS/2 is 10. A way around this is to modify
the code so that it calls DOSGETVERSION and, if it gets back 10,
determines which mode it's executing by calling DOSGETMACHINEMODE. The
program can then take the appropriate action.


Handling Devices

Most of your old device drivers will not run in compatibility mode. OS/2
doesn't support any of the MS-DOS block device drivers, such as those used
with disk or tape drives. The only character device drivers it supports
are VDI (video display interface) and CON (console) drivers, since
character device drivers will work only if they are polled rather than
interrupt-driven. The system supports all of the device driver command
packets shown in Figure 2.

When you run a driver, only programs running in the compatibility mode can
use its device. The device isn't available to protected-mode applications.
Device drivers cannot call user code, because they operate at a higher
privilege level than the user program.

Drivers are installed the same way as in MS-DOS(R), using the configuration
command

  device = driver filename

MS-DOS device drivers are loaded and initialized in compatibility mode.
Initialization is in most respects the same as in MS-DOS, except that no
interrupt 21h (hardware-independent) functions can be performed from the
initialization code.

Compatibility mode restricts which devices programs can manipulate. Sound-
generating programs that need a higher frequency time base for more
precise pitch control can remap the 8253 clock/timer chip, that is, assign
different numbers to its system interrupts. Remapping the 8259 interrupt
controller is not allowed. Such programs, which must remap the 8259 in
order to trap keystrokes, will not run. Applications can still hook
keystrokes after OS/2 gets them, however.

Programs that need low-level disk I/O access for copy protection purposes
cannot remap the disk controller. Programs do have direct access to the
disk controller via interrupt 13h (floppy disk services), interrupt 25h
(absolute disk read), and interrupt 26h (absolute disk write). Note that
interrupt 13h and interrupt 26h are not allowed for fixed media such as
hard disks.

High-speed communications applications that must remap the DMA controller
won't run because the operating system remaps the controller. Applications
can remap the COM, AUX, and parallel ports, although using one of these
ports in the compatibility mode makes it unavailable to protected mode
programs and vice versa.


80286 Restrictions

Programmers writing applications for MS-DOS developed some programming
techniques and coding shortcuts to enhance performance, even though the
books and manuals tended to discourage using them. Many of these
techniques won't work in OS/2 because of differences between the 8086 or
8088 chip and the 80286. You'll have to observe several restrictions if
you want to run your applications in the compatibility environment or in
protected mode.

First, don't use wrap-around segment offsets. The 8086 and 8088
microprocessors translate an out-of-range address value into something
recognizable, but the 80286 doesn't. You cannot address beyond the
allocated size of a segment; the system aborts the program if an offset
larger than the segment descriptor's limit value is used to reference that
segment.

The 80286 doesn't allow writable code segments. One of the bits in the
segment descriptor identifies the segment as either code or data. A code
segment's descriptor doesn't have a read/write bit, so only valid code
segments can be placed in the CS (code segment selector) register, and a
program may not write into valid code segments. However, an alias can be
used to make a data segment into a code segment to be executed.

Since different machines use different timing speeds, don't count on the
CPU clock as a timing constant. This is a typical problem in copy-
protected programs.

Don't allow a division-trap handler to resume execution in the original
code stream unless it is able to detect and understand differences between
the 8086 or 8088 and the 80286. After a division-error trap, the 80286
points to the division instruction (including prefixes) and doesn't change
the register values. The 8086 and 8088 point to the instruction following
the division instruction and may change the DX:AX or AH:AL register sets
as well.

The 80286 CL (low-order loop/shift/repeat count) registers won't permit
shift counts greater than 31. On the 80286, the shift and rotate counts
are masked to 5 bits.

When executing the PUSH SP (push stack pointer onto stack) instruction,
the 80286 pushes the SP value onto the stack before incrementing the
value, while the 8086 and 8088 push the SP value after incrementing it.
Few programs use this particular code sequence, but for those that do,
Microsoft offers this way around the problem:

  MOV AX,SP
  PUSH AX

these two lines of code can be implemented by a macro.

The PUSHF instruction followed by a POPF may change the contents of the
flag register in the 80286, since more flag bits are defined in the 80286
flag word than in those of the 8086 or 8088. Also, because of a bug in the
80286, the POPF may change the contents of the flag register in the
80286. You should use flag-specific instructions to set or test flag
register values instead of setting or testing for explicit bit patterns.


FAPI

In order to permit you to write new programs guaranteed to run in MS-DOS
and in OS/2's protected mode (both current and future versions), Microsoft
has defined a set of system calls that are guaranteed to support both
environments. This set of system calls, the Family Application Program
Interface (FAPI), is a subset of the full OS/2 Application Program
Interface (API).

Five types of code will run on OS/2: old programs designed for MS-DOS 3.x
that run on OS/2 in the compatibility mode; presentation manager programs,
FAPI programs that run on OS/2 in the compatibility mode; FAPI programs
that run on OS/2 in protected mode; and new programs that run on OS/2 only
in the protected mode. Figure 3 summarizes the characteristics valid for
each of these program types.

FAPI allows a program to be linked to run in both modes and includes
system calls in all categories except those specific to protected mode,
such as multitasking, run-time linking, and device monitors. Some
restrictions apply to using the FAPI calls in the compatibility mode,
which are discussed in detail in the OS/2 Programmer's Guide.

The protected mode does not give you direct access to the screen, as you
would have with a MS-DOS 3.x BIOS call because of its memory protection.
Instead, you have access to a virtual screen buffer within each screen
group. The screen buffer paints the screen only when that screen group is
active. For bimodal compatibility, FAPI provides a subset of the video
input/output (VIO) calls for creating and writing to a virtual screen
buffer.

Just using FAPI calls, however, won't guarantee protected mode
compatibility. Basically, you want a well-behaved program; it shouldn't
sneak past the operating system to the hardware or get too clever with the
segment registers. To write code that operates in protected mode, you give
up sovereignty over the hardware. Multitasking requires that the operating
system, rather than your program, allocate hardware and system resources.

If you've done any programming in XENIX or UNIX, you're used to this.
However, if you've only worked in MS-DOS, you'll want to adjust your
thinking a little.

OS/2 uses an indirect segment addressing scheme: a segment number points
to a table entry, called a segment decriptor, which in turn points to the
memory space. OS/2's memory management service will change the memory
pointer in the descriptor as the total system memory allocation changes,
so there is no way of knowing where a given segment number actually puts
you in physical memory. Thus, a well-behaved program doesn't try to
interpret segment numbers or calculate other segment locations from a
given segment number.

You can't assume that any given segments overlap or don't overlap, nor can
you assume any particular relationship between segment:offset combinations
and physical memory. The segment number is nothing more than a segment ID,
with no particular significance apart from that.

The segment registers are intended for the storage of valid segment
numbers. If you store invalid numbers there──for example, by using the
segment registers as scratch-pad memory (the 8086 doesn't seem to have
enough registers to suit some people)──your program will crash.

Your program cannot issue a CLI (clear interrupt) instruction in protected
mode because this causes a protection trap. When in compatibility mode,
IRET (interrupt return) restores the previous value of IFLG (interrupt
flag), but IRET has no effect in protected mode. Protected mode programs
can interact directly with hardware only by linking to a special I/O
Privilege Level Segment. This allows access to the 80286 processor's Ring
2 security level.


Bimodal Device Drivers

OS/2 supports bimodal device drivers that run in either mode, obviating
the need for the system to switch modes to process interrupts.

Unlike a MS-DOS 3.x device driver, a bimodal OS/2 driver has to support
multiple synchronous and asynchronous requests. However, the basic
structure remains pretty much the same: the driver contains a strategy
routine and an interrupt routine. In addition, some device drivers may
have to include a routine to trap ROM BIOS software interrupts from
compatibility mode.

An application program uses a request packet to call the strategy routine,
just as with a MS-DOS 3.x driver. The strategy routine determines whether
a request is valid and places valid requests in a queue to the device,
using the DevHlp functions to manage the queue. If the device is idle, the
strategy routine starts it and returns to the operating system, which
suspends the thread until the request has been executed.

When the device completion interrupt occurs, the interrupt routine sets
the return status in the request packet and removes that packet from the
queue. It then calls a DevHlp routine, DevDone, to tell OS/2 that the
request is complete.

The strategy routine should disable interrupts when checking to see if the
device is active and when examining the queue. This protects the interrupt
routine from other driver request interrupts. When interrupts are
reenabled, the interrupt routine will receive only ones of higher
priority.

This is only a sample of some design considerations in writing OS/2
bimodal device drivers. The subject is treated at greater length in the
OS/2 Device Driver Guide.


New Tools

A new C compiler that runs in both modes is included in the OS/2 Software
Development Kit. However, executable files written in Microsoft C
Compiler, Version 4.0, should have no trouble running in the compatibility
mode, as long as they meet the general criteria outlined earlier for
operation in OS/2. You don't need to recompile or relink them with the new
OS/2 C run-time library.

The new C library is nearly identical to the Version 4.0 library, which is
designed for single-thread execution only. Most of the functions are not
re-entrant and therefore will not work in a multithread process. Figure 4
lists the re-entrant functions that may be used in multithread programs.

All the routines in Figure 4 will work properly in programs that use a far
data model (compact and large model). However, in near-data-model-memory
(small- and medium-model) programs, only some of these routines are
guaranteed to function properly. The others have model-dependent pointers
in their interfaces and have the potential to allocate stack space outside
the default segment (SS!=DS).

The kit also contains a new macro assembler that will run in protected
mode. Again, existing MASM executable files will run in the compatibility
mode, subject to the general restrictions described earlier.

The MS-DOS 3.2 network function calls aren't supported in the
compatibility mode, but the new Microsoft OS/2 LAN Manager will allow
networking in protected mode.


Device Monitors

One problem that has plagued MS-DOS programmers is that when several
terminate-and-stay-resident (TSR) programs are loaded into memory,
they tend to step on each other in order to catch a reactivate keystroke
from the keyboard. When you put a TSR like Borland's SideKick, Rosesoft's
ProKey, or North Edge Software's TimeSlips into the background, the
program calls interrupt 21h function 31h (Terminate and Stay Resident) and
remains in memory even though you return to the system prompt. The program
is still watching the keyboard, trapping keystrokes before the system gets
them. When you hit a hot key combination, such as Ctrl-Alt, the program
pops back onto the screen. The problem is that when two or more pop-up
programs reside in the background at the same time, each wants to trap the
keyboard interrupt before any other program does (see "Moving Toward an
Industry Standard for Developing TSRs," MSJ, Vol. 1, No. 2).


OS/2 solves this problem in protected mode by giving each TSR its own
keyboard device monitor for reading keystrokes. Suppose you want to have
several TSRs written by different vendors available within a given screen
group. Each one will have its own keyboard device monitor. The monitors
receive keystrokes in the order of monitor registration, which is set when
the programs are first run. Keystrokes are passed on to the first monitor
registered, which can trap the keystroke and generate a response or pass
the keystroke on the next monitor registered (see Figure 5).

Obviously, in this scheme, no two TSRs can use the same reactivate-key
sequence. You can't reactivate a pop-up program in one screen group from a
different screen group. By definition, a screen group is made up of a
virtual screen, virtual mouse, virtual keyboard, and virtual memory
space, and screen groups can't talk to each other.


The Tradeoff

The constraints that OS/2 imposes on the programmer may seem at first
rather strict. With MS-DOS, you are pretty much free to use the system
services or to leave them alone and go directly to the CPU instead. But in
a multitasking system, you just can't do that. OS/2 takes over the CPU and
the hardware, granting access only in certain instances.

On the other hand, a lot of old problems go away. Applications will no
longer have to fight each other for system resources, because OS/2
allocates resources among them on a priority basis. Applications will have
a common program interface (API) to work with, ensuring future
compatibility. What you lose in resource access you gain in the new
opportunities of multitasking, such as the potential for real-time
multithread applications.

Similarly, the memory access restrictions are compensated for in protected
mode by the greatly increased size of the memory space that you can work
with and the addition of memory management capabilities. The only
limitation is that you must use system calls for access rather than
stuffing numbers directly into the segment registers. And the 80286 chip
is fast enough that efficiency issues aren't as critical as they were on
the original PCs and XTs, so programming shortcuts such as segment games
just aren't necessary anymore.


Figure 1:  Hardware/Operating Environment Compatibility Chart. Hardware
           compatibility and support is different for DOS 3.3 and OS/2.
           OS/2 seeks to insulate applications software from the hardware.

                                           ┌──────────OS/2─────────┐
                                DOS 3.3    Compatibility      New
                                            Environment     Programs
 Supported Hardware              8088            --             --
                                 8086            --             --
                                  286           286
                                  386           386

 Available Memory                640K           840K           16MB
 Can overcommit Memory            --             --             YES
 True Multitasking                --             --             YES
 Use Software Interrupts         YES            YES              NO
 Use Hardware Interrupts         YES            YES              NO
 Use undocumented DOS            YES           Some              NO
   Interfaces
 Have direct access to Hardware  YES            YES             YES
 Can run in the Background        NO             NO             YES
 Obey 286 Segment Rules           NO             NO             YES


Figure 2:  Device Driver Commands. Shown here are the device driver commands
           supported under OS/2.

Code      Command

  0       Init
  3       IOCtl Input
  4       Input (Read)
  5       Non-Destructive Input No Wait
  6       Input Status
  7       Input Flush
  8       Output
  9       Output with Verify
 10       Output Status
 11       Output Flush
 12       IOCtl Output (Write)
 13       Device Open
 14       Device Close
 15       Generic IOCtl


Figure 3:  Software Compatibility Chart. Programs may run in one or more
           modes depending on how they behave.

╓┌──────────────────────┌─────────────┌─────────────┌────────────┌───────────╖
                        ┌─ DOS ──┐    ┌────────────────── OS/2 ───────────┐
                       Old Programs  ┌─────FAPI Programs─────┐  New Programs

Start With             COMMAND.COM   COMMAND.COM   COMMAND.EXE  CMD.EXE

Can Run in OS/2
Compatibility Box      Yes           Yes           No           No

Can Run in Background  No            No            Yes          Yes

                        ┌─ DOS ──┐    ┌────────────────── OS/2 ───────────┐
                       Old Programs  ┌─────FAPI Programs─────┐  New Programs

Permit Old-Style INT
21H Dos 3.x
Interrupts             Yes           No            No           No

Permit Undocumented
Dos Interfaces         No            No            No           No

Have IOPL              Yes           Via FAPI      Via FAPI     Via OS/2

Obey 286 Segment
Rules                  No            Yes           Yes          Yes

Can Overcommit
Memory                 No            No            Yes          Yes

Addressable Memory     640K          640K          16M          16M

Permit Software
                        ┌─ DOS ──┐    ┌────────────────── OS/2 ───────────┐
                       Old Programs  ┌─────FAPI Programs─────┐  New Programs
Permit Software
Interrupts             Yes           Via FAPI      No           No

Permit Hardware
Interrupts             Yes           No            No           No

Program Residence      Below         Below         Above        Above
                       Boundary      Boundary      Boundary     Boundary

Permit Multitasking    No            Yes           Yes          Yes


Figure 4:  OS/2 API Calls

Family API (FAPI) functions are highlighted

* Indicates that FAPI support is limited (certain restrictions are
  imposed)

╓┌─────────────────────┌─────────────────────────────────────────────────────╖
API Function Name     Description

BadDynLink            Bad Dynamic Link
DosAllocHuge*         Allocate Huge Memory
DosAllocShrSeg        Allocate Shared Segment
DosBeep               Generate Sound From Speaker
DosBufReset           Commit File Cache Buffers
DosCaseMap*           Perform Case Mapping on String of
                      Binary Characters
DosChdir              Change Current Directory
DosChgFilePtr         Change File Read/ Write Pointer
DosClose              Close File Handle
DosCloseQueue         Close Queue
DosCloseSem           Close System Semaphore
DosCreateCSAlias      Create CS Alias
DosCreateSem          Create System Semaphore
DosCreateThread       Create another thread of execution
DosCreateQueue        Create Queue
DosCWait*             Wait for child termination
DosDelete             Delete File
API Function Name     Description
DosDelete             Delete File
DosDevConfig          Get Device Configuration
DosDevIOCtl*          I/O Control for Devices
DosDupHandle          Duplicate File Handle
DosEnterCritSec       Enter Critical Section of Execution
DosError*             Enable Hard Error Processing
DosExecPgm*           Execute Program
DosExit*              Exit Program
DosExitCritSec        Exit  Critical Section of Execution
DosExitList           Routine List for Process Termination
DosFileLocks*         File Lock Manager
DosFindClose*         Close Find Handle
DosFindFirst*         Find First Matching File
DosFindNext*          Find Next Matching File
DosFlagProcess        Set Process External Event Flag
DosFreeModule         Free  Dynamic-Link Module
DosFreeSeg            Free Segment
DosGetCtryInfo*       Get Country-Dependent Formatting Information
DosGetDateTime        Get Current Date and Time
DosGetDBCSev          Get DBCS Environmental Vector
API Function Name     Description
DosGetDBCSev          Get DBCS Environmental Vector
DosGetEnv             Get Address of Process
DosGetHugeShift       Get Shift Count
DosGetInfoSeg         Get Address of System VariablesSegment
DosGetMachineMode     Return Current Mode of Processor
DosGetMessage         Get System Message and Insert Text Strings
DosGetModHandle       Get Dynamic-Link Module Handle
DosGetModName         Get Dynamic-Link Module Name
DosGetProcAddr        Get Dynamic-Link Procedure Address
DosGetPrtyGet         Process Priority
DosGetShrSeg          Access Shared Segment
DosGetVersion         Get Dos Version Number
DosGiveSeg            Give  Access to Segment
DosHoldSignal*        Disable/Enable Signal
DosInsMessage         Insert Variable Text Strings In Message
DosIOAccess           Request I/O Access to Device
DosKillProcess        Terminate Process
DosLoadModule         Load  Dynamic-Link Module
DosMakePipe           Create Pipe
DosMkdir              Make Subdirectory
API Function Name     Description
DosMkdir              Make Subdirectory
DosMonClose           Close Connection to OS/2 Device Driver
DosMonOpen            Open  Connection to OS/2 Device Driver
DosMonRead            Read  Input from Monitor Structure
DosMonReg             Register Set of Buffers as Monitor
DosMonWrite           Write Output to Monitor Structure
DosMove               Move File or Subdirectory
DosMuxSemWait         Wait  for one of n semaphores to be cleared
DosNewSize            Change File Size
DosOpen*              Open File
DosOpenQueue          Open Queue
DosOpenSem            Open Exisiting Semaphore
DosPeekQueue          Peek Queue
DosPurgeQueue         Purge Queue
DosPutMessage         Output Message Text To Handle
DosQCurDir            Query Current Directory
DosQCurDisk           Query Current Disk
DosQFHandState        Query File Handle State
DosQFileInfo*         Query File Information
DosQFileMode          Query File Mode
API Function Name     Description
DosQFileMode          Query File Mode
DosQFSInfo            Query File System Information
DosQHandType          Query Handle Type
DosQueryQueue         Query Queue
DosQVerify            Query Verify Setting
DosRead               Read from File
DosReadAsync          Asynchronous Read from File
DosReadQueue          Read  from Queue
DosReAllocHuge*       Change Huge Memory Size
DosReAllocateSeg*     Change segment Size
DosResumeThread       Restart Thread
DosRmdir              Remove Subdirectory
DosSelectDisk         Select Default Drive
DosSemClear           Clear (release) Semaphore
DosSemRequest         Request Semaphore
DosSemSet             Set Semaphore Owned
DosSemSetWait         Set Semaphore and wait for next Clear
DosSemWait            Wait for Semaphore to be Cleared
DosSetDateTime        Set Current Date and Time
DosSetFHandState*     Set File Handle State
API Function Name     Description
DosSetFHandState*     Set File Handle State
DosSetFileInfo        Set File Information
DosSetFileMode        Set File Mode
DosSetMaxFH           Set Maximum File Handles
DosSetPrty            Set Process Priority
DosSetSigHandler*     Handle Signal
DosSetVec             Establish Handler For Exception Vector
DosSetVerify          Set/Reset Verify Switch Delay ProcessExecution
DosSubAlloc           Suballocate Memory within Segment
DosSubFree            Free Memory Suballocated within Space
DosSubSet             Initialize or Set Allocated Memory
DosSuspendThread      Suspend Thread Execution
DosSystemService      Dos System Process Services
DosTimerAsync         Start Asynchronous Timer
DosTimerStart         Start Periodic Interval Timer
DosTimerStop          Stop  Asynchronous or Interval Timer
DosPTrace             Interface for Program Debugging
DosWrite              Synchronous Write to File
DosWriteAsync         Asynchronous Write to File
DosWriteQueue         Write to Queue
API Function Name     Description
DosWriteQueue         Write to Queue
KbdCharIn             Read Character Scan Code
KbdFlushBuffer        Flush Keyboard Buffer
KbdGetStatus          Get Keyboard Status
KbdPeek*              Peek at Character-Scan Code
KbdRegister           Register keyboard Subsystem
KbdSetStatus          Set Keyboard Status
KbdStringIn           Read Character String
MouClose              Close Mouse Device For Current Screen
GroupMouDrawPtr       Release Screen Area For Device Driver Use
MouGetDevStatus       Get Current Pointing Device Driver Status Flags
MouGetEventMask       Get Current Pointing Device One- Word Event Mask
MouGetNumButtons      Get Number of Buttons
MouGetNumMickeys      Get Number of Mickeys-Per-Centimeter
MouGetNumQueel        Get Current Status for Pointing  Device Event Queue
MouGetScaleFact       Sets  Scale Factors for Current Pointing Device
MouOpen               Open  Mouse Device For Current Screen Group
MouReadEventQue       Read  Pointing Device Event Queue
MouRegister           Register Mouse Subsystem
MouRemovePtr          Reserve Screen Area For Application Use
API Function Name     Description
MouRemovePtr          Reserve Screen Area For Application Use
MouSetEventMask       Assign New Event Mask To Current Pointing Device
MouSetHotKey          Set System Hot Key
MouSetPtrShape        Set Pointer Shape and Size
MouSetScaleFact       Set Scale Factors for Current Positioning Device
VioEndPopUp           Deallocate a Pop-up Display Screen
VioGetAnsi            Get ANSI State
VioGetBuf             Get Logical Video Buffer
VioGetCurPos          Get Cursor Position
VioGetCurType         Get Cursor Type
VioGetPhysBuf         Get Physical Video Buffer
VioPopUp              Allocate Pop-up DisplayScreen
VioPrtScreen          Print Screen
VioReadCellStr        Read Character-Attribute String
VioReadCharStr        Read Character String
VioRegister           Register Video Subsystem
VioSavReDrawWait      Screen Save RedrawWait
VioScrLock*           Lock Screen
VioScrollDn           Scroll Screen Down
VioScrollLf           Scroll Screen Left
API Function Name     Description
VioScrollLf           Scroll Screen Left
VioScrollRt           Scroll Screen Right
VioScrollUp           Scroll Screen Up
VioScrUnLock          Unlock Screen
VioSetAnsi            Set ANSI On or Off
VioSetCurPos          Set Cursor Position
VioSetCurType         Set Cursor Type
VioSetMode            Set Display Mode
VioShowBuf            Display Logical Buffer
VioWrtCellStr         Write Character-Attribute String
VioWrtCharStr         Write Character String
VioWrtCharStrAttr     Write Character String With Attribute
VioWrtNAttr           Write N Attributes
VioWrtNCell           Write N Character-Attributes
VioWrtNChar           Write N Characters
VioWrtTty             Write TTY String


Figure 5:  When TSR programs are run in protected mode, OS/2 assigns each a
           keyboard device monitor, which reads keystrokes from the keyboard.
           This keeps the TSRs from fighting each other to be the first to
           trap keystrokes.

╔═══════════╗  ╔═══════════╗  ╔═══════════╗  ╔═══════════╗  ╔═══════════╗
║     1     ║  ║     2     ║  ║     3     ║  ║     4     ║  ║     5     ║
║           ║  ║           Pop-Up Applications           ║  ║           ║
╚═══════════╝  ╚═══════════╝  ╚═══════════╝  ╚═══════════╝  ╚═══════════╝
                                                              
┌─────╨─────┐  ┌─────╨─────┐  ┌─────╨─────┐  ┌─────╨─────┐  ┌─────╨─────┐
│     1     │  │     2     │  │     3     │  │     4     │  │     5     │
│           │  │         Keyboard Device Monitors        │  │           │
└────────╥──┘  └────────╥──┘  └────────╥──┘  └────────╥──┘  └───────────┘
         ║             ║             ║             ║        
┌────────║────────║─────║─────────║────║─────────║────║────────║────────┐
│        ╚════════╝     ╚═════════╝    ╚═════════╝    ╚════════╝        │
├ ─ ─ ─ ─ ┐                                                             │
│Interrupt│              Keystroke Distribution                         │
└────────┴────────────────────────────────────────────────────────╥────┘
     ╚═══════╗                                             ╔═══════╝
     ┌───────╨─────────────────────────────────────────────▼───────┐
     │                   Keyboard Device Driver                    │
     └────────────────────────────────────────────────────────────┘
         ║


───────────────────────────────────────────────────────────────────────────
Some Programming Don'ts for OS/2
───────────────────────────────────────────────────────────────────────────

When writing programs for either the compatibility mode or protected mode
in OS/2, you must observe a number of restraints that weren't necessary in
writing for MS-DOS. Many of these restrictions arise from differences
between the 8086 or 8088 and the 80286 microprocessors.

  ■ Don't depend on segment overlap or lack of it.

  ■ Don't depend on any relationship between segment:offset combinations and
    physical memory.

  ■ Don't use wrap-around address offsets.

  ■ Don't use the segment registers for anything but valid segment numbers.

  ■ Never address beyond the allocated size of a segment.

  ■ If you have to play with the I/O ports, use only the appropriate dynamic
    link routines (FAPI).

  ■ Don't mix code and data in the same segment.

  ■ Don't use undefined opcodes.

  ■ Don't use the PUSH SP instruction.

  ■ Don't use the POPF instruction.

  ■ Don't use shift counts greater than 31.

  ■ Don't use IDIV operands to produce a most-negative number.

  ■ Don't resume execution in the original code stream after a division
    trap.

  ■ Don't use redundant prefix bytes.

  ■ Don't use CLI instructions (in protected mode).

  ■ Don't use CPU speed as a timing constant.

  ■ Don't examine or set explicit flag register values.

  ■ Don't single-step interrupt instructions in debuggers.

  ■ Don't write self-modifying code.

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

OS/2 Multitasking: Exploiting the Protected Mode of the 80286

───────────────────────────────────────────────────────────────────────────
Also see the related article:
  Configuring the OS/2 Multitasker
───────────────────────────────────────────────────────────────────────────

Ray Duncan☼

Multitasking is the technique of dividing CPU time between multiple tasks
so that they appear to be running simultaneously. Of course, the
microprocessor is only executing one sequence of machine instructions
within one task at any given time, but the switching from one task to
another is invisible to both the user and the programs themselves. The
operating system is responsible for allocating system resources such as
memory to the various executing tasks and for resolving contending
requests to such peripherals as video displays and disk drives.

The part of the operating system that allocates CPU time between tasks is
called the scheduler or dispatcher, and the rotation from one task to
another, or from a task to a module of the operating system, is called a
context switch. When a context switch occurs, the dispatcher must save the
current state of the task that was executing, including its registers and
program counter, load the registers and program counter belonging to the
next task to run, and transfer control to that task at the point where it
was previously suspended.

There are two basic strategies for task scheduling that are used by modern
multitasking operating systems: event-driven and pre-emptive.

Event-driven schedulers rely on each task to be "well-behaved" and yield
control at frequent enough intervals so that every program has acceptable
throughput and none will be "starved" for CPU cycles. This yielding may be
explicit (the program calls a specific operating system function to give
up control) or implicit (the program is suspended when it requests the
operating system to perform I/O on its behalf and regains control only
after the I/O is completed and other tasks have in turn yielded control).
This strategy is quite efficient in transaction-oriented systems where
there is a great deal of I/O and not much computation, but the system can
be brought to its knees by a single compute-bound task, such as an in-
memory sort.

A pre-emptive scheduler relies on the presence of an external signal
generated at regular intervals, typically a hardware interrupt triggered
by a real-time clock. When the interrupt occurs, the operating system
gains control from whatever task was executing, saves its context,
evaluates the list of programs that are ready to run, and gives control to
(dispatches) the next program. This approach to multitasking is often
called "time-slicing"; this term is derived from the mental image of
dividing the sweep of a second hand around a clock face into little wedges
and doling out the pieces to all the eligible programs.

Modern mainframe and minicomputers, and the more powerful microcomputers,
such as the Intel 80286 and Motorola 68020, include hardware features
specifically designed to make multitasking operating systems more
efficient and robust. These include privilege levels and memory
protection.

In the simplest use of privilege levels, the CPU is either in kernel mode
or user mode at any given time. In kernel mode, which is reserved for the
operating system, any machine instruction can be executed, and any
location in memory can be accessed. As part of the mechanism of
transferring control from the operating system to an application program,
the CPU is switched into user mode. In this CPU state, any attempt to
execute certain reserved instructions, such as writing to an I/O port,
causes a hardware interrupt and returns control to the operating system.
(Although the Intel 80286 microprocessor actually supports four privilege
levels, OS/2 ordinarily uses only the highest and lowest of these.)
Similarly, the hardware memory protection mechanisms detect any attempt by
an application program to access memory that does not belong to it, and
generate a hardware interrupt that allows the operating system to abort
the offending task.

Microsoft OS/2 is designed around a pre-emptive, priority-based
multitasking scheduler. Understanding OS/2 multitasking requires a grasp
of three distinct, but related, concepts or system objects: processes,
threads, and screen groups.

───────────────────────────────────────────────────────────────────────────
Three types of systems objects are involved in OS/2 multitasking: processes,
threads, and screen groups. A process represents an application program
accessing system resources such as files, memory, and inter-process
communication facilities. A process can contain multiple concurrent points
of execution called threads; each thread has its own priority and stack.
Processes are collected into screen groups that read and write a virtual
display and keyboard; the Session Manager is used to select a screen group.

                   ┌──────────────────────────────────┐
                   │                                  │█
                   │                                  │█
                   │                                  │█
                   │         Physical Screen          │█
                   │                                  │█
                   │                                  │█
                   │                                  │█
                   │                                  │█
                   └──────────────────────────────────┘█
                     ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
                                     
                            Session manager maps
                     virtual screen for a group to the
                             physical display.
                   ┌ ─ ─ ─ ─ ─ ─ ─ ─ ┴──────────────────┐
                   │                                    │
        ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐              ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
                               █                                    █
        │                     │█             │                     │█
          Virtual screen for   █               Virtual screen for   █
        │   screen group 1    │█             │   screen group 2    │█
                               █                                    █
        └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘█             └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘█
         ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀              ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
                                                            
          │                │                     │             │
  ┌───────┴───────┐ ┌──────┴───────────────┐ ┌───┴────┐ ┌──────┴────────┐
  │   │      │    │ │   │      │      │    │ │   │    │ │   │      │    │
  │   │      │    │ │   │      │      │    │ │   │    │ │   │      │    │
  │   │      │    │ │   │      │      │    │ │   │    │ │   │      │    │
  │   │      │    │ │   │      │      │    │ │   │    │ │   │      │    │
  │ Thread Thread │ │ Thread Thread Thread │ │ Thread │ │ Thread Thread │
  │   A1     A2   │ │   B1     B2     B3   │ │   C1   │ │   D1     D2   │
  └───────────────┘ └──────────────────────┘ └────────┘ └───────────────┘
      PROCESS A            PROCESS B          PROCESS C     PROCESS D
───────────────────────────────────────────────────────────────────────────

Processes

The simplest case of an OS/2 process is very similar to an application
program loaded for execution under MS-DOS 2.x and 3.x. A process is
started (whether by a shell or command processor or by another
application) by a call to the OS/2 service DOSEXECPGM. OS/2 initiates a
new process by allocating memory segments to hold that process's code,
data, and stack, and then initializing the memory segments from the
contents of the program's .EXE disk file.

Once it is running, a process can obtain access to additional system
resources such as files, pipes, semaphores, queues, and additional memory
with various OS/2 function calls (see Figure 1). A process terminates
itself with a call to the OS/2 function DOSEXIT and can also be aborted by
its parent process, by an unrecoverable hardware error, or by a memory
protection fault. In any case, OS/2 will automatically release all
resources belonging to the process when it terminates.

Ordinarily, processes are only aware of themselves, OS/2 services, and any
child processes they start directly. They cannot directly access memory
space or resources belonging to another process, including a child
process, without the cooperation of that process.


Threads

The MS OS/2 scheduler, however, knows nothing about processes; it
partitions the available CPU cycles among dispatchable entities known as
threads. A thread has a point of execution, a priority, a stack pointer,
and general register contents (see Figure 1). At any given time, a thread
is either blocked (waiting for I/O or some other event), ready to execute,
or actively executing (it has control of the CPU).

Each process has a primary thread that receives control from OS/2 when the
process is started; it begins executing at the program's designated entry
point. However, that primary thread can start up additional threads within
the same process. Multiple threads within a process execute asynchronously
to one another, can have different priorities, and can manipulate one
another's priorities.

Although the threads within a process have separate stacks, they share the
same near data segment (DGROUP) and thus the same local heap. Carefully
designing code to use the stack for local variables allows procedures to
be shared between threads──this happens naturally in C programs. Access to
static variables or other data structures must be coordinated between
threads through the use of RAM semaphores or similar mechanisms. The
threads also share all the other system resources owned by the process as
a whole──open files, system semaphores, queues, and pipes──but OS/2
generally provides automatic serialization of operations on these
resources.

When a thread within an application program is executing, the system is in
user mode; the thread can only access the resources owned by the process
that contains it and cannot execute certain machine instructions. A clock
tick, another hardware interrupt, or a call by the application for an OS/2
function causes a transition back into kernel mode, so that OS/2 can
service the interrupt or provide the requested operation.

When OS/2 is ready to exit kernel mode, the scheduler receives control and
examines its list of active threads. The thread with the highest priority
that is ready to execute gains control of the machine. If the thread that
was just interrupted is one of several eligible threads and has not used
up its time-slice, it receives preference. If a thread becomes starved for
CPU cycles because other threads with higher priorities are getting all
the attention, OS/2 will temporarily bump that thread's priority to a
higher value.


Screen Groups

Processes are in turn members of screen groups, which is what the average
user perceives as being OS/2 multitasking. When the user presses the
SysReq key, he exits from the current session, or screen group, to a menu
displayed by the Session Manager. He can then select a command processor
or other program already executing in another screen group or establish a
new screen group by loading a new copy of the protected-mode command
processor.

OS/2 maintains a separate virtual screen for each screen group, which
receives the output of all the processes in that group. The virtual screen
is mapped to the physical display whenever that screen group is selected
by the user with the Session Manager. New processes are added to a screen
group by being "spawned" by the command processor or another process
already executing within the group.

By convention, only one of the processes within a screen group should be
in the foreground at any given time, that is, writing to the screen and
reading from the keyboard. The OS/2 DETACH command allows programs to be
placed in the background from the command processor level. However, since
OS/2 does not place any restrictions on access to the virtual display by
the various processes within a screen group, DETACHing normal programs is
not usually too useful since it just results in chaotic displays. Programs
intended to be used as background tasks must be specially designed to use
the OS/2 services for pop-up windows and keyboard monitors, so that they
do not disrupt the displays or otherwise interfere with the proper
operation of the foreground task within their group.

A screen group, or session, is removed from the system by first
terminating any active application programs within that group and
returning to the command processor prompt. Then, entry of the EXIT
command terminates the command processor itself. The Session Manager
regains control and displays a menu of the remaining screen groups.


OS/2 Programming

OS/2 offers a diverse set of services to application programs that allow
the creation of complex and powerful multitasking applications (see
Figure 2). These services include

  ■  starting and stopping child processes

  ■  obtaining the return code of a child process

  ■  starting, suspending, and destroying threads

  ■  altering the priorities of threads

  ■  inter-process communication (see "OS/2 Inter-Process Communication:
     Semaphores, Pipes, and Queues,")

OS/2 DOSEXECPGM is used by the parent process to load and execute the
child process. This function is analogous to, but considerably more
powerful than, the EXEC function (Int 21h Function 4Bh) that was available
in MS-DOS 2.x and 3.x. The child process can, in turn, load other
processes and so on until system limits on threads or open files are
exhausted or the system runs out of swap space on the disk.

The OS/2 DOSEXECPGM function is called with

  ■  the address of the filename of the child process to be executed

  ■  a flag controlling whether the child process is synchronous or
     asynchronous

  ■  the addresses of an argument string block and an environment block to
     be passed to the child process (Each of these blocks consists of a
     series of null-terminated (ASCIIZ) strings; the block is terminated
     by an extra null byte. The environment block corresponds exactly to
     the environment block you are familiar with in MS-DOS 2.x and 3.x,
     while the simplest case of the argument block is simply a copy of the
     command line that invoked a process.)

  ■  addresses of buffers to receive the process ID of the child and other
     information

When a child process executes synchronously, execution of the thread in
the parent process that made the DOSEXECPGM call is suspended until the
child process terminates, either intentionally or owing to an error
condition. When the thread in the parent resumes execution, it is supplied
with the return code of the child and a termination code that indicates
whether the child terminated normally or was aborted by the operating
system.

If the child process is asynchronous, all threads in the parent process
continue to execute while the child is running. Any thread in the parent
process can later use the DOSCWAIT call to resynchronize with the child
process by suspending itself until the child terminates and then obtaining
its return code. As an alternative, the parent can use the process ID of
the child, which is supplied by OS/2 on return from the original
DOSEXECPGM call, to unilaterally abort execution of the child process with
the DOSKILLPROCESS call at any time. (A special usage of the asynchronous
option of DOSEXECPGM, together with the DOSPTRACE function, allows the
child program to be traced, inspected, and modified under the parent
program's control. This combination of OS/2 services allows the creation
of program debuggers that are compatible with 80286 memory protection.)

The child process automatically inherits access to certain resources of
the parent process. These resources include the handles for any open files
(unless the parent process explicitly opened the files with the
noninheritance flag), handles to any open pipes, handles to any open
system semaphores (but not ownership of the semaphores), and a copy of the
parent's environment block (unless the parent goes to the trouble of
creating a new block and passes a pointer to it).

Figure 3 is a demonstration of the DOSEXECPGM function──a synchronous
execution of CHKDSK as a child process. The various options for
asynchronous execution, coupled with several options that are available
with the DOSCWAIT function and the DOSKILLPROCESS function, allow for very
flexible execution-time relationships between parent and child processes.


Managing Threads

OS/2 has a rich repertoire of function calls for control of multiple
threads of execution within a process. The use of multiple threads is
particularly appropriate for cases in which a process must manage several
I/O devices with vastly different I/O rates (for example, the keyboard,
disk, and video display) and remain rapidly responsive to the needs of
each. Multiple threads are also appropriate for cases in which a process
needs to run in multiple instances, but the instances do not require
separate data segments or resources, since multiple threads are started
much more quickly than multiple processes and have less system overhead.

As mentioned previously, each process has a single primary thread that is
known to the OS/2 dispatcher when it is started up, and this thread's
initial point of execution is the program's designated entry point. The
primary thread can use the MS OS/2 function DOSCREATETHREAD to start up
additional threads within the process, and those new threads can also
start up threads and so on. Each thread is initially entered through a far
call from OS/2 and can terminate through a far return or by issuing the
OS/2 function call DOSEXIT. (A process is terminated when the sole
remaining active thread in a process issues DOSEXIT, or when any thread
issues DOSEXIT with a special parameter that indicates that all threads
should be immediately terminated.)

A thread can use the function DOSSLEEP to suspend itself for a programmed
period of time, or it can block on a semaphore to await reactivation by
another thread or process triggering that same semaphore. Alternatively, a
thread can use the functions DOSSUSPENDTHREAD or DOSRESUMETHREAD to
suspend or reactivate other threads within the same process without their
cooperation. Similarly, a thread can use the functions DOSGETPRTY or
DOSSETPRTY to inspect or modify the execution priority of itself or other
threads in accordance with execution-time requirements.

Because a thread can be unilaterally suspended by another thread without
its knowledge or cooperation, the functions DOSENTERCRITSEC and
DOSEXITCRITSEC are provided in order to protect a thread from interruption
while it is executing a critical section of its code.

Figure 4 demonstrates the use of DOSCREATETHREAD by one thread to start up
another thread that emits ten beeps at one-second intervals and then
terminates. Although this is a trivial use of multiple threads, it gives
an inkling of the enormous power of this concept and the ease with which
asynchronous processing can be incorporated into an OS/2 application.


Summary

OS/2 uses a time-slicing, pre-emptive, priority-based multitasking
strategy. The Intel 80286 microprocessor's support for privilege levels
and memory protection are fully exploited by OS/2 in order to run multiple
concurrent tasks and to protect those tasks from damaging each other or
the operating system.

A process represents the execution of an application and the ownership of
any resources──files, memory, etc.──associated with that execution.
Processes can spawn other processes and can exert certain control over
those child processes, but sharing of data between two processes is
possible only with the cooperation of both processes. OS/2 has a wealth of
facilities for inter-process communication, which are discussed in "OS/2
Inter-Process Communication: Semaphores, Pipes, and Queues."

The thread is the dispatchable element used by OS/2 to track execution
cycles of the processor. Threads can start, stop, and influence the
execution of other threads within the same process. Sharing of data
between threads is natural and, in fact, difficult to avoid, since all
threads in a process share access to the same memory, files, pipes,
queues, and semaphores. In essence, the OS/2 loader knows about processes,
but the OS/2 scheduler knows about threads.


Figure 1:  Thread- and Process-specific information maintained by OS/2.

Per-Process Information

Process ID (PID)
Disk-swapping information
Local Descriptor Table (LDT) pointer
System resources owned or opened:
   Files
   Pipes
   Queues
   System Semaphores
   Device monitors
   Memory
Child processes

Per-Thread Information

Thread ID
Thread priority
Thread state: blocked, ready to execute, active
Time-slice
Instruction pointer
Processor state (registers and flags)
Stack pointer


Figure 2:  OS/2 multitasking services at a glance.

Process Control

DOSEXECPGM        Load and execute a child process
DOSCWAIT          Wait for child process to terminate
DOSKILLPROCESS    Unconditionally terminate another process.
DOSPTRACE         Inspect/modify/trace a child process

Threads Controlling Threads

DOSCREATETHREAD   Create another execution thread within the sameprocess
DOSSUSPENDTHREAD  Suspend the execution of a thread
DOSRESUMETHREAD   Reactivate a thread
DOSEXIT           Terminate current thread or all threads in process

Read/Alter Thread Priorities

DOSGETPRTY        Get the priority of specified thread
DOSSETPRTY        Set the priority of specified thread

Inter-Thread Protection

DOSENTERCRITSEC   Disable other threads in same process
DOSEXITCRITSEC    Re-enable other threads in same process


Figure 3:  This sample code fragment demonstrates the use of the OS/2 system
           DOSEXECPGM to run CHKDSK as a synchronous child process

                                    ∙
                                    ∙
                                    ∙
           push  ds                      ; address of object buffer
           push  offset DGROUP:ObjName
           push  ObjNameLen              ; length of object buffer
           push  0                       ; execute synchronously
           push  ds                      ; address of argument block
           push  offset DGROUP;ArgBlk
           push  0                       ; address of envir.block
           push  0                       ; (OL=inherit parent's)
           push  ds                      ; address to receive return
           push  offset DGROUP:RetCode   ;   and termination codes
           push  ds                      ; address of name of child
           push  offset DGROUP :PgmName
           call  DOSEXECPGM              ; transfer to 286DOS
           or    ax,ax                   ; was EXEC successful?
           jnz   error                   ; jump if EXEC failed...
                                    ∙
                                    ∙
                                    ∙
ObjName    db   64 dup (0)          ; receives name of dynamic link
ObjNameLen equ  $-ObjName           ; causing EXEC failre

ArgBlk     db   'chkdsk *.*',0      ; block of argument strings for child...
           db   0                   ; extra null byte terminates block

                                    ; receives return codes from child...
RetCode    dw   0                   ; termination code for child
           dw   0                   ; result code from child's DOSEXIT

PgmName    db   'chkdsk.eve',0      ; name of child program to run


Figure 4:  An example of the use of multiple threads for asynchronus
           execution of tasks within a single process. The main line of
           execution allocates memory for a new stack and then starts up
           another thread called beeper. The new thread uses the OS/2 service
           DOSBEEP to emit ten short tones at one second intervals and then
           terminates.

stksiz    equ   1024                     ; size of new thread's stack
                                    ∙
                                    ∙
                                    ∙
Selector  dw    ?                        ; selector from DOSALLOCSEG

BeeperlD  dw    ?                        ;  Thread ID for new thread
                                         ; named 'beeper'
                                    ∙
                                    ∙
                                    ∙
          push  stksiz                   ; size of new segment
          push  ds                       ; address of variable
          push  offset DGROUP:Selector   ; to receive new selector
          push  0                        ; non-shared segment
          call  DOSALLOCSEG              ; TRANSFER TO 286DOS
          or    ax,ax
          jnz   error                    ; jump if alloc failed

          push  cs                       ; execution address of
          push  offset_TEXT:Beeper       ; new thread
          push  ds                       ; address to receive new
          push  offsetDGROUP;BeeperID    ; Thread ID
          push  Selector                 ; address of base of
          push  stksiz                   ; new thread's Stack
          call  DOSCREATETHREAD          ; transfer to 286DOS
          or    ax,ax
          jnz   error                    ; jump if create failed
                                    ∙
                                    ∙
                                    ∙
beeper    proc  far                      ; entry point for thread

          mov   cx,10                    ; emit ten beeps...

beep1:    push  440                      ; sound a 440 Hz tone
          push  100                      ; for 100 milliseconds
          call  DOSBEEP                  ; transfer to 286DOS

          push 0                         ; now pause for one sec.
          push  1000
          call  DOSSLEEP                 ; transfer to 286DOS

          loop  beep1                    ; loop ten times
          ret

beeper    endp
                                    ∙
                                    ∙
                                    ∙

───────────────────────────────────────────────────────────────────────────
Configuring the OS/2 Multitasker
───────────────────────────────────────────────────────────────────────────

There are four different directives that can be placed in the system
CONFIG.SYS file to influence the operation of OS/2 multitasking. These
are

  THREADS=n
  MAXWAIT=seconds
  PRIORITY=ABSOLUTE|DYNAMIC
  MAXWAIT=x[,y]

The THREADS directive controls how many threads can be simultaneously
created in the system. The parameter n may be 16-255, with a default of
16, which is sufficient for OS/2 and a few simple processes. The upper
limit of 255 cannot be expanded.

When a thread is denied the CPU for the number of seconds specified by
MAXWAIT because of other higher-priority threads using up all the time-
slices, the starved thread receives a temporary increase in its priority
for one time-slice.

The PRIORITY directive activates OS/2's mechanisms for dynamically
altering the priority of each thread based on the activity of other
threads within the system. When PRIORITY=ABSOLUTE, the MAXWAIT directive
has no effect whatsoever.

The TIMESLICE directive controls the length of the time-slices used by the
OS/2 scheduler. The x parameter represents the normal length of a thread's
time-slice in milliseconds. If a thread uses up its entire time-slice and
must be pre-empted, its next time-slice will be one tick longer, up to the
limit given by the y parameter. This strategy is used by OS/2 to reduce
the amount of context-switching overhead when several compute bound
threads are running.

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

OS/2 Memory Management

In a multitasking system, random access memory (RAM) must be administered
as a critical, finite system resource akin to disk storage or a line
printer. The well-bred application program uses the host operating
system's facilities to request additional memory politely when necessary,
releases unneeded memory, and abstains from meddling with memory that
doesn't belong to it.

In the MS-DOS environment, such genteel programs are few and far between.
Most popular application programs place the goal of high performance above
all else, and a disposition to grab all available memory, capture
interrupt vectors, and directly access the video controller's refresh
buffer for fast screen displays is the rule rather than the exception.
This behavior causes many headaches for those system software vendors who
seek to graft multitasking abilities onto MS-DOS with such new user
interfaces as Windows, TopView, and DESQview.

The 80286 microprocessor, which is the heart of the IBM PC AT and
compatible computers, can execute in either of two modes: real mode and
protected mode. When the 80286 is running in real mode, as it does under
MS-DOS, it essentially functions as a fast 8086/88 processor with a few
added machine instructions. The segment registers contain physical
addresses, which can be manipulated directly, and the total amount of
memory that can be addressed is 1 megabyte (Mb) (see Figure 1). On the
8086, and the 80286 running in real mode, there is no way for the
operating system to intervene when a program does not use memory properly
or to prevent one program from writing into another's memory space.

Under OS/2, however, the rules are changed. OS/2 runs the 80286 processor
in protected mode, providing hardware support for memory protection and
memory access privilege levels, and presenting a very different hardware
architecture to the programmer. The contents of segment registers are now
logical selectors rather than physical addresses (see Figure 2). Selectors
are indexes into descriptor tables that contain the actual physical
address and length of the corresponding chunk of memory, and the
descriptor tables themselves are typically not accessible to application
programs at their privilege level. Each segment has an attribute that
determines how it can be accessed: executable, read-only data, or read-
write data.

Memory protection between programs is enforced by each process's local
descriptor table (LDT), a map of the memory segments that can be accessed
by that process. If a task tries to load a segment register with a
selector that is not valid for its own descriptor table or for the global
descriptor table (GDT), a hardware interrupt is generated (see Figure 3).
The operating system then recovers control, terminates the offending
process, and displays diagnostic information on the system console.

Thus, applications running under OS/2 must be civil to survive: there is
no alternative to using the host system's services for managing memory.
Only the operating system is entrusted with the privilege level and the
knowledge of physical memory layout, which is necessary to create and
destroy memory segments through manipulation of descriptor tables.
Fortunately, OS/2 provides applications with a complete repertoire of
memory management services, in three major groups: local heap management,
conventional allocation and release of global memory segments, and "huge"
global block management (logically contiguous memory spaces that are
larger than 64Kb).

In exchange for good behavior, the 80286's protected mode provides a
reward──virtual memory (see Figure 4). A task can request, and own, more
bytes of RAM memory than physically exist in the system. The combination
of hardware support for paging and a specialized module of the operating
system called the swapper present to the task the illusion that all of its
segments are simultaneously present and accessible.

When all of physical memory is in use and a process attempts to allocate
additional memory, the least recently used segment is copied to a swap
file on the disk, and the memory it occupied is freed up. A special bit in
the descriptor table entry for the swapped-out segment is set to indicate
that it is nonresident. When a selector for a swapped-out segment is used
in a memory access, a hardware interrupt called a page fault occurs. The
virtual memory manager (swapper) gains control, reads the needed segment
from the disk into physical memory (possibly writing another segment out
first to make room), updates the descriptor table entry for the selector
to correspond to the new physical address, and restarts the process's
instruction that was trying to access the segment.

Thus, virtual memory means that the presence or absence in physical memory
of any particular segment is completely transparent to the process that
owns it. If an address that is not present is accessed, it will be
invisibly loaded on demand. Correspondingly, the amount of memory that can
be committed by all the processes in the system combined is limited only
by the amount of swapping space on the disk (although limits on the number
of available segment selectors do exist, they are of no practical
significance owing to the sizes of today's fixed disks).

The 80286's support for protected, virtual memory facilitates two other
OS/2 features: shared text (code) segments and dynamic linking. Segments
containing machine instructions are marked with the executable attribute
and cannot be modified. Thus, when the same program is loaded for
execution in two or more sessions, each instantiation can share the same
memory-resident piece of code, instead of requiring separate, distinct
copies. The protected-mode command processor called CMD.EXE is a good
example: the overhead of multiple screen groups is small, since only one
copy of the machine code in CMD.EXE is needed. Adding another screen group
with its own command processor requires only the creation of another data
segment and virtual screen buffer.

Dynamic link libraries are simple extensions of the concepts of virtual
memory and shared text segments. Under MS-DOS, when a program was linked
to a run-time library, the actual machine code for the library routines
became a permanent part of the application. Under OS/2, the linker and the
.EXE file header have been extended to allow run-time binding (Figure 5)
of library routines. When a program containing dynamic links is loaded for
execution, OS/2 examines the file header and loads any modules from
dynamic link libraries that are required, then updates the FAR CALL
addresses within the application. Naturally, these dynamically linked
routines can be shared between multiple processes and will be discarded
and reloaded as needed by the memory manager. Much of OS/2 itself is
implemented in the form of dynamic link libraries.


Figure 1:  Real Address Mode Segment Selector Interpretation. The selector
           is used to identify segments in real memory.

   ┌─────────────────────────────────────────────────────────────────┐
   │                                                                 │█
   │  ╔══════════════╤══════╗              ╔══════════╗ ───┐         │█
   │  ║   Selector   │ 0000 ║              ║          ║    │         │█
   │  ╚══════════╤═══╧══════╝              ║          ║    │         │█
   │             │                      ┌─ ╟──────────╢   1M         │█
   │      Segment and Base              │  ║          ║   Physical   │█
   │          Address                  64K ║  Seg 1   ║   Address    │█
   │             │                      │  ║          ║   Space      │█
   │             └──────────────────────┴─►╟──────────╢    │         │█
   │                                       ║          ║    │         │█
   │                                       ║          ║    │         │█
   │                                       ╚══════════╝ ───┘         │█
   │                                                                 │█
   └─────────────────────────────────────────────────────────────────┘█
     ██████████████████████████████████████████████████████████████████


Figure 2:  In protected mode the selector does not specify a segment's
           location in physical memory. Instead, it uniquely identifies
           (or names) any one of 16K possible segments──the selector uses
           14 bits to address the segments in a given task's virtual address
           space. Because each segment can address 64K, the total virtual
           space available is Gb.

                          ╔════════════╗ ──┐
                          ╠═ Seg 3fff ═╣   │
                          ╠═ Seg 3ffe ═╣   │
                          ╠═ Seg 3ffd ═╣   │
                  ┌──────►╠═ Seg 3ffc ═╣   │
                  │       ╠═ Seg 3ffb ═╣   │
             ┌────┴───┐   ║            ║   │
             │Selector│   <            <   1 G Virtual
             └────────┘   >            >   Address Space
                          ║            ║   │
                          ╠══ Seg 4  ══╣   │
               1 to 64K───╠══ Seg 3  ══╣   │
                          ╠══ Seg 2  ══╣   │
                          ╠══ Seg 1  ══╣   │
                          ╠══ Seg 0  ══╣   │
                          ╚════════════╝ ──┘


Figure 3:  The GDT maintains system-wide information while the LDT maintains
           task-specific information. There is one GDT for any given system
           that is shared by every task or process. Each task's virtual local
           space, managed bu the LDT, is isolated from that of any other.
           OS/2 itself resolves virtual addresses to map tasks to real
           memory.

                 CPU                       │   Memory   │
    ┌─────────────────────────┐            ├────────────┤──┐
    │                         │      ┌─┬──►├────────────┤  │
    │         ┌15─────────0┐  │      │     ├────────────┤  │
    │         │ GDT Limit  ├─────────┘ │   │      ∙     │  │
 GDT│  ┌23────┴────────────┤  │            │      ∙     │ GDT
    │  │     GDT Base      ├─────────┐ │   │      ∙     │  │
    │  └───────────────────┘  │      │     ├────────────┤  │
    ├ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┤      │ │   ├────────────┤  │
    │         ┌15─────────0┐  │      │     ├────────────┤  │
    │         │LDT Selector│  │      └─┴──►├────────────┤──┘
    │         └────────────┘  │            │            │
    │                         │            │   LDT{1}   │
    │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │      ┌─┬──►├────────────┤──┐
 LDT│         ┌15─────────0┐  │      │     ├────────────┤  │
    │ │       │ LDT Limit  ├┼────────┘ │   ├────────────┤  │
    │  ┌23────┴────────────┤  │            │      ∙     │  │
    │ ││     LDT Base      ├┼────────┐ │   │      ∙     │ Current
    │  └───────────────────┘  │      │     │      ∙     │ LDT
    │ │                     │ │      │ │   ├────────────┤  │
    │    Program Invisible    │      │     ├────────────┤  │
    │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │      │ │   ├────────────┤  │
    └─────────────────────────┘      └────►├────────────┤──┘
                                           │            │
                      TASK 1               │   LDT{n}   │
                      VIRTUAL ADDRESS─┐    ├────────────┤
                            SPACE     │    ├────────────┤
                   ┌──────────────┐   │    ├────────────┤
                   │ ╔══════════╗ │   │    │      ∙     │
                   │ ║TASK 1    ║ │   │    │      ∙     │
 TASK 3            │ ║LOCAL     ║ │   │    │      ∙     │
 VIRTUAL ADDRESS   │ ║ADDRESS   ║ │   │    ├────────────┤
        SPACE ┐    │ ║SPACE     ║ │◄──┘    ├────────────┤
              ▼    │ ╚══════════╝ │        ├────────────┤
     ┌─────────────│──────────────│┐       ├────────────┤
     │ ╔══════════╗│┌─────────────│───────────────┐
     │ ║TASK 3    ║││ ╔════════╗  ││ ╔══════════╗ │
     │ ║LOCAL     ║││ ║GLOBAL  ║  ││ ║TASK 2    ║ │
     │ ║ADDRESS   ║││ ║ADDRESS ║  ││ ║LOCAL     ║ │
     │ ║SPACE     ║││ ║SPACE   ║  ││ ║ADDRESS   ║ │
     │ ╚══════════╝││ ╚════════╝  ││ ║SPACE     ║ │
     └─────────────└──────────────┘┘ ╚══════════╝ │
                    └─────────────────────────────┘
                                  
                                  └TASK 2
                                   VIRTUAL ADDRESS
                                   SPACE


Figure 4:  80826 Virtual Address Space. OS/2 provides one GDT that is shared
           by each task or process. Hardware support for paging and the OS/2
           swapper together provide the illusion that all segments are
           simultaneously present and accessible.

                                               Task B Address Space
      Task A Private Address Space         Task B Private Address Space

                     ┌───────┐ 65535                      ┌───────┐ 65535
                     │       │                           │       │   
                     │  SEG. │ Offset                     │  SEG. │ Offset
                     │       │   │                        │       │   │
    ┌───────┬───────►└───────┘ 0 ▼       ┌───────┬───────►└───────┘ 0 ▼
    │       │            ∙               │       │            ∙
    │ LDT A │            ∙               │ LDT B │            ∙
    │       │            ∙               │       │            ∙
    └───────┴──┐     ┌───────┐ 65535     └───────┴──┐     ┌───────┐ 65535
               │     │       │                     │     │       │   
               │     │  SEG. │ Offset               │     │  SEG. │ Offset
               │     │       │   │                  │     │       │   │
               └────►└───────┘ 0 ▼                  └────►└───────┘ 0 ▼

      Task C Private Address Space              Shared Address Space

                     ┌───────┐ 65535                      ┌───────┐ 65535
                     │       │                           │       │   
                     │  SEG. │ Offset                     │  SEG. │ Offset
                     │       │   │                        │       │   │
    ┌───────┬───────►└───────┘ 0 ▼       ┌───────┬───────►└───────┘ 0 ▼
    │       │            ∙               │       │            ∙
    │ LDT C │            ∙               │  GDT  │            ∙
    │       │            ∙               │       │            ∙
    └───────┴──┐     ┌───────┐ 65535     └───────┴──┐     ┌───────┐ 65535
               │     │       │                     │     │       │   
               │     │  SEG. │ Offset               │     │  SEG. │ Offset
               │     │       │   │                  │     │       │   │
               └────►└───────┘ 0 ▼                  └────►└───────┘ 0 ▼


Figure 5:  To invoke run-time dynamic linking, the application uses the
           following system calls.

DOSLOADMODULE       Load Dynamic Link Module
DOSFREEMODULE       Free Dynamic Link Module
DOSGETPROCADDR      Get Dynamic Link Procedure Address
DOSGETMODHANDLE     Get Dynamic Link Module Handle
DOSGETMODNAME       Get Dynamic Link Module Name

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

OS/2 Inter-Process Communication: Semaphores, Pipes, and Queues

Ray Duncan☼

Operating System/2 multitasking services allow applications to create
multiple concurrent threads within a process or to create child processes.
However, as we have seen earlier (see "OS/2 Multitasking: Exploiting the
Protected Mode of the 80286,"), the multitasking functions provide
a parent process with only a very limited ability to influence and
communicate with a child process. The parent process can pass information
to the child at load time in the form of command strings and the
environment block, it can obtain the child process's exit (return) code
and a code describing the child process's method of termination, and
unilaterally abort a child process.

To fill the need for rapid exchange of all kinds of information between
concurrent threads and related or unrelated processes, OS/2 includes a
rich set of system services called inter-process communication (IPC)
functions. Computer literature is teeming with different mechanisms for
inter-process communication, and more are being added every day. Still,
there is broad agreement on three basic methods of IPC, all of which are
supported by OS/2: semaphores, pipes, and queues.

In this article, we'll briefly discuss each of these three IPC mechanisms,
summarize the relevant OS/2 functions, and supply a brief coding example.
Since there are many options available in the IPC services, we can't
provide a comprehensive description of their capabilities here; the
program fragments are only intended to demonstrate the most common usage.
More detailed information can be found in the OS/2 Programmer's Reference
manual.

While reading about the IPC functions, bear in mind the distinctions
between processes and threads. When a process opens or creates a
semaphore, pipe, or queue, a handle is returned that can be used by any
thread within that process and is inherited by any child processes. On the
other hand, when a thread issues an OS/2 function call that waits on a
semaphore or performs a synchronous read or write to a pipe or queue, only
the calling thread is suspended──other threads in the process continue to
run as they did before. When multiple threads in the same process issue
requests against the same IPC object, any necessary serialization of these
requests is taken care of within the operating system.


Semaphores

A semaphore can be viewed as a simple object with two states: set (owned)
or cleared (not-owned). OS/2 provides a complete battery of system
services to test, set, and clear semaphores; a summary of these functions
is found in Figure 1. Semaphores are a high-performance mechanism of
inter-process communication, because they are always resident in memory
and the OS/2 services that manipulate them are relatively fast.

The classical use of semaphores, and a usage fully supported by OS/2, is
to control access to a nonreentrant routine or a serially reusable
resource (SRR). An SRR is a file, peripheral device, or a memory object
that is damaged or produces unpredictable results if it is accessed by
more than one thread or process at a time. With semaphores, mutual
exclusion on such a resource is easy to arrange between cooperating
processes. A semaphore is established that represents the resource, and a
thread or process refrains from accessing the resource unless it "owns"
the corresponding semaphore.

An alternative use of semaphores in OS/2 is to provide for synchronization
or signaling between threads or processes. In this usage, one or more
threads can suspend themselves by using an OS/2 function to "block" or
wait on a semaphore. When the semaphore is cleared by another thread in
response to some event, all of the threads that were blocking on that
semaphore will wake up and run. When a semaphore is used for signaling and
no resource that can be corrupted is involved, any thread that knows about
the semaphore can set, clear, or test the semaphore at any time.

To provide for the slightly different requirements of inter-thread and
inter-process communication, OS/2 supports two types of semaphores: RAM
semaphores and system semaphores.

RAM semaphores are used for signaling or resource control between multiple
threads in a single process. Each RAM semaphore requires the allocation of
a double word of data storage; the double word should be initialized to
zero when the process is started. The handle for a RAM semaphore is simply
its 32-bit address (selector and offset).

System semaphores are named objects that are used for signaling or
synchronization between processes; the name always takes the form:

  \SEM\name.ext

The extension (.ext) is optional. The storage for a system semaphore is
allocated by the operating system somewhere outside the creating
processes' memory space.

Access to a system semaphore is obtained by creating or opening it by
name; OS/2 returns a 32-bit handle that can be used for subsequent access
to the semaphore. When the process no longer requires access to the system
semaphore, it can use the handle to close it as it would close a file. The
three functions that control access to a system semaphore, DOSCREATESEM,
DOSOPENSEM, and DOSCLOSESEM, are described in more detail below.

Creating a system semaphore. The function DOSCREATESEM is called with the
address of a null-terminated (ASCIIZ) semaphore name, the address of a
double-word variable that will receive the semaphore handle, and a flag
that indicates whether ownership of the semaphore will be "exclusive"──in
which case the state of the semaphore cannot be altered by a process that
does not own it (see Figure 2). If the create operation is successful, the
initial state of the semaphore will be not-owned (cleared).

Opening a system semaphore. The function DOSOPENSEM is called with the
address of a null-terminated (ASCIIZ) semaphore name of an existing
semaphore and the address of a double word of storage that will receive a
handle. Opening a semaphore does not establish ownership, test, or change
the value of the semaphore.

When a process creates or opens system semaphores, the handles to those
semaphores are inherited by any child processes that are started with
DOSEXECPGM. However, the child process does not own the semaphores even if
the parent owned them; only one process at a time can own a given
semaphore.

Closing a system semaphore. The function DOSCLOSESEM is called with a
handle and terminates access to a system semaphore. If a process
terminates with open semaphores, they are closed automatically.

However, if any of these semaphores were owned by the process, any threads
in other processes that were waiting on the semaphore are awakened and
receive an error code that indicates that the owning process may have
terminated abnormally and the resource controlled by the semaphore may be
in an indeterminate state. The semaphore itself ceases to exist when
processes that use the semaphore have terminated or called DOSCLOSESEM.

───────────────────────────────────────────────────────────────────────────
RAM Semaphores: A thread (Tx) issues the DosSemRequest call to claim access
to the semaphore controlling a given SRR. If the semaphore is currently
unused, DosSemRequest gives the calling thread ownership of the semaphore.
All other threads are locked out until the owning thread clears the semaphore
through the DosSemClear and relinquishes ownership of both the SRR and the
semaphore.


             T1  ►  D      Tx enters and owns
Multiple            o      the SRR (semaphore        D
Concurrent          s      not available)            o
Threads             S          │                     s
(T1...Tn)    T2  ►  e          │                     S          Tx exits and
desire              m          ▼                     e          relinquishes
access to           R     Tx  ►     Tx  ►     Tx  ►  m     Tx  ►ownership of
a Serially          e                               C          the SRR and
Reusable    ...  ►  q                    │           l          the semaphore
Resource            u                    │           e
(SRR)               e       Serially Reuseable       a
                    s       Resource                 r
             Tn  ►  t

                                              ▼▼▼▼▼
                           SEMAPHORE AVAILABLE

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

Mutual Exclusion

The two OS/2 functions that are associated with the classical use of a
semaphore are DOSSEMREQUEST and DOSSEMCLEAR.

Claiming a semaphore. The OS/2 function DOSSEMREQUEST is used by a thread
to establish ownership of a semaphore and, by inference, ownership of the
resource associated with the semaphore. DOSSEMREQUEST is called with the
handle of a system or RAM semaphore and a time-out parameter. If the
specified semaphore is currently unowned, the function sets it as owned
and returns a success code. If the semaphore is already owned by another
thread, the system either suspends the thread indefinitely until the
semaphore becomes available, waits for a specified interval and then
returns with an error code, or returns immediately with an error code,
depending on the time-out parameter.

Recursive requests for system semaphores are supported by means of a use
count, maintained by OS/2, of the number of times the semaphore owner has
issued DOSSEMREQUEST without a corresponding DOSSEMCLEAR. However,
recursive use of RAM semaphores is not supported.

Releasing a semaphore. The function DOSSEMCLEAR is called with a semaphore
handle and releases the thread's ownership of that semaphore. An error
code is returned if the handle is invalid or if the semaphore was created
with the exclusive option and is not currently owned by the calling
thread. If the semaphore is already cleared, then no error is returned.

When multiple threads are waiting on the same semaphore with
DOSSEMREQUEST, it can be difficult to predict which thread will be
awakened and acquire the semaphore when it is released by the current
owner. The thread selected depends on an interaction of the priorities of
the waiting threads, the position of the threads in the scheduler's list,
and the activity of other threads in the system.


Signaling

Semaphores are used as signals when only one thread is in a position to
detect an event but other threads may wish to take action based on this
event. For example, a thread in a detached process utility that is
monitoring the keyboard data stream for a hot key might wish to trigger
other threads in the same process to display a pop-up screen or write to a
file. The MS OS/2 functions that support signaling include DOSSEMSET,
DOSSEMCLEAR, DOSSEMWAIT, DOSSEMSETWAIT, and DOSMUXSEMWAIT.

Setting a semaphore. The OS/2 function DOSSEMSET is called with the handle
of a system or RAM semaphore and unconditionally sets that semaphore (see
Figures 3 and 4). The function fails if the handle is invalid or if a
system semaphore was created with the exclusive option and is currently
owned by another process (a semaphore used for signaling would not
ordinarily be created with the exclusive option).

Clearing a semaphore. The function DOSSEMCLEAR is called with the handle
of a RAM or system semaphore and unconditionally clears that semaphore. As
with DOSSEMSET, the function fails if the semaphore handle is invalid or
if the semaphore was created with the exclusive option and is currently
owned by another process. If any threads were blocked on the semaphore,
they are restarted.

Waiting on a semaphore. OS/2 contains several functions that allow a
thread to suspend itself until one or more semaphores are cleared. The
function DOSSEMWAIT is called with a semaphore handle and a time-out
value. The calling thread is suspended until the indicated semaphore is
cleared by another thread or process with DOSSEMCLEAR; it regains control
immediately if the semaphore is not set. The function returns an error if
the handle is invalid, no-wait was specified, and the semaphore is
currently set; if a finite wait was specified and timed out; or if the
semaphore was created with the exclusive option and is currently owned by
another process.

The function DOSSEMSETWAIT works just like DOSSEMWAIT, except that it sets
the semaphore if it was not already set and then suspends the current
thread until the semaphore is cleared by another thread or process or the
indicated time-out expires.

DOSSEMWAIT and DOSSEMSETWAIT are level-triggered, not edge-triggered. This
means that it is possible for a thread that is blocking on a semaphore to
miss a quick clearing and resetting of the semaphore, depending on the
thread's priority and position in the scheduler's list and the activity of
the other threads in the system.

The function DOSMUXSEMWAIT is called with a list of up to 16 semaphores
and an optional time-out interval. The calling thread is suspended until
any of the indicated semaphores are cleared or the indicated interval has
elapsed. Unlike DOSSEMWAIT and DOSSEMSETWAIT, the function DOSMUXSEMWAIT
is edge-triggered; the waiting thread is awakened when one of the
semaphores in the list changes state even if that semaphore gets set
(armed) again right away by another process or another thread.


Pipes

Pipes are a means of inter-process communication midway in power between
semaphores and queues. Like semaphores, the level of performance of pipes
is relatively high, because the information is always kept resident in
memory; like queues, pipes can be used to pass any type of data between
processes. Physically, a pipe is simply a chunk of memory that is used as
a ring buffer, with In and Out pointers maintained by the system. From a
process's point of view, reading and writing a pipe are equivalent to
reading and writing a file, except that a pipe is much faster.

Pipes are not named entities. A process creates a pipe by calling the OS/2
function DOSMAKEPIPE with the addresses of two variables to receive the
read and write handles for the pipe, and a maximum pipe size (up to 65,504
bytes). The function fails if there is not enough memory to create the
pipe or if no handles are available.

The two handles for reading and writing the pipe are assigned out of the
same numeric sequence as those used for access to files. Any child
processes automatically inherit the handles and have access to the pipe.
We have already encountered the two major restrictions on the use of
pipes: they can only be used for communication between closely related
processes, and the maximum amount of data that a pipe can contain at one
time is relatively small.

Threads read and write a pipe with the usual file DOSREAD and DOSWRITE
calls, supplying a handle, buffer address, and record length (see
Figure 5). If a thread attempts to write to a pipe and the pipe is full,
that thread is suspended until another thread or process removes enough
data from the pipe so that the write can be completed. If a thread
requests a synchronous read from a pipe and the pipe is empty, that thread
is suspended until some other thread writes enough data to the pipe to
satisfy the read request. There are no control or permission mechanisms or
checks performed on operations to pipes.

A pipe vanishes from the system when all the processes using the pipe
either close the pipe handles or terminate. If two processes are
communicating with pipes and the process reading the pipe ends, the
process writing the pipe receives an error code.

The most common usage of pipes by a process──and the way that the system's
command interpreter uses them──is to arrange for the pipe's handles to be
substituted for the standard input and standard output handles. Any child
processes that are started then automatically communicate with the parent
process for their input and output instead of the keyboard and screen,
without any special knowledge or action on the child's part.

───────────────────────────────────────────────────────────────────────────
Communicating with a Pipe: Pipes are a high performance mechanism for
communication between closely related processes (such as a parent process and
multiple child processes). Pipes are read and written like a file, and may
contain variable-length messages with any content.

                  ╔═════════╗      PIPES      ╔══════════╗
                  ║ NEXT IN ║  ╔═══════════╗  ║ NEXT OUT ║
                  ╚═════════╝  ║  aaaaaa   ║  ╚═════════╝
                          █   ║  aaaaaa   ║     │    │
     ╔═════════════╗  █    █   ╟───────────╢     │    │     ╔═════════════╗
     ║  PROCESS A  ╟─►◘    █   ║  eeeeee   ║     │    └────►║  PROCESS X  ║
     ╚═════════════╝  █    █   ║  eeeeee   ║     │          ╚═════════════╝
                      █    █   ╟───────────╢     │
     ╔═════════════╗  █    █   ║   bbbbb   ║─────┘
     ║  PROCESS B  ╟─►◘    █   ╟───────────╢
     ╚═════════════╝  █    █   ║   ggggg   ║
                      █    █   ║   ggggg   ║
     ╔═════════════╗  █    █   ║   ggggg   ║
     ║  PROCESS C  ╟─►◘    █   ╟───────────╢
     ╚═════════════╝  █    █   ║   ccccc   ║
                           █   ╠═══════════╣
                           █─► ╠═══════════╣
                           █   ╠═══════════╣
                               ╚═══════════╝
───────────────────────────────────────────────────────────────────────────

Queues

Queues are the most powerful inter-process communication service provided
by OS/2, and consequently the most complex to use. Queues are slower than
pipes, and much slower than semaphores, but they are also far more
flexible.

  ■  Queues are named objects and can be opened and written by any
     process.

  ■  The size of a queue is limited only by the amount of free memory plus
     swap space on the disk.

  ■  Each record in a queue is a separately allocated block of memory
     storage and can be as large as 65,536 bytes.

  ■  The records in a queue may be ordered by the first-in-first-out
     (FIFO), last-in-first-out (LIFO), or priority methods.

  ■  The queue owner can examine records in the queue and remove them
     selectively, in any order.

  ■  Data in the queue is not copied from place to place by the operating
     system; instead, the data is passed in shared memory segments.

The name of a queue will always take the form of \QUEUES\name.ext where
the extension (.ext) is optional. When a queue is created or opened, OS/2
returns a handle that is used for subsequent access to the queue. A
summary of OS/2 queue services can be found in Figure 6.

Creating a queue. The function DOSCREATEQUEUE is used to originate a new
queue in the system. The function is called with a null-terminated
(ASCIIZ) string that names the queue, a parameter that specifies the queue
ordering (FIFO, LIFO, or priority), and the address of a variable to
receive the queue handle (see Figure 7). An error code is returned if a
queue already exists with the same name, the given name is invalid, or
there is not enough free memory in the system to establish the queue's
supporting data structure. Only the process that creates the queue, called
the queue owner, can remove records from the queue, but all threads in the
owning process can access the queue with equal authority.

Opening a queue. Any process can open an existing queue by calling the
OS/2 function DOSOPENQUEUE with the name of the queue and the address of
two variables to receive the queue handle and the PID of the queue owner.
The handle is used for subsequent writes of records to the queue.

Writing a queue. Adding records to a queue is much more involved than
writing a record to a pipe. The writer must first allocate a chunk of
global shared memory of appropriate size with the function DOSALLOCSEG and
copy the data for the queue record into it. Next, the writer must use the
function DOSGIVESEG to obtain a new selector for the memory segment that
can be passed to another process. Finally, the writer calls DOSWRITEQUEUE
with a queue handle, a priority, the address (from DOSGIVESEG) and length
of the data to be added to the queue (see Figure 8). The queue write
proper fails if the queue handle is invalid or if there is insufficient
system memory to expand the supporting structure of the queue. Obviously,
the sequence can also fail at an earlier point (in DOSALLOCSEG or
DOSGIVESEG) if the system runs out of memory (the physical memory plus
swap space on the disk is exhausted) or selectors.

Reading a queue. The function DOSREADQUEUE is used by the queue owner to
remove a record from a queue; it is called with a queue handle and several
other selection parameters and returns the address of a queue element
(selector and offset) and its length. The owner can choose a record from
the queue based on its position or priority or simply take the next record
in line. The read operation can also be synchronous (the calling thread is
suspended until a record is available) or asynchronous (the calling thread
gets control back immediately, and a semaphore is set when a record is
available).

The function DOSPEEKQUEUE works similarly to DOSREADQUEUE, but the record
is not removed from the queue when it is retrieved. This allows the queue
owner to scan through the data waiting in the queue and decide on a
processing strategy, without disturbing the data structure or being forced
to copy the records to its own memory.

Miscellaneous operations. The function DOSQUERYQUEUE allows any process
that has a valid handle for a queue to obtain the current number of
elements in that queue. DOSPURGEQUEUE discards all records that are
currently in the queue; this function can only be called by the queue
owner.

Closing a queue. The function DOSCLOSEQUEUE is called with a queue handle
and informs OS/2 that the calling process will not require further access
to the queue. If the closing process is also the queue owner, the queue is
purged and destroyed──any further attempts to write records to the queue
by other processes will fail.

───────────────────────────────────────────────────────────────────────────
Communicating with a Queue: Queues are the most flexible and powerful
mechanism of inter-process communication supported by OS/2. A queue is
basically an ordered list of shared memory segments; each segment contains
a separate message and may be as large as 64K. The messages may be ordered
in the queue by First-In-First-Out, Last-In-Last-Out, or priority, and may
be inspected or removed from the queue by the server process in any order
whatsoever.

                        ╔═════════════╗
                        ║ Queues Data ╠════════════╗
                    █ ► ║  Structure  ║            ║
    ╔═══════════╗   █   ╚═══╦═════════╝            ║    ╔═══════════╗
    ║ Process D ║ ► ◘       ▼                      ╚═══►║ Process Y ║
    ╚═══════════╝   █   ╔════════╦════════════╗         ╚═══════════╝
    ╔═══════════╗   █   ║ MSG 'H'║            ▼
    ║ Process E ║ ► ◘   ╚════════╝        ╔════════╦═════════════╗
    ╚═══════════╝   █                     ║ MSG 'K'║             ▼
    ╔═══════════╗   █                     ╚════════╝        ╔════════╗
    ║ Process F ║ ► ◘                     ╔═════════════════╣ MSG 'J'║
    ╚═══════════╝   █                     ▼                 ╚════════╝
                    █                ╔════════╗
                              ╔══════╣ MSG 'N'║
                              ▼      ╚════════╝
                         ╔════════╗
                         ║ MSG 'M'║
                         ╚════════╝
───────────────────────────────────────────────────────────────────────────

Summary

OS/2 supports three methods of inter-process communication that are used
to pass messages between threads or processes: semaphores, pipes, and
queues.

Semaphores are used to symbolize ownership of a resource or signal
occurrence of an event; they can be thought of as simple flags that can be
set, cleared, or tested.

Pipes are a high-performance means of passing variable-length, variable-
content data between two closely related processes; however, pipes have
the relative disadvantage that their maximum size is fixed at the time
that they are created and they can never hold more than 64K.

Queues allow a vast amount of prioritized data to be passed from multiple
client processes to a server process; each message in the queue can be as
large as 64K, and the total amount of data in the queue is limited only by
the system's virtual memory. The selection of a particular IPC technique
for a given application must be made carefully on the basis of performance
requirements and the character of the messages to be exchanged between the
interested threads or processes.


Figure 1:  A summary of OS/2 semaphore support functions. Semaphores can be
           used to coordinate access to a resource or for signaling between
           threads or processes.

Access to system semaphores

DOSCREATESEM   Create a system semaphore
DOSOPENSEM     Open an existing system semaphore
DOSCLOSESEM    Close a system semaphore

Semaphore functions for resource control

DOSSEMREQUEST  Establish ownership of a semaphore
DOSSEMCLEAR    Release ownership of a semaphore

Semaphore functions for signaling

DOSSEMSET      Unconditionally set a semaphore
DOSSEMCLEAR    Unconditionally clear a semaphore
DOSSEMWAIT     Wait until semaphore is cleared
DOSSEMSETWAIT  Unconditionally set semaphore and wait
               until it is cleared by another thread
DOSMUXSEMWAIT  Wait for any of several semaphores to be cleared


Figure 2:  A code fragment that illustrates the creation or opening of a
           system semaphore.

          push 1                    ; exclusive ownership not desired
          push ds                   ; variable to receive semaphore handle
          push offset DGROUP:shandle
          push ds                   ; address of semaphore name
          push offset DGROUP:sname
          call DOSCREATESEM         ; transfer to OS/2
          or   ax,ax                ; create successful?
          jz   continue             ; jump if it was...
          cmp  ax,err_sem_exists    ; semaphore already exists?
          jnz  error                ; jump if some other error

                                    ; semaphore exists, open it instead
          push ds                   ; variable to receive handle
          push offset DGROUP:shandle
          push ds                   ; address of semaphore name
          push offset DGROUP:sname
          call DOSOPENSEM           ; transfer to OS/2
          or   ax,ax                ; was open successful?
          jnz  error                ; if can't open and can't create,
                                    ; something is terribly wrong
                                    ∙
                                    ∙
                                    ∙

sname     db   '\SEM\RESOURCE.LCK',0

shandle   dd   0


Figure 3:  A code sample that demonstrates setting a system semaphore.
           System semaphores are best suited for "between" processes.

                                    ∙
                                    ∙
                                    ∙
                                      ; open the semaphore

          push ds                     ; variable to receive handle
          push offset DGROUP:shandle
          push ds                     ; address of semaphore name
          push offset DGROUP:sname
          call DOSOPENSEM             ; transfer to O/S2
          or   ax,ax                  ; open successful?
          jnz  open_err               ; jump if open failed
                                    ∙
                                    ∙
                                    ∙
                                      ; set the semaphore:
                                      ; push semaphore handle...
          push word ptr shandle+2
          push word ptr shandle
          call DOSSEMSET              ; transfer to O/S2
          or   ax,ax                  ; was operation successful?
          jnz  set_err                ; jump if set failed
                                    ∙
                                    ∙
                                    ∙
sname     db   '\SEM\RESOURCE.LCK',0

shandle   dd   0                      ; handle for system semaphore


Figure 4:  A code fragment that demonstrates setting a RAM semaphore. RAM
           semaphores are best suited for use "within" processes.

                                    ∙
                                    ∙
                                    ∙
                                       ; set the RAM semaphore
          push ds                      ; push its address
          push offset DGROUP:my_sem
          call DOSSETSEM               ; transfer to O/S2
          or   ax,ax                   ; was set successful?
          jnz  set_err                 ; jump if set failed
                                    ∙
                                    ∙
                                    ∙
mysem     dd   0                       ; storage for RAM semaphore


Figure 5:  Establishing Pipes
                                    ∙
                                    ∙
                                    ∙
          push ds                        ; address to receive handle
          push offset DGROUP:prdh                 ; for reading pipe
          push ds                        ; address to receive handle
          push offset DGROUP:pwriteh              ; for writing pipe
          push 0                         ; max pipe size = default
          call DOSMAKEPIPE               ; transfer to O/S2
          or   ax,ax                     ; was pipe created?
          jnz  error                     ; jump if create failed.
                                    ∙
                                    ∙
                                    ∙
                                         ; in here we spawn a "child"
                                         ; process which inherits handles...

                                         ; now send a message to child
                                         ; process through the pipe...

          push pwriteh                   ; pipe write handle
          push ds                        ; address of message
          push offset DGROUP:message
          push message_length            ; length of message
          push ds                        ; address of variable to
          push offset DGROUP:status               ; receive bytes written
          call DOSWRITE                  ; transfer to O/S2
          or   ax,ax                     ; did write succeed?
          jnz  error                     ; jump if write failed.
                                    ∙
                                    ∙
                                    ∙
preadh    dw   ?    : handle to read pipe

pwriteh   dw   ?                         ; handle to write pipe

status    dw   ?                         ; receives length from DOSWRITE


Figure 6:  Substantial control over queues is provided by the OS/2 queue
           management functions.

Queue access

DOSCREATEQUEUE Create a queue (process is owner, can read or write
                 messages to queue)
DOSOPENQUEUE   Open a previously existing queue (can only write
                 messages to queue)
DOSCLOSEQUEUE  Close queue (queue is destroyed when closed by owner)

Queue input/output

DOSWRITEQUEUE  Write message to queue (either queue owner or any process
                 that has opened queue)
DOSREADQUEUE   Read message from queue (queue owner only)
DOSPEEKQUEUE   Nondestructive read of queue message (queue owner only)

Queue information

DOSQUERYQUEUE  Find number of messages currently in queue (queue owner or
                 any process that has opened queue)
DOSPURGEQUEUE  Discard all messages currently in queue (queue owner only)


Figure 7:  Creating a queue, waiting for a message to be written into it, and
           then closing the queue (destroying it).

                                    ∙
                                    ∙
                                    ∙
                                     ; first create queue...
          push ds                    ; address to receive handle
          push offset DGROUP:qhandle
          push 0                     ; queue ordering = FIFO
          push ds                    ; address of queue name
          push offset DGROUP:qname
          call DOSCREATEQUEUE        ; transfer to OS/2
          or   ax,ax                 ; was create successful?
          jnz  error                 ; jump if create failed

                                     ; now read from queue
          push qhandle               ; queue handle
          push ds                    ; address to receive PID
          push offset DGROUP:qident          ; and event code
          push ds                    ; receives message length
          push offset DGROUP:qmsglen
          push ds                    ; receives message pointer
          push offset DGROUP:qmsgptr
          push 0                     ; 0= read first element
          push 0                     ; 0= synchronous read
          push ds                    ; receives message priority
          push offset DGROUP:qmsgpri
          push ds                    ; handle for RAM semaphore
          push offset DGROUP:qsem         ; (not used for synch reads)
          call DOSREADQUEUE          ; transfer to OS/2
          or   ax,ax                 ; was read successful?
          jnz  error                 ; jump if read failed

          les  bx,qmsgptr            ; let ES:BX point to queue msg

          push es                    ; now release queue message's
          call DOSFREESEG            ; shared memory segment
          or   ax,ax                 ; was release successful?
          jnz  error                 ; jump if released failed

          push qhandle               ; now close the queue
          call DOSCLOSEQUEUE         ; also destroying it
          or   ax,ax                 ; was close successful?
          jnz  error                 ; jump if close failed
                                    ∙
                                    ∙
                                    ∙
qhandle   dw   ?                     ; receives handle from DosCreateQueue

qname     db   '\QUEUES\MYQUEUE',0        ; ASCIIZ queue name

qident    dw   0,0                   ; writer's PID, event code

qmsglen   dw   ?                     ; length of queue msg received

qmsgptr   dd   0                     ; address of queue msg received

qmsgpri   dw   ?                     ; priority of queue msg


Figure 8:  Opening a queue, writing a message into it, and then closing it.
                                    ∙
                                    ∙
                                    ∙
          push ds                         ; address to receive PID of
          push offset DGROUP:qowner       ; queue's owner
          push ds                         ; address to receive handle
          push offset DGROUP:qhandle
          push ds                         ; address of queue's name
          push offset DGROUP:qname
          call DOSOPENQUEUE               ; transfer to OS/2
          or   ax,ax                      ; was open successful?
          jnz  error                      ; jump if open failed


          push qmsglen                    ; length of shared memory segment
          push ds                         ; receives selector for segment
          push offset DGROUP:qselc
          push 1                          ; 1 = segment will be shared
          call DOSALLOCSEG                ; transfer to OS/2
          or   ax,ax                      ; was allocation successful?
          jnz  error                      ; jump if memory allocate failed


          mov  si,offset qmsg             ; copy our queue message
          mov  es,qselc                   ; to shared segment
          xor  di,di
          mov  cx,qmsglen
          cld
          rep movsb


          push qselc                      ; get a new selector for the
          push qowner                     ; shared segment that can
          push ds                         ; be used by receiver of
          push qselr                      ; the queue message
          call DOSGIVESEG                 ; transfer to OS/2
          or   ax,ax                      ; did function succeed?
          jnz  error                      ; jump if can't give it away


          push qhandle                    ; handle for queue
          push 0                          ; 0= event data
          push qmsglen                    ; length of queue message
          push qselr                      ; selector for queue data
          push 0                          ; (offset)
          push 0                          ; queue element priority
          call DOSWRITEQUEUE              ; transfer to OS/2
          or   ax,ax                      ; did write succeed?
          jnz  error                      ; jump if queue write failed


          push qselc                      ; release shared memory that
          call DOSFREESEG                 ; contains the queue message
          or   ax,ax                      ; did release succeed?
          jnz  error                      ; jump if release failed


          push qhandle                    ; now close the queue
          call DOSCLOSEQUEUE              ; transfer to OS/2
          or   ax,ax                      ; did close succeed?
          jnz  error                      ; jump if close failed
                                    ∙
                                    ∙
                                    ∙
qhandle   dw   ?                ; receives handle from DosOpenQueue

qowner    dw   ?                ; receives PID of queue owner

qname     db   '\QUEUES\MYQUEUE',0        ; ASCIIZ queue name

qselc     dw   ?                ; shared memory selector for caller

qselr     dw   ?                ; shared memory selector for receiver

qmsg      db   'This is my queue message'
qmsglen   equ  $-qmsg

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

The MS OS/2 LAN Manager

System connectivity has become an increasingly significant issue in the
development of today's personal computing systems. As a result, it is
becoming substantially more important for operating systems to provide
expanded support for Local Area Networking.

The design of Microsoft OS/2 recognizes this fact by providing substantial
networking support in the form of the Microsoft OS/2 LAN Manager.

Basically, the MS OS/2 LAN Manager is a protected mode version of
Microsoft Networks, and it is compatible with existing versions of MS-Net,
XENIX Net and the IBM PC Local Area Network Program.

The LAN Manager is a separately "installable" component of MS OS/2; once
installed, it works with the operating system to seamlessly solve
networking requirements.

The LAN Manager is designed to facilitate the traditional network file-
and printer-sharing services. But it goes far beyond this by providing for
a new class of communication facilities, which will allow software
developers to increase the sophistication of network services
substantially and will make distributed processing practical.

The LAN Manager uses the protected mode, multitasking facilities of MS
OS/2 to implement non-dedicated servers with extensive network
administration and security services, as well as transparent file- and
printer-sharing capabilities.

The LAN Manager allows an OS/2 based system to be added to existing MS-
Net-based networks. The new system can function within the network as a
workstation or a server or both.


Protected Mode NetBios

The multitasking environment in which the LAN Manager must function
requires a new, protected-mode version of the MS-Net NetBios. The NetBios
is the equivalent of the transport layer of the OSI model, and provides
for virtual circuit and datagram services.

The standardized NetBios API allows network drivers to access low-level
hardware (for such tasks as checking the status of a network card). The
NetBios is designed not only to understand the hardware at one end
(network hardware comes with a customized Hardware/NetBios layer), but
also to understand API calls from network drivers at the other end.
Applications access NetBios services via the network drivers.

The LAN Manager must deal with several multitasking issues. First,
applications will most likely need to share access to more than one
network driver. Second, there must be a way for network drivers to share
access to the NetBios services.

Central to the LAN Manager is the Microsoft Net Kernel, which provides
applications with the support necessary to simultaneously access one or
more network drivers. Very briefly, the kernel serves as a dispatcher for
applications' requests to access network drivers, and controls access to
those network drivers by treating each driver as a named, installable
device driver that can be opened or closed.

To insure that network drivers can interface to the net kernel and
therefore share access to the NetBios, developers must adhere to the
system interface specifications (and the protected mode NetBios API)
published as part of the Microsoft OS/2 LAN Manager. These specifications
will ease the effort that is necessary for OEMs and third-party network
software developers to design their hardware and software applications to
support the new multitasking, distributed processing environment.


Distributed Processing

From a software developer's point of view, the LAN Manager can be seen as
providing a set of extensions to the MS OS/2 Inter-process Communications
API. Just as MS OS/2 provides semaphores, pipes, and queues for
communication between different processes running on the same machine, the
LAN Manager provides named pipes and mailslots for communication between
processes running on different machines on the network.

Named pipes and mailslots are the building blocks of distributed
processing. These remote inter-process communication facilities provide
the ability to establish communications channels between tasks, allowing
for sharing of data──not only between a server machine and its clients,
but also among peer machines residing on a given network.

Data-base applications provide a clear example of the distinction between
file service and distributed proessing. Using current LAN technology, it
is typical to search a file resident on another machine by reading a
virtual file that is mapped to the real file by the MS-Net redirector.

In the distributed processing model, the requesting workstation passes a
request to the server, which then conducts the search and passes back the
results. The only information that travels on the network is the original
request and the reply. This requires a level of inter-machine
communication not possible with file- or record-oriented facilities.

The LAN Manager API is analogous at the network level to the MS OS/2
Inter-process Communication API that allows thread-to-thread and process-
to-process communication at the operating system level. At any given
moment, LAN API functions can block, be blocked, or can be in a state of
execution (i.e., reading, writing or querying either named pipes or
mailslots).

At the applications level, the LAN API provides CALL-based functions that
can be incorporated into high-level language code. Programs written to the
specifications of the API will be able to communicate across a network.


Virtual Circuits

The LAN API supports communications at the "Network and Transport Levels"
of the Open System Interconnect (OSI) model. Stream-oriented and
optionally, message-oriented information is communicated through a
mechanism called the named pipe. Message-oriented information can also be
communicated via the high-speed mechanism called a named mailslot.

To understand the difference between named pipes and mailslots, one needs
to understand two concepts basic to communications: those of virtual
circuits and datagrams.

Virtual circuits are network-level data channels that guarantee an error-
free and continuous communication channel, where each packet of routed
information is delivered in order. Regardless of how a channel is
established between two points (it could, for example, be a very complex
switching network in a phone system), the two communicating ends perceive
the channel as a direct communications link. This is, in essence, how
named pipes work.

Datagrams are network-level channels that accept isolated messages
(specific packets of information) from the transport layer and attempt to
deliver them without regard for order or timeliness, much the same way as
a postal system, in which the sender mails letters without knowing exactly
when they will arrive, or in what order (message B, sent after message A,
might arrive before message A). Named mailslots work in this fashion.

Across the network, either of these mechanisms lets processes
transparently establish remote or local communications through symbolic
names that follow MS OS/2 file naming conventions. Additionally, it is
possible to impose access restrictions on named pipes and mailslots, just
as can be done with files.


Named Pipes

Every reader of MSJ should be familiar with MS-DOS pipes. These pipes
function as a transparent holding area for the output of one program to
become the input of another. For example, DIR | SORT routes the output
from DIR to a temp file that is then read by SORT.

Named pipes essentially extend this facility to provide direct
communication between processes running on a network. Named pipes are two-
way (or full duplex) channels that can be written at either end and read
at the opposite end (that is, what is written at one end cannot be read
from that same end).

To create a named pipe a process executes the DosMakeNmPipe function call
(see Figure 1), which then opens a pipe as either a byte stream or a
message stream. DosMakeNmPipe returns an identifying handle for that end
of the pipe, which is called the serving end of the pipe. DosOpen (a
regular OS/2 API call) opens the other end of the pipe, known as the
client end, and similarly returns a handle.

Once a named pipe has been established it is controlled through the
facilities provided by the LAN Manager. It can be read, written to and
queried for activity or information. Named pipes can, at this point be
utilized across a network, and can be serially closed and opened by other
processes.

Named pipes are therefore extremely flexible. Once created they can be
accessed just like any sequential file, from beginning to end. Because
rewinding of pipes is not possible (information comes and goes), the
contents of a pipe can only be accessed once, unless the original
information is again sent through the pipe.

Local and remote procedure call dialogues between processes can be
efficiently implemeted because named pipes support transaction I/O calls.

Named pipes can also be accessed by applications running on MS DOS 3.x,
machines serving as workstations on a given network, allowing those
programs to communicate with server-based applications.

Thus, DosMakeNmPipe can allow multiple instances of a pipe, each with the
same name. This insures that multiple clients DosOpening to that name will
obtain separate and distinct pipes to the serving process.


Named Mailslots

Named pipes must insure that a full duplex communication channel exists
between processes. There is, of necessity, a considerable amount of system
overhead necessary to maintain such a connection.

Certain types of application-specific transactional messages to be
exchanged between processes do not require a full duplex, error-free
channel of communication. A named mailslot provides the optimal means for
handling messages under these conditions. Note that byte streams cannot
take advantage of named mailslots. Byte streams need the support
mechanisms built into named pipes to ensure error-free transmission.

Mailslots are inherently fast because they need not be opened or closed by
processes. Once a named mailslot has been established through the
DosMakeMailslot function call processes running on different machines
across a network simply write to it by name.

The function call used to write to a named mailslot, DosWriteMailslot,
supports both "class" and "priority" settings for messages. Priority
controls which messages DosReadMailslot reads first.

Class is usually used with remote mailslots. Messages can be assigned
either a first- or second-class status. First-class mail is reliable in
that DosWriteMailslot blocks until a message is delivered or an error
occurs, insuring that the state of the message will be known. Second-class
mail is sent strictly without error checking. Although delivery will
probably succeed, if it fails an error will not be reported.

Between high-speed named mailslots and highly reliable named pipes, the
LAN Manager provides the necessary communications facilities necessary to
meet most sophisticated network communications needs. Software developers
now have a base on which to develop complete, distributed applications.
The opportunity to take advantage of a programming environment in which
applications can access remote sites, and in which workstations can
offload work to a server, will drive the next generation of software.──T.R.


Figure 1:  DosMakeNmPipe Function

int FAR PASCAL

DosMakeNmPipe(name, handle, omode, pmode, size1, size2, timeout)
char far * name;         asciz pipe name
int far * handle;        place for handle to be returned
unsigned int omode;      DOS open mode
unsigned int pmode;      pipe open mode
unsigned int size1;      hint of outgoing buffer size
unsigned int size2;      hint of incoming buffer size
long      timeout;       time out for DosWaitNmPipe
Name:     Asciz name of pipe. Pipes are named
          \PIPE\NAME.

Handle:   Handle of the named pipe that is created.

Omode:    Open mode mask as for DosOpen call. The following bits are
          defined:

     Open Mode Bits
  5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
  O W * * * * * * I O O O * A A A

  I  Inheritance:   0 = Spawned processes inherit the pipe handle
                    1 = Spawned processes don't inherit the pipe
  W  Write-through: 0 = Write-behind to remote pipes is allowed
                    1 = Write-behind to remote pipes is not allowed
  AAA Access mode:  000 = Inbound pipe (client to server)
                    001 = Outbound pipe (server to client)
                    010 = Full-duplex pipe (server to/from client)
                    Other values invalid

Pmode: Pipe-specific mode parameters

  5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
  B * * * T T R R |... I count..|

  B Blocking:  0 = Reads/writes block if no data available
               1 = Reads/writes return immediately if no data

     Reads normally block until at least partial data can be returned.
     Writes by default block until all bytes requested have been written.
     Nonblocking mode (B=1) changes this behavior as follows:

  1) Reads will return immediately with BytesRead = 0 if no data is
     available.

  2) Writes will return immediately with BytesWritten  = 0 if the data
     transfer cannot be started. Otherwise, the entire data area will be
     transferred.

     TT Type of a pipe:     00 = Pipe is a byte stream pipe
                            01 = Pipe is a message stream pipe

        All writes to message stream pipes record the length of the write
        along with the written data.

     RR Read mode           00 = Read pipe as a byte stream pipe
                            01 = Read pipe as a message stream

        Message pipes can be read as byte or message streams, depending
        on the setting of RR. Byte pipes can only be read as pipe streams

     Icount: 8-bit count to control pipe instancing. When making the
        first instances of a named pipe, Icount specifies how many
        instances can be created: 1 means that this can be the only
        instance (pipe is unique) and -1 means the number of
        instances is unlimited; 0 is a reserved value. Subsequent
        attempts to make a pipe will fail if the maximum number of
        allowed instances already exists. The Icount parameter is
        ignored when making other than the first instance of a pipe.
        When multiple instances are allowed multiple clients can
        simultaneously DosOpen to the same pipe name and get handles
        to distinct pipe instances.

  Size1: Hint to system, number of bytes to allocate for outgoing buffer.

  Size2: Hint to system, number of bytes to allocate for incoming buffer.

  Timeout: Default value for timeout parameter to PipeWait. This value
        may be set only at the creation time of the first instance of the
        pipe name. If at that time the value is zero, a systemwide default
        value (50 ms) will be chosen.

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

A Complete Guide to Writing Your First OS/2 Program

Charles Petzold☼

For the program developer, OS/2 opens up a whole new world of capabilities
and challenges, and along with them, new concepts to learn. We'll explore
some of these new concepts by walking through the creation of a typical OS/2
application that demonstrates inter-process communication (IPC) and
multitasking.

FINDDEMO and FINDER work in conjunction to locate and list all files on a
disk that fit a particular file specification, for instance, *.EXE. You can
enter up to nine different file specifications at any one time, and all nine
run simultaneously. Figure 1☼ shows FINDDEMO in action.

FINDDEMO.EXE is the program that you execute from the OS/2 command line.
FINDER.EXE must be in the current subdirectory or a subdirectory in your
PATH. FINDDEMO manages the screen display and keyboard input and loads as
many as nine instances of FINDER.EXE as child processes. FINDER.EXE does the
actual file searching and funnels data back to FINDDEMO via a queue.
Separate threads within FINDDEMO read the queue and monitor termination of
the FINDER instances.

In other words, there's a real party going on here. I'll have more to say
about the workings of this program after we look at what's involved in
coding, compiling, and linking it. If you've already done some Windows
programming, don't be surprised to see some familiar things along the
way──you're several steps ahead of everybody else.


Protected Mode

Your first OS/2 programs will probably be existing MS-DOS programs that you
will port to OS/2. Most programs written in straight C with standard library
functions can simply be recompiled by using the new .LIB files.

However, programs with assembly language routines that make software
interrupt calls, programs that use the Microsoft C int86 or intdos library
functions, and programs that access memory outside the program or directly
manipulate I/O ports will have to be modified somewhat.

OS/2 supports nearly 200 function calls that you'll use for communicating
with the operating system and the PC's hardware. Obviously, these new
function calls encompass virtually everything you can do now under MS-DOS
3.x using Int 21H. However, they also include a much faster and more
versatile set of character-mode video I/O calls than the calls that have
previously been available from the IBM PC BIOS services. You will no longer
have to directly access video display memory in order to get good character-
mode performance. OS/2 also includes a set of mouse function calls and many
new calls that are related to tasking and inter-process communication.


DOS Function Calls

In C programs, OS/2 function calls look like ordinary function or subroutine
calls. The names of all the functions are capitalized, and they begin with
the letters DOS, KBD (keyboard), VIO (video), or MOU (mouse).

Virtually all OS/2 function calls return a zero if the function is
successful and an error code otherwise. If the function must return
information back to your program, one or more of the arguments will be a far
pointer to your program's data segment. OS/2 uses this pointer to store the
result of the function. Some OS/2 function calls also require far pointers
to structures for returning more extensive information.

For instance, if you want to obtain the current cursor position, the OS/2
function that you must use is VIOGETCURPOS. The first argument is a far
pointer to a variable where OS/2 stores the cursor row position, the second
argument is a far pointer to a variable to store the column, and the third
parameter is a "video I/O handle" that currently must be set to zero.

Within a C program you would define two variables for the row and column
cursor positions like this:

  unsigned row, col ;

and then call VIOGETCURPOS by using pointers to these variables:

  VIOGETCURPOS(&row, &col, 0) ;

The C source code files for FINDDEMO, shown in Figure 2, and FINDER, shown
in Figure 3 begin with the statement

  #include <doscall.h>

This DOSCALL.H header file contains just two lines:

  #include       <doscalls.h>
  #include       <subcalls.h>

In the DOSCALLS.H and SUBCALLS.H header files are the declarations for all
OS/2 functions. The DOS functions are declared in DOSCALLS.H, and the KBD,
VIO, and MOU functions are declared in SUBCALLS.H. For instance, the
VIOGETCURPOS function is declared in SUBCALLS.H as

  extern unsigned
  far pascal
  VIOGETCURPOS(unsigned far *, unsigned far *, unsigned) ;

All OS/2 functions are declared far, which directs the compiler to generate
a far inter-segment call, and pascal. The pascal keyword indicates a Pascal
calling sequence──the arguments are pushed onto the stack from left to
right, and the called function then adjusts the stack before returning
control to the caller.

Normally, C compilers generate code that pushes arguments on the stack from
right to left, and the caller fixes the stack. This calling sequence permits
C to support functions with a variable number of arguments. Moreover, you
can usually call a C function with an incorrect number of parameters, and
while the function may not work correctly, in many cases the program will
not crash.

A function that uses the Pascal calling sequence, however, expects to
receive a fixed number of arguments. Therefore, if you get the compiler
warning message "too few actual parameters" for an OS/2 function call, you
should fix the code before you try to run the program.

Note that the function declaration shows the first two parameters to be far
pointers to unsigned integers, but the VIOGETCURPOS call used simply &row
and &col. The C compiler generates far addresses without casting based on
the function declaration.

The DOSCALLS.H and SUBCALLS.H header files also contain structure
declarations used in some of the other OS/2 function calls. For instance,
FINDER.EXE uses the FileFindBuf structure in conjunction with the
DOSFINDFIRST and DOSFINDNEXT function calls. FINDDEMO.C uses the ResultCodes
structure for returning values from the DOSEXECPGM and DOSCWAIT functions,
and the KeyData structure for returning keyboard input from KBDCHARIN.
Here's what the KeyData structure declaration looks like:

  struct KeyData
    {unsigned char char_code ;
     unsigned char scan_code ;
     unsigned char status ;
     unsigned char nls_shift ;
     unsigned shift_state ;
     unsigned long time} ;

That's a little more information than you currently get from Int 16H, isn't
it?


Assembly Language

If you are programming in assembly language, OS/2 function calls are made by
pushing arguments on the stack and calling external far functions. Near the
top of your source code you specify that the OS/2 function is in a different
segment:

  extrn
  VIOGETCURPOS:far

The two variables to receive the row and column positions would go in your
data segment:

  row  dw   ?
  col  dw   ?

When you call VIOGETCURPOS, you push the arguments on the stack and make the
call:

  push ds
  push offset row
  push ds
  push offset col
  push 0
  call VIOGETCURPOS

When you return from the VIOGETCURPOS call, the AX register contains the
return value of the function call, and the stack pointer is the value prior
to pushing the arguments on the stack.

This code sample pushes immediate values on the stack. Because these
instructions are supported by the 80286 but not by the 8086, you have to
include the assembler directive

  .286c

in your source code file. Or you can simply transfer the values to registers
before pushing them on the stack.


Linking

The new LINK generates a "new-format executable." The extension is still
.EXE, but the format of the file is not the same as that used in MS-DOS
versions through 3.2. However, it is the same as the new-format executable
currently in use for Windows applications.

Among other things, the new-format executable contains an "import table"
that OS/2 uses to match the far calls in your program code to the
appropriate OS/2 function calls. The actual entry address of the function
call routine is inserted into the code when the program is loaded into
memory. (This is dynamic linking, which has appeared previously in Windows
programming.)

LINK requires that two libraries be explicitly listed in the command line.
These are SLIBC5.LIB, which contains normal C library functions that make
OS/2 functions calls, and DOSCALLS.LIB.

The DOSCALLS.LIB library is an "import library." It merely provides LINK
with the module names and ordinal numbers associated with the OS/2 function
calls. LINK uses this information to set up the import table contained in
the new-format executable. DOSCALLS.LIB performs the same function in OS/2
programs that the SLIBW.LIB library performs in Windows applications.


Module Definition File

The new LINK also requires that a module definition file be specified as the
fifth argument on the LINK command line. The module definition file (yet
another concept that Windows programmers are already familiar with) provides
information about your program's segments, stack size, and local heap
size. The two module definition files that describe FINDDEMO.DEF and
FINDER.DEF are shown in Figures 4 and 5, respectively.

The NAME line indicates the name of the module, which is usually the same as
that of the program. The DESCRIPTION is generally a copyright notice
imbedded in the .EXE file. The keyword PROTMODE indicates that the program
runs only in protected mode.

The CODE and DATA lines specify characteristics of the code and data
segments in your program. The options that are shown here are normal for
protected mode.

Both the code and data segments are MOVABLE. This is no sweat in protected
mode because a segment can be physically moved but still retain the same
selector address, previously known as the segment address. Specifying the
CODE segment as PURE means that the same code segment loaded in memory can
be used for multiple instances of the program. The automatic data segment of
a program cannot be PURE because it contains the stack and most likely
contains read/write data as well. However, data segments with read-only data
without relocation information can also be flagged as PURE and can be shared
among multiple instances of the program.

For large applications, you might divide the program into many different
segments and specify characteristics of each in the module definition file.
These characteristics would include the keywords PRELOAD, which means the
segment is loaded into memory when the program is run, and LOADONCALL, which
means the segment is loaded into memory only when a routine within it is
needed. Based on a least-recently-used algorithm, OS/2 can free up memory
that is occupied by code segments and later reload them when needed from the
.EXE file. This facility is essentially a built-in, hassle-free overlay
manager.

The automatic data segment of your program contains static variables, the
stack, and a local heap organized as in Figure 6.

The stack is fixed to the size specified in the STACKSIZE line of the module
definition file. The HEAPSIZE value is a minimum local heap size, which is
the size that OS/2 sets aside when your program is first loaded into memory.
If you use C memory allocation functions such as malloc to allocate more
memory from your local heap than is specified by HEAPSIZE, OS/2 can expand
the local heap──by physically moving the data segment in memory if
necessary.


MAKEing the Program

A make-file for the FINDDEMO application is shown in Figure 7. You create
the FINDDEMO.EXE and FINDER.EXE executables by running

  MAKE FINDDEMO

The compile step uses several compiler switches:

  cl -c -G2 -Zp
  finddemo.c

The -c switch specifies that the program is to be compiled but not linked;
this is necessary because the link step requires nondefault arguments. The
-G2 switch compiles code by using 80286 instructions. This is optional, but
it results in a slightly smaller .EXE size and faster execution.

The -Zp switch indicates that structures are "packed." Normally the C
compiler aligns each element of a structure on an even address. However,
some of the structures declared in DOSCALLS.H and SUBCALLS.H, such as the
KeyData structure shown earlier, contain consecutive char variables.


New Executable

The new-format executable created by LINK is really a superset of the old-
format executable. It begins with an old-format header. This makes it appear
to old DOS versions (and the OS/2 compatibility box) as a normal .EXE
program. However, when you link the FINDDEMO and FINDER programs, the old-
format header is constructed so that the program appears to require more
memory than is available. So, if you attempt to run FINDDEMO.EXE under real
mode (the OS/2 compatibility box or MS-DOS 2.x or 3.x), you get the message
"Program too big to fit in memory."

You have some alternatives to this message. The first alternative involves
the STUB option in the module definition file, which Windows programmers
already know about:

  STUB 'oldprog.EXE'

The file oldprog.EXE is an old-format executable that runs under MS-DOS 2.x
or 3.x. The STUB option directs LINK to insert this old-format executable in
the top of the new-format executable. Now the .EXE file appears to MS-DOS
2.x or 3.x as a regular program. When MS-DOS 2.x or 3.x loads the program,
this old program is executed instead.

Windows programmers generally include the line

  STUB 'WINSTUB.EXE'

in their module definition files. The standard WINSTUB.EXE program simply
displays the message "This program requires Microsoft Windows." You might
want to create a similar program that would display a message such as "This
program must be run in OS/2 protected mode."

The second alternative is to write a real-mode version and include that in
the STUB line if your program is short. If you're writing a program using
straight C without any explicit OS/2 function calls, you could simply
compile and link it under a different name by using the old real-mode
libraries. This process creates one .EXE file that actually contains two
versions of the same program: the protected-mode version runs under
protected mode, and the real-mode version runs under real mode.

For longer programs, or programs that use explicit OS/2 function calls, you
should consider the third alternative: going family style.


Family Style

The OS/2 developer's kit includes a library file called API.LIB that
translates many OS/2 function calls into equivalent MS-DOS Int 21H function
calls. Video I/O calls are translated into equivalent BIOS calls and direct
screen accesses. When you run

  BIND program

after linking, parts of this API.LIB library are tacked onto the end of the
.EXE file, and the old-format header is modified to run a loader program
that is also inserted into the .EXE file. When you run the resulting .EXE
file under a real-mode DOS version, the loader patches addresses into your
program that call the routines within this library rather than OS/2
functions. If you run the program under OS/2 protected mode, the loader and
library routines are simply discarded.

Thus, you have one executable file and one program that runs under
protected-mode and real-mode MS-DOS. This is called the "family API" model.

Not all OS/2 functions can be converted into old MS-DOS Int 21H calls. For
instance, the FINDDEMO and FINDER programs shown here use several OS/2
facilities that have no old MS-DOS equivalents. Some options of other OS/2
functions are not fully supported. However, you can determine within your
program whether it is running in real mode or protected mode and have
separate logic that is appropriate for each.


The Workings

Now that we know how to create FINDDEMO.EXE and FINDER.EXE, let's look at
how they work. FINDER is a child process run by FINDDEMO. FINDER does all
the work while FINDDEMO sits back and waits for the messages from FINDER
that report what it has found.

FINDDEMO starts off by creating a queue named \QUEUES\FINDDEMO. A queue is a
linked list maintained by OS/2 that you can use to transfer data from one
program to another. One program creates the queue; another program opens the
same queue. The program that creates the queue──in this case, FINDDEMO──has
read and write privileges. The program that then opens the existing
queue──in this case, FINDER──has write privileges. Queues may be first-in-
first-out (FIFO), last-in-first-out (LIFO), or based on priority; the
FINDDEMO queue is FIFO.

FINDDEMO also creates two additional threads of execution. These appear in
the program as functions called dispthread and waitthread. These two threads
run simultaneously with the main thread, sometimes called the "parent"
thread. I'll explain what they do a little later.

FINDDEMO then prompts for a number and a file specification from the user.
For each file specification you enter, FINDDEMO executes FINDER by using the
OS/2 function call DOSEXECPGM. FINDER is executed in an asynchronous
mode──the DOSEXECPGM call returns immediately, and FINDDEMO can prompt for
the next file specification. As many as nine instances of FINDER can be
executed. Although these multiple instances use different data segments,
they share the same code segment in memory.

When FINDDEMO executes FINDER it passes to it the file specification, the
name of the queue, and an index number from 1 to 9 that indicates the
display line on which the file specification appears.

Now let's look at FINDER. FINDER opens the queue and searches for files
meeting the file specification over the entire disk by using a recursive
function called find. You'll note that FINDER changes the subdirectory
during this search. Under OS/2, each process maintains its own current
subdirectory. So each instance of FINDER can change the subdirectory and
not affect other instances. Nice, huh?

When FINDER finds a file that fits the file specification, it allocates a
small shared segment of memory using DOSALLOCSEG and copies the pathname
into it. The DOSGIVESEG function obtains a new selector (segment address)
appropriate for its parent FINDDEMO. (FINDER obtained the process ID of its
parent when it opened the queue.) FINDER can then write this segment to the
queue.

Now back to FINDDEMO. During initialization, FINDDEMO created a second
execution thread, the subroutine called dispthread. This thread sits in an
infinite loop that starts off with a call to DOSREADQUEUE. When DOSREADQUEUE
returns control to the program, it reads the contents of the shared memory
segment created by FINDER and writes it to the display.

The other thread created during FINDDEMO initialization is waitthread. This
is also an infinite loop that waits, using DOSCWAIT, for instances of FINDER
to terminate. Because DOSCWAIT returns with an error if no child processes
are running, FINDDEMO first sets nine semaphores and clears one only when
executing FINDER. The waitthread thread is suspended, through the use of the
function DOSMUXSEMWAIT, until one of these semaphores is cleared.

Another set of semaphores, which are in the array runsem, are normally
cleared. They are set when an instance of FINDER is executed and cleared
when the instance terminates. These semaphores are used when you terminate
FINDDEMO by using the Esc key. FINDDEMO uses DOSKILLPROCESS to terminate any
instances of FINDER that are still running and then waits for all of the
runsem semaphores to be cleared.


The New Generation

I claimed earlier that FINDDEMO's use of inter-process communication and
multitasking represented a "typical" OS/2 application. Obviously, you can
take advantage of the larger protected mode addressing space and more
versatile OS/2 API without getting into the fancy stuff. Whether you do get
fancy or not, I'm sure you'll find some interesting applications for these
new OS/2 capabilities.


Figure 2:  FINDDEMO.C

/* finddemo.c -- program to demonstrate IPC and multitasking */

#include <doscall.h>

#define WORD  unsigned int
#define DWORD unsigned long

#define NUMPROC     9                   /* number of process */
#define CHILDPROG   "FINDER.EXE"
#define QUEUENAME   "\\QUEUES\\FINDDEMO"
#define THREADSTACK 1024

void far dispthread (void) ;
void far waitthread (void) ;

WORD   queuehandle, count [NUMPROC] ;
DWORD  runsem [NUMPROC], waitsem [NUMPROC] ;
struct ResultCodes rc [NUMPROC] ;
struct {
     WORD count ;
     struct {
          WORD  reserved ;
          DWORD far *sem ;
          } index [NUMPROC] ;
     } semtab ;

main ()
     {
     static char prompt[] = "Line number or Esc to end -->  \b" ;
     struct KeyData keydata ;
     WORD   index, len, i, dispID, waitID, sigaction ;
     DWORD  sigaddr ;
     char   dirmask [15], dispstack [THREADSTACK],
            waitstack [THREADSTACK] ;

     /*
         Initialize: Set up "semtab" structure for DOSMUXSEMWAIT.
         ----------  Disable Ctrl-Break and Ctrl-C exits.
                     Create queue for IPC with FINDER.EXE.
                     Create threads for messages from FINDER
                         and waiting for FINDER terminations.
                     Display text.
     */

     semtab.count = NUMPROC ;
     for (index = 0 ; index < NUMPROC ; index++) {
          DOSSEMSET ((DWORD) &waitsem[index]) ;
          semtab.index[index].sem = &waitsem[index] ;
          }
     DOSSETSIGHANDLER (0L, &sigaddr, &sigaction, 1, 1) ;
     DOSSETSIGHANDLER (0L, &sigaddr, &sigaction, 1, 4) ;

     if (DOSCREATEQUEUE (&queuehandle, 0, QUEUENAME)) {
          puts ("FINDDEMO: Cannot create new queue") ;
          DOSEXIT (1, 1) ;
          }
     if (DOSCREATETHREAD (dispthread, &dispID, dispstack + THREADSTACK) ||
         DOSCREATETHREAD (waitthread, &waitID, waitstack + THREADSTACK)) {
          puts ("FINDDEMO: Cannot create threads") ;
          DOSEXIT (1, 1) ;
          }
     displayheadings () ;

               /*
                    Main Loop: Display prompt and read keyboard.
                    ---------  Execute FINDER.EXE.
               */
     do   {
          VIOSETCURPOS (18, 0, 0) ;
          VIOWRTTTY (prompt, sizeof prompt - 1, 0) ;
          KBDCHARIN (&keydata, 0, 0) ;

          index = keydata.char_code - '1' ;

          if (index <= NUMPROC && rc[index].TermCode_PID == 0) {
               VIOWRTTTY (&keydata.char_code, 1, 0) ;
               VIOWRTNCHAR (" ", 77, 7 + index, 3, 0) ;
               do   {
                    VIOSETCURPOS (7 + index, 3, 0) ;
                    len = 13 ;
                    KBDSTRINGIN (dirmask, &len, 0, 0) ;
                    }
               while (len == 0) ;

               dirmask [len] = '\0' ;
               executeprogram (index, dirmask) ;
               }
          }
     while (keydata.char_code != 27) ;

               /*
                    Clean-up: Kill all existing FINDER.EXE processes.
                    --------  Wait for processes to terminate.
                              Close the queue and exit.
               */

     for (index = 0 ; index < NUMPROC ; index++)
          if (rc[index].TermCode_PID)
               DOSKILLPROCESS (0, rc[index].TermCode_PID) ;

     for (index = 0 ; index < NUMPROC ; index++)
          DOSSEMWAIT ((DWORD) &runsem [index], -1L) ;

     DOSCLOSEQUEUE (queuehandle) ;
     DOSEXIT (1, 0) ;
     }

displayheadings ()
     {
     static char heading  [] = "286DOS File Finder Demo Program",
                 colheads [] = "Dir Mask     Status  Files",
                 colunder [] = "--------     ------  -----" ;
     char        buffer  [5] ;
     WORD        row, col, i, len ;

     VIOGETCURPOS (&row, &col, 0) ;              /* get current attr */
     VIOWRTTTY (" ", 1, 0) ;
     len = 2 ;
     VIOREADCELLSTR (buffer, &len, row, col, 0) ;
     VIOSCROLLUP (0, 0, -1, -1, -1, buffer, 0) ;     /* clear screen */

     len = sizeof heading - 1 ;
     col = (80 - len) / 2 ;
     VIOWRTCHARSTR (heading, len, 1, col, 0) ;            /* heading */
     VIOWRTNCHAR   ("\xC6",   1, 2, col - 1,   0) ;     /* underline */
     VIOWRTNCHAR   ("\xCD", len, 2, col,       0) ;
     VIOWRTNCHAR   ("\xB5",   1, 2, col + len, 0) ;
     VIOWRTCHARSTR (colheads, sizeof colheads - 1, 5, 3, 0) ;
     VIOWRTCHARSTR (colunder, sizeof colunder - 1, 6, 3, 0) ;

     for (i = 0 ; i < NUMPROC ; i++) {                    /* numbers */
          sprintf (buffer, "%d.", i + 1) ;
          VIOWRTCHARSTR (buffer, 2, 7 + i, 0, 0) ;
          }
     }

executeprogram (index, dirmask)
     WORD index ;
     char *dirmask ;
     {
     char objbuf [32] ;
     char args [128] ;

     strcat (strcpy (args, CHILDPROG), " ") ;        /* construct args */
     strcat (strcat (args, dirmask), " ") ;
     strcat (strcat (args, QUEUENAME), " ") ;
     itoa (index, args + strlen (args), 10) ;

     count [index] = 0 ;                             /* initialize count */

     if (DOSEXECPGM (objbuf, 32, 2, args, 0, &rc[index], CHILDPROG))
          {
          puts ("FINDDEMO: Can't run FINDER.EXE") ;
          DOSEXIT (1, 1) ;
          }
     VIOWRTCHARSTR ("Running", 7, index + 7, 16, 0) ;/* now executing */
     DOSSEMSET   ((DWORD) &runsem [index]) ;
     DOSSEMCLEAR ((DWORD) &waitsem[index]) ;
     }

void far dispthread ()        /* thread to read messages from FINDER */
     {                        /*   and display filenames.            */
     DWORD request ;
     WORD  len, index, i ;
     char  far *farptr ;
     char  priority, pathname [80], buffer [64] ;

     while (1) {
          DOSREADQUEUE (queuehandle, &request, &len,
                         &(DWORD)farptr, 0, 0, &priority, 0L) ;
          i = 0 ;
          while (pathname [i++] = *farptr++) ;
          index = (WORD) (request >> 16) ;
          count [index] += len > 0 ;
          sprintf (buffer, "%5d   %-48.48s", count [index], pathname) ;
          VIOWRTCHARSTR (buffer, 56, 7 + index, 24, 0) ;
          DOSFREESEG ((WORD) ((DWORD) farptr >> 16)) ;
          }
     }

void far waitthread ()     /* thread to wait for FINDER terminations */
     {
     WORD   index, PID ;
     struct ResultCodes rescode ;

     while (1) {
          DOSMUXSEMWAIT (&index, (WORD far *) &semtab, -1L) ;
          DOSCWAIT (0, 0, &rescode, &PID, 0) ;

          for (index = 0 ; index < NUMPROC ; index++)  /* find index */
               if (PID == rc[index].TermCode_PID)
                    break ;

          VIOWRTCHARSTR (rescode.TermCode_PID ? "Halted " : "Done   ",
                              7, index + 7, 16, 0) ;
          rc[index].TermCode_PID = 0 ;
          DOSSEMCLEAR ((DWORD) &runsem [index]) ;
          DOSSEMSET   ((DWORD) &waitsem[index]) ;
          }
     }


Figure 3:  FINDER.C

/* finder.c -- child process of finddemo */

#include <doscall.h>

unsigned index ;
unsigned queuehandle ;
unsigned parentPID ;

main (argc, argv)
     int  argc ;
     char *argv [] ;
     {
     if (argc < 4) {
          puts ("Run FINDDEMO rather than FINDER") ;
          DOSEXIT (1, 1) ;
          }
     if (DOSOPENQUEUE (&parentPID, &queuehandle, argv [2])) {
          puts ("FINDER: Cannot open queue") ;
          DOSEXIT (1, 1) ;
          }
     index = atoi (argv [3]) ;

     writequeue ("") ;
     chdir ("\\") ;
     find (argv [1]) ;
     writequeue ("") ;

     DOSCLOSEQUEUE (queuehandle) ;
     DOSEXIT (1, 0) ;
     }

find (searchstr)
     char     *searchstr ;
     {
     struct   FileFindBuf ffb ;
     char     cwd [81], pathname [100] ;
     unsigned handle = 0xFFFF, num = 1 ;

     if (cwd [strlen (getcwd (cwd, 80)) - 1] != '\\')
          strcat (cwd, "\\") ;

     DOSFINDFIRST (searchstr, &handle, 7, &ffb, sizeof ffb, &num, 0L) ;
     while (num) {
          writequeue (strcat (strcpy (pathname, cwd), ffb.file_name)) ;
          DOSFINDNEXT (handle, &ffb, sizeof ffb, &num) ;
          }
     DOSFINDCLOSE (handle) ;

     handle = 0xFFFF ;
     num  = 1 ;
     DOSFINDFIRST ("*.*", &handle, 0x17, &ffb, sizeof ffb, &num, 0L) ;
     while (num) {
          if (ffb.attributes & 0x10 && ffb.file_name [0] != '.') {
               chdir (ffb.file_name) ;
               find (searchstr) ;
               chdir ("..") ;
               }
          DOSFINDNEXT (handle, &ffb, sizeof ffb, &num) ;
          }
     DOSFINDCLOSE (handle) ;
     }

writequeue (str)
     char *str ;
     {
     unsigned selector, parentselector ;
     char far *farptr ;
     int      len = strlen (str) ;

     DOSALLOCSEG (len + 1, &selector, 1) ;
     farptr = (char far *) (((unsigned long) selector) << 16) ;

     while (*farptr++ = *str++) ;

     DOSGIVESEG (selector, parentPID, &parentselector) ;
     DOSFREESEG (selector) ;
     farptr = (char far *) (((unsigned long) parentselector) << 16) ;

     DOSWRITEQUEUE (queuehandle, index, len, farptr, 0) ;
     }


Figure 4:  FINDDEMO.DEF

NAME            FINDDEMO
DESCRIPTION     'File Finder Demonstration Program'
PROTMODE
DATA            MOVABLE
CODE            MOVABLE PURE
HEAPSIZE        2048
STACKSIZE       4096


Figure 5:  FINDER.DEF

NAME            FINDER
DESCRIPTION     'File Finder Module for FINDDEMO'
PROTMODE
DATA            MOVABLE
CODE            MOVABLE PURE
HEAPSIZE        2048
STACKSIZE       8192


Figure 6:  Automatic data segment containing static variables, the stack,
           and a local heap.

               ┌───────────────────────────┐▄
               │                         ╔═══════════════╗
               │        Local Heap       ║  High Memory  ║
               │                         ╚═══════════════╝
               └───────────────────────────┘█
                 ████████████████████████████

               ┌───────────────────────────┐
               │                           │█
               │           Stack           │█
               │                           │█
               └───────────────────────────┘█
                 ████████████████████████████

               ┌───────────────────────────┐▄
               │                         ╔═══════════════╗
               │     Static Variables    ║   Low Memory  ║
               │                         ╚═══════════════╝
               └───────────────────────────┘█
                 ████████████████████████████


Figure 7:  Make-file for FINDDEMO.EXE and FINDER.EXE

finddemo.obj  :  finddemo.
    cl -c -G2 -Zp finddemo.c

finddemo.exe  :  finddemo.obj finddemo.def
    link finddemo, /align:16, /map, doscalls slibc5, finddemo.def

finder.obj  :  findef.c
    cl -c -G2 -Zp finder.c

finder.exe  :  finder.obj finder.def
    link finder, /align:16, /map, doscalls slibc5, finder.def

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

An Interview with Gordon Letwin: OS/2: Turning Off the Car to Change Gears

───────────────────────────────────────────────────────────────────────────
Also see the related article:
  Gordon Letwin: The Challenge of the 286 Speaks His Language
───────────────────────────────────────────────────────────────────────────

Lori Valigra☼

The development of Microsoft's multitasking operating system has taken many
twists and turns since Microsoft began working on it three years ago. The
many stages it has gone through so far have resulted from working with the
schizophrenic Intel 80286 chip, which can run in either of two incompatible
modes: protected (286) mode, and unprotected (8086 or "real") mode. The main
problem has been to get an operating system which runs both protected- and
real-mode programs. Microsoft solved the problem with mode switching──a
feature which essentially works by mimicking the act of turning off the
computer and restarting it, similar to turning off a car in order to switch
gears.

The new operating system began as an ambitious project to produce an
operating system for Intel 80286-based computers running a mixture of office
automation applications. This new operating system was to be capable of
multitasking and compatible with the protected 286 mode. But as the project
got too unwieldly, Microsoft split it up in early 1985, announcing a version
called DOS 4. This version incorporates multitasking but does not include
protected mode features. The DOS 5 project, as it was called, is intended to
support the protected mode and add other features.

OS/2, as the project has finally been named, was easily the most difficult
and challenging program written at Microsoft to date, with more than 350,000
lines of code. While Microsoft generally feels that the most efficient
projects are those staffed by one person, the complexity and time
constraints of the OS/2 project required that the company assign more than
35 programmers to it. Managing the project──dividing the programmers into
teams, dividing the project among them, and coordinating the various teams'
modifications to the program's code──became an enormous challenge. Adding to
the challenge was the joint development of the product with IBM, a company
whose culture and methodology are quite different from Microsoft's.

Microsoft Systems Journal talked with the project's chief architect, Gordon
Letwin, to learn more about the planning, development, and cooperative
effort that went into the development of OS/2.

MSJ: What was the goal of the OS/2 project?

Letwin: We tried to create the underpinnings for the ideal office automation
operating system. In our opinion, that operating system would allow
applications direct access to high-bandwidth peripherals, offer device-
independent graphics drivers, provide multitasking without performance
degradation (compared with a single-tasking system), run programs in both
the protected 286 and real 8086 modes of the 286 processor, and provide a
protected environment to assure system stability. I think we've done a
pretty good job.

MSJ: Microsoft has been developing multitasking operating systems for three
years now, beginning with DOS 4. What was the purpose of that operating
system, and where does it stand now that OS/2 is out?

Letwin: DOS 4 was the first product to result from Microsoft's multitasking
DOS effort. We began it even before IBM introduced the PC AT. It was an
ambitious product that was originally to include a protected mode with mode
switching abilities so it could run on the 8086 or the 286.

A general-purpose multitasking system needs to run in both modes: the
unprotected 8086 mode so that we can run existing DOS applications, and the
protected 286 mode so that we can multitask arbitrary, unrelated
applications. But the architecture of the 286 caused some delays. Although
we knew the project would be difficult, it was only after we'd gotten deeply
into it that we realized just how difficult it would be.

As a result, DOS 4 became too complicated for our schedules. Because of the
pressure of customer demand as well as that of previous commitments, we
broke the project into two parts. DOS 4 runs only in real mode and provides
multitasking only for specialized applications. DOS 5, which has now been
released as OS/2, includes the protected mode and other features. DOS 4 was
delivered in the last half of 1986 and is being sold in special application
environments, primarily in Europe. It is a specialized product that can
share the market with OS/2, because it runs on 8086 hardware, while OS/2
requires a 286. The move from DOS 4 to OS/2 was a gradual evolutionary
process. OS/2 is by far the most complex and sophisticated operating system
project we've worked on. To offer multitasking in a protected environment,
we developed it jointly with IBM to get around the constraints of the 286.

MSJ: How does OS/2 compare with earlier multitasking operating systems like
Concurrent CP/M?

Letwin: Unlike OS/2, Concurrent CP/M has a major problem. It serializes
access to the file system. When multitasking you need to make the operating
system features such as the file system accessible to multiple programs at a
time. But Concurrent CP/M didn't do this. It only allowed one program to
call the file system at a time. So in a real life situation with lots of
I/O, Concurrent CP/M was too slow.

MSJ: What are some of the features of OS/2 that you consider particularly
neat?

Letwin: One is the seamless system service interface, which means that
system services don't need to be in the kernel. Some can be provided by
subroutine libraries and others by system processes. Many services can be
upgraded piecemeal, in the field, without changing the whole operating
system. This is accomplished by a technique called "dynamic linking," which
allows external procedure calls to be linked up at execution time, rather
than link time. This same mechanism is used as the standard system interface
between applications, utility packages, etc.

Another exceptional feature is direct hardware access, which allows an
application to write directly to the hardware. For example, it will do
special graphics on a screen.

MSJ: How did you get around the limitations of the 286 chip?

Letwin: The 286 is designed to run in only one mode, either real mode or
protected mode. Since we knew we needed to support programs written using
protected mode, a group of us, including Bill Gates and some project
engineers, brainstormed to try to figure out how to run 8086 mode
applications from within protected 286 mode. One option was "real mode
emulation," where we could emulate the operation of 8086 real mode by doing
a lot of special things to the protected mode segment tables. IBM was
examining this technique independently. We thought emulation would be too
slow, since the 286 would run at one-third the speed of an 8086. IBM's
research later showed this to be the case. Soon thereafter, Intel began
shipping a chip with a bug in it that would stop the technique anyway, so
we tried another approach: mode switching.

The 286 chip switches from real mode to protected mode very easily, but it
was not designed to switch back from protected mode to real mode at all. You
could only switch back to real mode by causing a full system reset. The idea
of doing a full system reset while running a multitasking system was very
radical. IBM was aware of the mode switching technique itself and had the
capability for it in the AT BIOS for use during power-up diagnostics and for
special block copies from high memory. However, this feature was never
intended for use during full system operation. What we were thinking of was
similar to switching gears in a car by turning the motor off. The idea was
to master-clear the processor and restart it. The question was, would this
interfere with DMA transfers that might be happening at the same time, lose
pending interrupts, or fail in some other way? The IBM ROM code was designed
to "switch gears by turning off the car" with the car standing still. We
wanted to switch gears with the car traveling at highway speeds, and without
causing a jolt. Working together with IBM, we set up an experiment to see if
it would work, and it did. However, it took us months to convince everyone
involved that the technique was feasible. The first time we presented the
idea to IBM Boca, they gave us a funny look and suggested that perhaps we'd
had a bit too much sun.

There is one difficulty with mode switching, however. Because the designers
thought that it would be an infrequent operation, done primarily during the
power-on diagnostics, they implemented it in a fashion that saved some
components but produced a very slow mode switch: typically 1,000
microseconds, round trip. During this 1 millisecond interval the system
cannot respond to interrupts. Any interrupts that arrive then are processed
after the mode switch. This delay would normally be a problem only for high-
speed communications at 96K bps or faster while you are simultaneously
interacting with a real-mode program. You may experience data overruns while
the real-mode screen group is on the display. Mode switching will affect any
program that requires a very high-speed interrupt service rate faster than
500 Hz. If the communications protocols on your system can handle missing
characters by asking for a retransmit, and most can, then communications
will be okay.

MSJ: Why is protection so important in OS/2?

Letwin: We knew we needed a multitasking system to accomplish our goals, and
we knew we needed to have it protected. If you run a multiprocessor system,
software can accidentally read or change other programs or data if the
memory is unprotected. You can't stop it and you can't tell it happened. The
system can crash or you could corrupt data. Even in an unprotected DOS 3
system you can have problems with running programs simultaneously. SideKick,
for example, loads into memory and terminates itself. It keeps memory in
use, but the system doesn't know what is in that memory location. If you
load several such programs at the same time and they conflict on how they
use memory or interrupt vectors, the software will not work correctly, or
the system will crash. So in an unprotected environment the system is always
vulnerable to flaky software or conflicts between software products from
different vendors.

Of even greater importance than this is the fact that we needed to use
protected mode to allow programs to access more than 640K bytes of memory.
The 640K limit in earlier versions of DOS is a result of the addressing
limitations of real mode. Protected mode programs are released from this
restriction, and can use many megabytes of memory. As memory costs continue
to drop and memory consumption by applications rises, it has become
unacceptable to be restricted to the 640K that real mode provides.

MSJ: How did you plan such an ambitious project, both conceptually and in
getting the needed resources?

Letwin: The first step was to decide what we wanted the system to do. Then,
when we discovered how many of those capabilities we could implement, and by
when, we could revise our goals. As a result, the initial planning process
was iterative. We needed to have multitasking and to execute existing DOS 3
programs. While there were still only a few engineers on the project, we
thought about techniques such as mode switching and when necessary,
prototyped some code. As we got a better handle on what we could do, and how
long it would take, we met again with Bill Gates and revised our goals. The
tone of the first phase was as ambitious as possible, and we listed all the
things we thought we could do. The second phase involved determining which
of those features were feasible in the required time frame. As soon as the
general scope of the project was determined, the engineers met in groups of
two and three to lay out further details of the architecture of the system.

After the initial meetings came the joint design work. Engineering then
finalized the goals. Because of the scope of the project, this design phase
took a few months.

Next was the sign-up phase, a tradition in the computer industry, where we
explained the excitement, the importance, and the tight schedule of the
project, then let everyone "sign up" for a long stretch of hard, focused
work. You can't force someone to excel; you've got to provide the
opportunity and the encouragement and then let them excel.

Then the actual work began. We decided on the key systems and the key
characteristics of the product. Next came the design sessions for each of
the key systems. About a half dozen people were involved, typically three
from Microsoft and three from IBM.

During the coding months, my job was to understand fully all of the parts of
the system and to make sure that everthing worked toward achieving our
overall system goals. In a project this large, with this many people working
on it, it's easy for design elements to diverge. One project member might
think that speed is more important than code size, and write his code that
way. Another might think the opposite and write his code the other way. The
result would be that neither goal is achieved. I participated in the design
of each component to insure that the goals and design philosophies of the
system's architecture were consistent throughout the code. On most projects,
this role can be fulfilled on a "part time" basis, leaving time for the
chief designer to also get to write some code. Unfortunately, on the OS/2
project this position required a full time effort, leaving me little
opportunity to write any real code.

MSJ: How did you divide up tasks on the project?

Letwin: The most efficient programming is done by one person. It's a classic
saying in the industry that "if one person can do it in one year, two people
can do it in two years." But in a project like OS/2, although one person
might be able to do it in 8 man years, you can't afford to wait 8 years, so
you have to make it a multi-person project and have it take 30 man years. In
order to reduce the inefficiency introduced by having multiple people on a
project, it's important to break the work up properly, and to have each
piece of work be a one man project. This project had about 35 people on it.

The programmers worked on individual parts of the code, but they were
organized into teams with a maximum of eight people each to facilitate
coordinating their work. There was a test team, two teams of developers each
headed by a senior engineer, a build team with four people, a test team with
five people, and myself as the chief architect. This project was so
complicated, especially since development work was simultaneously taking
place at IBM in Boca Raton, Florida, that we needed a build team just to
coordinate the sources, build the executable binaries, and otherwise bring
organization to the physical aspects of software development. The test team,
as its name implies, was responsible for the development of test cases, bug
tracking, and performance measurement.

MSJ: What was the most difficult part of managing the project?

Letwin: This project's very large staff added to the complexity of
integrating changes made at the IBM locations and timing the development
between the two sites. We had to be especially careful that any changes made
didn't affect other parts of the project. In the source code control system,
for example, we split the source into about 200 modules, with modification
tracking on each. We had a rule that you couldn't add a change to a module
until you'd proven that the change wouldn't break anything. That way we
retained a totally stable version. Without this rule, each engineer could
have spent days looking for problems that were, in fact, introduced by
another engineer.

MSJ: IBM and Microsoft, to put it mildly, have very different corporate
cultures and philosophies. How did you mesh the two environments during the
development of OS/2?

Letwin: First, whenever we arrived in Boca Raton, we rushed into the
bathroom and changed into suits.

But seriously, there were some times when differences of opinions were
frustrating. Normally each company builds its own products and calls all the
shots. But in a joint development, compromises were necessary. IBM engineers
were slowed down by a set of rules which are intended to bring stability to
large IBM projects. This naturally caused some conflicts. IBM-ers
undoubtedly felt that Microsoft engineers were "wild and crazy guys."

MSJ: What parts of OS/2 would you do differently if you had the choice?

Letwin: A year ago it became clear we weren't going to meet our schedules,
and we had to go through a simplification procedure. For example, we took
out named pipes, a form of inter-process communication to send information
to another program (now part of the OS/2 LAN Manager). We'll put that in
another release of the product. Installable file systems were another
victim. As a result, the hard disk file system still doesn't perform as well
as we'd all like. These features will be added later. Also, we now know the
parts of the system which turned out to be bigger or slower than they
should, or which in some other way were a bit disappointing. Naturally we'd
give them additional attention. At the end of any project, there are always
a few things you'd like to improve. And we will, in the next release.

MSJ: How does it feel to give up your product when it's ready to go to
market?

Letwin: At first, I felt relief when we got the product through testing and
it went out the door. The last 20 percent is hard work. You just get tired
of the product. The exciting stuff takes place in the earlier stages of the
project. The last days are dealing with problem after problem──fixing bugs,
making it smaller, making it faster, etc. You've worked on it so much there
dosen't seem to be much change over the last few months. You're tired and
all you do is deal with problems. In my opinion, it's the ability to keep
working hard during the last part of a long project that separates the great
people from the average. In any case, when you finally move on to something
else, you feel a mixture of relief that it's completed, excitement about
your new project, and anxiety about how well your efforts will be received.

MSJ: What's your philosophy of programming?

Letwin: Software is special at our level. Computer programming is 20 percent
science, 60 percent engineering, and 20 percent art. The "art" element is
the hardest to achieve, and it's the hardest to explain. When engineers talk
about the "elegance" of their work, people usually think that such elegance
is non-functional: the engineers are just indulging themselves. This might
be the case in mechanical or civil engineering since an elegant bridge
carrys no more load than an inelegant one. But it isn't so in advanced
software development because mechanical engineering deals with physical
objects and software engineering deals with information. Physical objects
have properties independent of human perception, but information doesn't. In
other words, although a bridge is equally functional whether or not it's
elegant, information is much more useful when it's organized in a clear,
flexible and useable form──a form a programmer would call "elegant." Bridge
design is organized to meet just two goals──buildability and final function.
Software is designed to meet those goals plus one more. It has to have
minimal intellectual complexity and maximum flexibility so that it can be
understood by the programmers who will maintain and upgrade it in the
future.


───────────────────────────────────────────────────────────────────────────
GORDON LETWIN: THE CHALLENGE OF THE 286 SPEAKS HIS LANGUAGE
───────────────────────────────────────────────────────────────────────────

What attracted 34-year-old Gordon Letwin to computers in the late 1960s has
also kept him in the forefront of programming today: an insatiable drive to
overcome technical obstacles.

"I'd read about computers in the late 1960s, that they were supposed to be
super complicated. Computers held an abstract mystique. And I wanted to
understand how that stuff worked," he says. That was a tough goal at a time
when the cheapest computers cost $100,000 and were locked behind glass.
Letwin taught himself FORTRAN from a manual, without access to a computer to
try it on. While in high school he took some courses at a small private
technical college to get free time on the school's computer.

Letwin studied physics at Purdue, but found he was more interested in
getting computer time than in studying physics. Whenever he could, he'd
spend his day breaking through the computer's protective environments
instead of studying physics. "Saying that there's something that I can't
understand, or can't access because I'm not initiated into some mystery is
like waving a flag in front of a bull," he says. He finally solved this
conflict by changing his major to computer science.

A large part of the enjoyment in computing for Letwin is getting immediate
results. He likens computers to pinball machines. "You get positive and
negative reinforcement right away." And, being "innately lazy," he likes the
idea of computers doing work for him.

Upon getting his Bachelor's and Master's degrees in computer science from
Purdue, Letwin went to work for Wintek, a company run by one of his
professors. He ended up bidding on a contract to supply Heathkit with a
BASIC interpreter, assembler, and editor.

He later moved to Michigan and went to work at Heath but found he didn't
agree with its management's direction. "I wasn't happy with the direction of
the company, which was run by `suits' who didn't know computers well but
insisted on making the technical decisions anyway." When his concerns with
Heath's approach at that time weren't improving, he signed on with
Microsoft, then in Albuquerque, New Mexico. "I came here because of Bill
Gates," says Letwin, who met Microsoft's chairman during sales meetings at
Heath.

Letwin became the first new employee when the firm relocated to Seattle
about eight years ago. He started working on a BASIC compiler, then a Pascal
compiler.

When he became tired of being a "compiler compiler," he took advantage of
his operating system experience to switch to the newly formed operating
systems group, starting work on the IBM PC. "We thought the 8086 chip was
the first processor powerful enough for general purpose desktop use. We had
a vision of an office of the future. We knew that to realize this vision,
we'd need a powerful operating system, much more sophisticated then those
which were available at that time. Since the only way to insure its
existence was to write it ourselves, we got into the operating system
business." The widespread acceptance of MS-DOS makes that statement
Microsoft's platform for its development of the single-user office
automation operating system

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

A Simple Windows Application for Custom Color Mixing

Charles Petzold☼

COLORSCR ("Color Scroll"), is a program for Microsoft Windows that displays
three scroll bars in the left half of the client area, which are labeled
"Red," "Green," and "Blue," as shown in Figure 1☼. As you scroll the scroll
bars, the right half of the client area will change to the composite color
that is produced by the mix of the three primary colors. The numeric values
of the three primary colors are displayed under the three scroll bars.

You can scroll the scroll bars by using either the mouse or the keyboard.
COLORSCR works best when Windows is installed to use a color display, such
as an Enhanced Graphics Adapter connected to a color monitor, but you can
also run the program on a monochrome display.

You can use COLORSCR as a development tool in experimenting with color and
choosing attractive colors for your own Windows programs. But COLORSCR is
most interesting for what it doesn't do.

For instance, if you've done some Windows programming, you'll probably
assume that the Red, Green, and Blue labels on top of the scroll bars and
the color values at the bottom are displayed to the client area by using the
TextOut function.

However, COLORSCR does not use TextOut.

Also, you'll probably assume that the colored rectangle at the right half of
the client area is drawn by using the Rectangle or FillRect function.

No, COLORSCR doesn't use these functions either, or any of the other GDI
drawing functions.

When COLORSCR is made into an icon, the entire surface of the icon is
painted with the selected color. You might think that COLORSCR traps WM_SIZE
messages to check if it's being iconed and then paints the client area
differently if it is.

Wrong again. COLORSCR does not know when it's an icon and doesn't have
separate icon logic.

In fact, COLORSCR doesn't process WM_PAINT messages and doesn't directly
display anything to its client area. At some point the COLORSCR program does
obtain a handle to a display context, but only for the purpose of
determining the height of a character in the default system font.

In short, COLORSCR is one of the laziest Windows programs ever. How can it
afford to do so little? Because COLORSCR puts its children to work.


Child Windows

Child windows are separate windows that can be overlaid on the client area
of a tiled or pop-up window. They usually have their own window functions
separate from the parent window function. In their simplest form, child
windows can simply divide the client area of a window into smaller
rectangular windows, each with its own window function.

For instance, the Windows MS-DOS Executive uses three child windows to
divide its client area into sections for the drive icons, the current
subdirectory path, and the file list. Three different window functions
process messages from the three child windows. This simplifies painting and
mouse processing in the MS-DOS Executive because each child window has its
own client area coordinates.

One type of child window is called a child window control, which usually
sends messages back to its parent window (the tiled or pop-up window on
which the child control appears) based on mouse or keyboard input to the
control.

Windows contains several predefined child window controls, which take the
form of buttons, check boxes, edit boxes, list boxes, text strings, and
scroll bars. Child window controls are most often used in dialog boxes; in
such instances, the placement and size of the child window controls are
defined in a dialog box template contained in the program's resource script.
When you create a dialog box template, you can either calculate the position
and sizes manually or you can use the DIALOG program included with the
Windows Software Development Kit 1.03 (see "Latest Dialog Editor Speeds
Windows Applications Development," MSJ, Vol. 1 No. 1).

However, you can also use predefined child window controls on the surface of
a tiled or pop-up window's client area. You create each child window with a
CreateWindow call and adjust the position and size of the child windows with
calls to MoveWindow.

COLORSCR uses ten predefined child window controls. The color that appears
on the right half of the window is actually the background color of the
tiled window, as shown in Figure 2☼.

On the left half of the client area there is a "static" child window called
SS_WHITERECT (white rectangle) that blocks out half the client area (see
Figure 3☼). The three scroll bars are child window controls with the style
SBS_VERT (see Figure 4☼) placed on top of the SS_WHITERECT child. Six more
static child windows of style SS_CENTER (centered text) provide the labels
and the color values (see Figure 5☼).


Raising the Children

The COLORSCR.C source code, the COLORSCR.DEF module definition file, and the
COLORSCR make-file are shown in Figures 6, 7, and 8, respectively. If you
have the Microsoft C Compiler 4.0 and the Windows Software Development Kit
installed on your hard disk, you will be able to create COLORSCR.EXE by
executing:

  MAKE COLORSCR

COLORSCR creates its normal tiled window and the ten child windows within
the WinMain function through the use of the Windows CreateWindow call.

The first parameter to the CreateWindow call indicates the window class of
the window. The tiled window uses the window class called "ColorScr," which
is defined and registered during WinMain initialization; this is perfectly
normal. The child windows use classes called "static" (for the white
rectangle and the text) and "scrollbar," which are the predefined child
window classes. The window functions for these child windows are contained
within Windows.

For a tiled window, the second parameter to the CreateWindow call is the
text that appears in the window's caption bar. For the six child windows
that display text, this parameter is the text displayed in the child window.
For the rectangle and scroll bars, the text is ignored.

The window style parameter for all ten child windows includes the WS_CHILD
indentifier. The window style for the three scroll bars includes the
SBS_VERT identifier, indicating a vertical scroll bar. For the static child
windows, the SS_CENTER identifier indicates centered text, and the
SS_WHITERECT identifier indicates a white rectangle.

The fourth through seventh parameters to the CreateWindow call normally
indicate a position and size of the child window. The position is relative
to the upper left-hand corner of the client area of the parent window. These
values are all set to 0 initially because the placement and sizing depend on
the size of the client area, which is not yet known.

For child windows, the eighth parameter is the window handle of the parent,
which is simply the hWnd value returned from the first CreateWindow call.
The following parameter is a child ID that uniquely identifies the child
window. We'll use this child ID later on.

COLORSCR's window function (WndProc) resizes all ten child windows when it
receives a WM_SIZE message. The formulas are based upon xClient, the width
of the client area, yClient, the height of the client area, and yChar, the
height of a character.

Whenever you resize the COLORSCR window, the sizes of the child windows
change proportionally. This is particularly interesting for the scroll bars.
Depending on the dimensions of COLORSCR's window, the scroll bars can be
long and thin or short and stubby (see Figure 9☼). They may look a little
peculiar, but they work just fine.


Scroll Bar Messages

Child window controls that take the form of buttons, edit boxes, list boxes,
and scroll bars pass messages back to the window function of the parent
window. Buttons, edit boxes, and list boxes will post WM_COMMAND messages to
the parent window. Scroll bar controls, however, will use the messages
WM_VSCROLL (to scroll vertically) and WM_HSCROLL (for scrolling
horizontally) to indicate that the scroll bar has been clicked with the
mouse or the scroll bar thumb is being dragged.

The first step in using the scroll bars is to set a range for the scroll
bar, which is done in WinMain. Since the primary color values range from 0
to 255, the range is set accordingly:

  SetScrollRange (hChScrol[n],SB_CTL,0, 255, FALSE) ;

When the WndProc window function receives a WM_VSCROLL message, the high
word of the lParam parameter is the child window ID number, which is the
number specified in the ninth parameter to the CreateWindow call. For the
three scroll bars, we have conveniently set that ID to a 0, 1, and 2. That
tells WndProc which scroll bar is generating the message:

  n = GetWindowWord(HIWORD(lParam),GWW_ID) ;

Because the handles to the child windows had been saved in arrays when the
windows were created, WndProc can set the new value of the appropriate
scroll bar by using the SetScrollPos call:

  SetScrollPos(hChScrol[n],SB_CTL,color[n],TRUE) ;

and it can change the text of the child window at the bottom of the scroll
bar:

  SetWindowText(hChValue[n],itoa(color[n],szbuffer,10)) ;


Keyboard Interface

Scroll bar controls can also process keystrokes, but only if they have the
input focus. (Because only one window can receive keyboard input at any
time, the window that receives the keyboard input is referred to as the
window with the input focus.) The keyboard cursor keys are translated into
the scroll bar messages:

                         Scroll Bar
                         Message
  Cursor Key             wParam Value

  Home                   SB_TOP
  End                    SB_BOTTOM
  PgUp                   SB_PAGEUP
  PgDown                 SB_PAGEDOWN
  Left-arrow             SB_LINEUP
  Up-arrow               SB_LINEUP
  Right-arrow            SB_LINEDOWN
  Down-arrow             SB_LINEDOWN

In fact, the SB_TOP and SB_BOTTOM scroll messages are never generated when
using the mouse on the scroll bar, only when using the keyboard.


The Input Focus

If you want a scroll bar control to obtain the input focus when the scroll
bar is clicked with the mouse, you must include the WS_TABSTOP identifier in
the window class parameter of the CreateWindow call. When a scroll bar has
the input focus, a blinking gray block is displayed on the scroll bar
thumb.

However, to provide a full keyboard interface to the scroll bars, some more
work is necessary. First, the WndProc window function must specifically
give a scroll bar the input focus. It does this by processing the WM_FOCUS
message, which the parent tiled window receives when the parent window
obtains the input focus. WndProc simply sets the input focus to one of the
scroll bars:

  SetFocus(hChScrol[nFocus]);

But you also need some way to get from one scroll bar to another by using
the keyboard, preferably with the Tab key. This is more difficult, because
once a scroll bar has the input focus it processes all keystrokes. The
scroll bar only cares about the cursor keys and ignores the Tab key.


Window Subclassing

Adding a facility to COLORSCR in order to jump from one scroll bar to
another using the Tab key requires window subclassing.

The window function for the scroll bar controls is somewhere inside Windows,
but the address of this window function can be obtained by a call to
GetWindowLong using the GWL_WNDPROC identifier as a parameter. Moreover, you
can set a new window function for the scroll bars by calling SetWindowLong.

This facility allows you to hook into the existing scroll bar window
function, process some messages within your own program, and pass all the
other messages to the old window function.

The window function that does preliminary scroll bar message processing in
COLORSCR is called ScrollProc and is found toward the end of the COLORSCR.C
listing. Since ScrollProc is a function within COLORSCR that is called by
Microsoft Windows, it must be defined as FAR PASCAL and it must be listed in
the EXPORTS in the COLORSCR.DEF module definition file.

First, to ensure that ScrollProc accesses the proper data segment, you must
obtain a far address for the function with MakeProcInstance:

  lpfnScrollProc = MakeProcInstance((FARPROC)ScrollProc,hInstance);

For each of the three scroll bars, COLORSCR uses GetWindowLong to obtain and
save the address of the existing scroll bar window function:

  lpfnOldScr[n] =(FARPROC)GetWindowLong(hChScrol[n],GWL_WNDPROC) ;

Next, it sets the new scroll bar window function:

  SetWindowLong(hChScrol[n],GWL_WNDPROC,(LONG)lpfnScrolProc) ;

Now, the function ScrollProc gets first dibs on all messages that Windows
sends to the scroll bar window function for the three scroll bars in
COLORSCR (but not, of course, scroll bars in other programs). The ScrollProc
window function simply changes the input focus to the next, or previous,
scroll bar when it receives a Tab or Shift-Tab keystroke. It calls the old
scroll bar window function with CallWindowProc.


Use of Color

The Windows functions that require (or return) a color value use an unsigned
long (32-bit) integer, where the lowest three bytes specify red, green, and
blue values ranging from 0 through 255 (see Figure 10).

This results in a potential 224 (about 16 million) possible colors.

This unsigned long is often referred to in the Windows documentation as
rgbColor. The WINDOWS.H header file provides several macros for working with
rgbColor values. The RGB macro in WINDOWS.H takes three arguments
representing red, green, and blue values and then sticks the bytes together
in order to form an unsigned long:

  #define RGB(r,g,b)(((DWORD)(b << 8 | g) = << 8)| r)

Thus, the value

  RGB (255, 0, 255)

is really 0x00FF00FF, an rgbColor value for magenta. When all three
arguments are set to 0, the rgbColor is black; all three arguments set to
255 yields white.

C mavens will justifiably recoil upon seeing this RGB macro. The b, g, and r
identifiers within the definition should be surrounded by parentheses to
avoid errors when one of the parameters is an expression involving operators
of higher precedence than << and |. You should watch out when you use
expressions for the b, g, and r values.

The GetRValue, GetGValue, and GetBValue macros extract the unsigned
character primary color values from an unsigned long rgbColor value. These
macros are not used in COLORSCR, but they sometimes come in handy when you
are using Windows functions that will return rgbColor values to your
program.

The Windows display driver for the Enhanced Graphics Adapter attached to a
color display uses only three of the four color planes of the EGA board, so
only eight pure colors are possible. (In COLORSCR, the eight pure colors
result when all scroll bars are either at the top or bottom positions.)

Windows can display additional colors on the EGA by dithering, the creation
of a pixel pattern that combines pixels of different pure colors. You'll
find from experimenting on an EGA that the dithering pattern remains the
same for every increment of four units for the red, green, and blue values.
So, for an EGA, you have 218 or 262,144 dithered colors.

On a monochrome display──such as a CGA, an EGA connected to a monochrome
display, or a Hercules board──Windows uses 64 different patterns of white
and black. The density of white is approximately equal to

  red + green + blue
  ──────────────────
       255 * 3

where the red, green, and blue values range from 0 to 255.

Watch out for this──if you're developing using a color display, you might
see nothing wrong with putting a pure red object against a pure blue back-
ground. However, on a monochrome display the pure red and pure blue
"colors" appear to be the same.


Background Color

When COLORSCR is first executed, it sets up a window class structure, called
wndclass, that defines certain characteristics of its window. One member of
this structure is a handle to a brush that Windows uses to color the
background of the client area. This starts off as a solid black brush:

  wndclass.hbrBackground=CreateSolidBrush(0L);

CreateSolidBrush requires an rgbValue and returns a handle to a brush.
Although the hbrBackground member of the structure refers to a background,
in more technical terms it is the brush that Windows uses to erase the
window's client area in preparation for repainting by the program's window
function.

When you change the settings of COLORSCR's scroll bars, the program must
create a new brush and put the new brush handle in the window class
structure. Just as we were able to get and set the scroll bar window
function by using GetWindowLong and SetWindowLong, we can get and set the
handle to this brush by using the calls GetClassWord and SetClassWord.

First, it's necessary to delete the existing brush:

  DeleteObject(GetClassWord(hWnd,GCW_HBRBACKGROUND)) ;

Then the new brush can be created and the handle inserted in the window
class structure:

  SetClassWord (hWnd,GCW_HBRBACKGROUND,
     CreateSolidBrush(RGB(color[0],color[1],color[2]))) ;

Now, the next time Windows recolors the background of the window, Windows
will use this new brush. To force Windows to erase the background, we
invalidate the entire client area:

  InvalidateRect(hWnd, NULL, TRUE);

The TRUE (nonzero) value as the third parameter indicates that we want the
background erased before repainting.

InvalidateRect causes Windows to put a WM_PAINT message in the message queue
of the window function. Because WM_PAINT messages are low priority, this
message will not be processed immediately if you are still moving the scroll
bar with the mouse or the cursor keys. Alternatively, if you want the window
to be updated immediately after the color is changed, you could add the
statement

  UpdateWindow(hWnd) ;

after the InvalidateRect call. However, this slows down keyboard and mouse
processing.

COLORSCR's WndProc function doesn't process the WM_PAINT message and just
passes it on to DefWindowProc. Windows's default processing of WM_PAINT
messages simply involves calling BeginPaint and EndPaint to validate the
window. Because we specified in the InvalidateRect call that the background
should be erased first, the BeginPaint call causes Windows to generate the
message WM_ERASEBKGND (erase background). WndProc ignores this message also.
Windows processes it by erasing the background of the client area with the
brush specified in the window class.

Normally Windows would erase the entire client area using the window class
brush. This would cover the 10 child windows. Windows would then have to
send WM_PAINT messages to all 10 child windows so that they could repaint
themselves, which would be very annoying. However, you can avoid this
problem by using the WS_CLIPCHILDREN style value when you are first creating
the parent window with the CreateWindow call. The WS_CLIPCHILDREN style
prevents the parent window from painting over its children. If you then take
the WS_CLIPCHILDREN style out of CreateWindow, you'll see a big difference
in how COLORSCR works.

Like all GDI objects, the brushes created by a program that uses
CreateSolidBrush are not automatically deleted by Windows when the program
terminates. So far, we've been good about deleting each brush before
creating a new one, but when the program is about to terminate, there is
still one final brush in the window class that we should discard. Thus,
during the processing of the WM_DESTROY message, DeleteObject is called once
more:

  DeleteObject(GetClassWord(hWnd,GCW_HBRBACKGROUND));


Multiple Instances

Normally, most Windows programs will reuse the same window class when you
load multiple instances of the program. The window class is registered only
if the previous instance is NULL:

  if (!hPrevInstance){wndclass.style = CS_HREDRAW | CS_VREDRAW;

      .
      .
      .

  }

But COLORSCR must take a different approach because the background color is
specified in the window class. If all instances of COLORSCR used the same
window class, then each instance would alter and use the same background
color.

This problem is easy to solve: each instance simply registers its own window
class. COLORSCR doesn't check if it's the first instance or the twentieth.
Contrary to Windows documentation, a window class registered with the same
name as a previously registered window class does not replace the earlier
window class. A program simply has the option of using its own window class
or a previously registered window class. COLORSCR takes the former
approach.


COLORSCR as an Icon

One final mystery remains. When you make COLORSCR into an icon, the entire
surface of the icon appears as the selected color rather than just the right
half. Yet COLORSCR doesn't seem to have any separate icon logic.

Notice that COLORSCR specifies a NULL icon in the window class:

  wndclass.hIcon=NULL;

This indicates that COLORSCR is responsible for painting its icon. But why
does the entire icon appear as the selected color?

This is the simplest part of COLORSCR──Windows hides child windows when a
program is iconed. The colored background is then completely uncovered. So,
when the parent is sleeping, the children are neither seen nor heard.


Figure 6:  COLORSCR.C

/* COLORSCR.C -- Color Scroll (using child window controls) */

#include <windows.h>
#include <stdlib.h>

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

FARPROC lpfnOldScr[3] ;
HWND    hChScrol[3], hChLabel[3], hChValue[3], hChRect ;
short   color[3], nFocus = 0;

int PASCAL WinMain (hInstance, hPrevInstance, lpszCmdLine, nCmdShow)
     HANDLE   hInstance, hPrevInstance;
     LPSTR    lpszCmdLine;
     int      nCmdShow;
     {
     MSG      msg;
     HWND     hWnd ;
     WNDCLASS wndclass ;
     FARPROC  lpfnScrollProc ;
     short    n ;
     static   char *szColorLabel[] = { "Red", "Green", "Blue" } ;
     static   char szAppName[] = "ColorScr" ;-

     wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
     wndclass.lpfnWndProc   = WndProc ;
     wndclass.cbClsExtra    = 0 ;
     wndclass.cbWndExtra    = 0 ;
     wndclass.hInstance     = hInstance ;
     wndclass.hIcon         = NULL ;
     wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
     wndclass.hbrBackground = CreateSolidBrush (0L) ;
     wndclass.lpszMenuName  = NULL ;
     wndclass.lpszClassName = szAppName ;

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

     lpfnScrollProc = MakeProcInstance ((FARPROC) ScrollProc, hInstance) ;

     hWnd = CreateWindow (szAppName, " Color Scroll ",
                    WS_TILEDWINDOW | WS_CLIPCHILDREN,
                    0, 0, 0, 0, NULL, NULL, hInstance, NULL) ;

     hChRect = CreateWindow ("static", NULL,
                    WS_CHILD | WS_VISIBLE | SS_WHITERECT,
                    0, 0, 0, 0, hWnd, 9, hInstance, NULL) ;

     for (n = 0 ; n < 3 ; n++)
          {
          hChScrol[n] = CreateWindow ("scrollbar", NULL,
                        WS_CHILD | WS_VISIBLE | WS_TABSTOP | SBS_VERT,
                        0, 0, 0, 0, hWnd, n, hInstance, NULL) ;

          hChLabel[n] = CreateWindow ("static", szColorLabel[n],
                        WS_CHILD | WS_VISIBLE | SS_CENTER,
                        0, 0, 0, 0, hWnd, n + 3, hInstance, NULL) ;

          hChValue[n] = CreateWindow ("static", "0",
                        WS_CHILD | WS_VISIBLE | SS_CENTER,
                        0, 0, 0, 0, hWnd, n + 6, hInstance, NULL) ;

          lpfnOldScr[n] = (FARPROC) GetWindowLong (hChScrol[n],
                        GWL_WNDPROC) ;
          SetWindowLong (hChScrol[n], GWL_WNDPROC,(LONG) lpfnScrollProc) ;

          SetScrollRange (hChScrol[n], SB_CTL, 0, 255, FALSE) ;
          SetScrollPos   (hChScrol[n], SB_CTL, 0, FALSE) ;
          }

     ShowWindow (hWnd, nCmdShow) ;
     UpdateWindow (hWnd);

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

long FAR PASCAL WndProc (hWnd, iMessage, wParam, lParam)
     HWND       hWnd;
     unsigned   iMessage;
     WORD       wParam;
     LONG       lParam;
     {
     HDC        hDC ;
     TEXTMETRIC tm ;
     char       szbuffer[10] ;
     short      n, xClient, yClient, yChar ;

     switch (iMessage)
          {
          case WM_SIZE :
               xClient = LOWORD (lParam) ;
               yClient = HIWORD (lParam) ;

               hDC = GetDC (hWnd) ;
               GetTextMetrics (hDC, &tm) ;
               yChar = tm.tmHeight ;
               ReleaseDC (hWnd, hDC) ;

               MoveWindow (hChRect, 0, 0, xClient / 2, yClient, TRUE);

               for (n = 0 ; n < 3 ; n++)
                    {
                    MoveWindow (hChScrol[n],
                         (2 * n + 1) * xClient / 14, 2 * yChar,
                         xClient / 14, yClient - 4 * yChar, TRUE) ;

                    MoveWindow (hChLabel[n],
                         (4 * n + 1) * xClient / 28, yChar / 2,
                         xClient / 7, yChar, TRUE) ;

                    MoveWindow (hChValue[n],
                         (4 * n + 1) * xClient / 28,
                         yClient - 3 * yChar / 2,
                         xClient / 7, yChar, TRUE) ;
                    }
               SetFocus (hWnd) ;
               break ;

          case WM_SETFOCUS:
               SetFocus (hChScrol[nFocus]) ;
               break ;

          case WM_VSCROLL :
               n = GetWindowWord (HIWORD (lParam), GWW_ID) ;

               switch (wParam)
                    {
                    case SB_PAGEDOWN :
                         color[n] += 15 ;         /* fall through */
                    case SB_LINEDOWN :
                         color[n] = min (255, color[n] + 1) ;
                         break ;
                    case SB_PAGEUP :
                         color[n] ╤= 15 ;         /* fall through */
                    case SB_LINEUP :
                         color[n] = max (0, color[n] - 1) ;
                         break ;
                    case SB_TOP:
                         color[n] = 0 ;
                         break ;
                    case SB_BOTTOM :
                         color[n] = 255 ;
                         break ;
                    case SB_THUMBPOSITION :
                    case SB_THUMBTRACK :
                         color[n] = LOWORD (lParam) ;
                         break ;
                    default :
                         break ;
                    }
               SetScrollPos  (hChScrol[n], SB_CTL, color[n], TRUE) ;
               SetWindowText (hChValue[n], itoa (color[n], szbuffer, 10)) ;

               DeleteObject (GetClassWord (hWnd, GCW_HBRBACKGROUND)) ;
               SetClassWord (hWnd, GCW_HBRBACKGROUND, CreateSolidBrush
                            (RGB (color[0], color[1], color[2]))) ;

               InvalidateRect (hWnd, NULL, TRUE) ;
               break ;

          case WM_DESTROY:
               DeleteObject (GetClassWord (hWnd, GCW_HBRBACKGROUND)) ;
               PostQuitMessage (0) ;
               break ;

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

long FAR PASCAL ScrollProc (hWnd, iMessage, wParam, lParam)
     HWND      hWnd ;
     unsigned  iMessage ;
     WORD      wParam ;
     LONG      lParam ;
     {
     short     n = GetWindowWord (hWnd, GWW_ID) ;

     switch (iMessage)
          {
          case WM_KEYDOWN:
               if (wParam == VK_TAB)
                    SetFocus (hChScrol[(n +
                         (GetKeyState (VK_SHIFT) < 0 ? 2 : 1)) % 3]) ;
               break ;

          case WM_SETFOCUS:
               nFocus = n ;
               break ;
          }
     return CallWindowProc (lpfnOldScr[n], hWnd, iMessage, wParam,
                           lParam) ;
     }


Figure 7:  COLORSCR.DEF

NAME            ColorScr
STUB            'WINSTUB.EXE'
CODE            MOVABLE
DATA            MOVABLE MULTIPLE
HEAPSIZE        1024
STACKSIZA       4096
EXPORTS         WndProc
                ScrollProc


Figure 8:  Make-file for COLORSCR.EXE

colorscr.obj  :  colorscr.c
    cl -c -d -Gsw -Os -W2 -Zpd colorscr.c

colorscr.exe  :  colorscr.obj colorscr.def
    link4 colorscr, /align:16, /map /line, slibw, colorscr
    mapsym colorscr


Figure 10:  An unsigned long (32-bit) integer where the lowest three bytes
            specify red, green, and blue values.

╔═╤═╤═╤═╤═╤═╤═╤══╤══╤═╤═╤═╤═╤═╤═╤══╤══╤═╤═╤═╤═╤═╤═╤══╤══╤═╤═╤═╤═╤═╤═╤══╗
║0│ │ │ │ │ │ │7 │ 8│ │ │ │ │ │ │15│16│ │ │ │ │ │ │23│24│ │ │ │ │ │ │31║
╚═╧═╧═╧═╧═╧═╧═╧══╧══╧═╧═╧═╧═╧═╧═╧══╧══╧═╧═╧═╧═╧═╧═╧══╧══╧═╧═╧═╧═╧═╧═╧══╝
  •           •     •           •     •           •     •           •
    •       •         •       •         •       •         •       •
      •   •             •   •             •   •             •   •
        •                 •                 •                 •
     ╔═════╗           ╔═════╗           ╔═════╗           ╔═════╗
     ║  0  ║           ║BLUE ║           ║GREEN║           ║ RED ║
     ╚═════╝           ╚═════╝           ╚═════╝           ╚═════╝

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

Ask Dr. Bob

The Driverless Printer

Dear Dr. Bob,
Could you tell me if it is possible in a Windows application to send text
and control characters to a printer for which there is no driver?──Wondering

Dear Wondering,
If you're using a commercial Windows app, you're in trouble; you can't print
without a driver. If you're writing your own app, you can't set up a DC
(display context) and use standard GDI functions like Rectangle(), BitBlt(),
and TextOut(). But you can use DOS file I/O to open, write, and close the
DOS PRN: device. If you do this, it's up to you to understand the language
your printer talks──its escape codes and special control characters. This,
of course, ties your program to that specific printer and loses the device
independence normally provided by Windows.

Math Library Functions

Dear Dr. Bob,
How can I use Microsoft C math library functions in my assembler
program?──Math Maven

Dear Maven,
Microsoft C, Version 4.0, has several floating-point options: in-line 8087
code, calls to an 8087 emulator, or calls to an alternative library. Before
we tell you how to go about this task, we'll make a few disclaimers: C
floating-point functions are meant for use by the compiler and handle
arguments differently from standard functions, they're undocumented, and the
fact that they work with Version 4.0 is no guarantee that they'll work with
future versions.

To figure out how the compiler uses floating point, we used the /Fc compiler
switch to examine the compiler's assembly language output and /FPa or /FPc
so that the compiler would produce calls to the floating-point functions
instead of 8087 code.

First, you have to initialize the library. If possible, let the regular C
startup code handle this. If this won't work for you, look at the __CINIT
function found in CRT0DAT.ASM from the C startup code disk. You should be
able to pull the floating-point initialization code from this file.

To find the names of the floating-point functions and the arguments that
each function requires, write small C programs that demonstrate the
floating-point functions you need and compile them with the /Fc switch.
Study the assembler code and model yours after it. There are library
functions for putting arguments onto a stack, doing math functions, and
popping them off. Figure 1 is a sample C program and the assembler code it
generates.

You can explore other areas of compiler code this way, too. Another one you
might want to look at is long int arithmetic.

You don't have to worry about formatting your numbers because MASM's DD,
DQ, and DT directives use the IEEE formats for the real number constants
that the library expects.

The floating-point functions documented in the C library manual, such as
sin() and exp(), use standard C calling conventions, but before you use
them, look at how the compiler converts all operands to double precision.

Using COM2 Without COM1

Dear Dr. Bob,
I just recently ran into a problem with QuickBASIC. I opened COM2 for I/O
and kept getting a bad filename error. The problem turned out to be that I
did not have anything addressed as COM1 in my system. Everything worked fine
after I installed a modem or serial card addressed as COM1.

All that Microsoft product support would say is that you should not use
COM2 if you do not have a COM1 already installed. I have designed IBM-
compatible serial interfaces, so I know how the hardware works. To me they
are two completely separate entities with two unique I/O addresses. Why am I
having this problem, and why doesn't Microsoft consider it a problem? They
told me they would not try to fix it.──Constrained

Dear Constrained,
We're on your side. The hardware for COM2 can work without COM1. It seems
that QuickBASIC looks in the BIOS data area at RS232_BASE. This area stores
the addresses of up to four serial communications ports. When you start your
system, the BIOS initialization code checks for serial ports, puts in
addresses of any it finds, and sets the rest of the locations to zero. When
both COM1 and COM2 are present, the address of COM1 (3F8h), is in the first
word of RS232_BASE and the address of COM2 (2F8h) is in the second word. But
when COM2 is the only port installed, its address, 2F8h, is in the first
word, and the second word is zero. When QuickBASIC is loaded, it looks at
RS232_BASE to see what serial ports are present, but it only recognizes COM2
when its address is in the second word.

Here's a way to work around this. The program shown in Figure 2 will patch
the second word of RS232_BASE with the address of COM2. You must run it
before you run your QuickBASIC application that uses COM2. If you put this
code at the beginning of your program, it will be executed too late to do
any good because QuickBASIC checks for serial ports before it starts running
your first BASIC statement.

C Queries and Suggestions

Dear Dr. Bob,
I'd like to offer some suggestions and ask some questions regarding Version
4.0 of Microsoft C. In CodeView, in order to trace through a C source
program, one must link with the object module. This is fine, but it would
also be nice to have CodeView trace through the C source programs that have
modules within a library. Is this possible?

A related question: does the EXEPACK linker option remove the extra
debugging information from specified libraries? If so, this would at least
eliminate the need to compile a file twice──once with the CodeView options
and once without. This way I would compile everything with the CodeView
options before creating the libraries. Then if I needed to debug a
particular program from the library, I could link in the appropriate object
file.

While using the watch option in CodeView, I tried to enter the following
expression:

  strptr[ (idx==0)
    ? 0
    : idx-1 ],s

where strptr was declared as char *strptr[] and idx was an int. CodeView did
not allow this and returned a message that ']' was missing. Am I missing
something?

Also, I often find myself setting several debugging options in CodeView
(tracepoints, watchpoints, breakpoints, etc.), finding a bug, exiting
CodeView, editing the source, compiling, linking, and then starting CodeView
and having to reset the options I had before. It would be nice to have a
save/reset option for CodeView that will save and later reset the
tracepoints, watchpoints, and breakpoints. Here's what I suggest. I usually
enter CodeView with the same options. There could be an environment variable
(let's say CV) that would look something like "set CV= -w -f" so that these
options will now be the default for entering CodeView.

One more question, about malloc, free, and _memavl. If I have a program
like the one shown in Figure 3, the last _memavl () does not reflect the
amount of space now available from the prior free () call. This was done by
using the small compiler model with the two options /Zi and /Od.

Finally, I would like to share some tips about the MAKE facility. I now set
up MAKE files that look like Figure 4.

There might be standard include files in subdirectories \include,\gf\s,
\gf\m, and gf\l, and standard libraries in subdirectories \lib, \gf\s,
\gf\m, and \gf\l.──C Buff

Dear C Buff,
Yes, you are missing something: the CodeView manual, Section 4.4, which says
CodeView supports a subset of C operators. The conditional operator ?: is
not in the subset. That's the reason for the error.

One thing we found interesting about the complex operations CodeView does
support is that it reevaluates the entire expression each time it displays
the watch window. This means that if you're watching an array such as
strptr[idx], CodeView may not only watch a fixed location; when idx changes,
you'll be watching a different element of the array. This is not what we
expected, but we like what CodeView does.

Your idea for the CodeView save/reset options is a good one. We'll pass it
on. We, too, get tired of turning case sense off each time.

Your malloc(), free(), and _memavl() question is interesting. By marking the
block as no longer in use, free() releases the memory. But as you
discovered, this free space does not show up immediately in _memavl().
According to the C manual, _memavl() returns "approximately" how much memory
is available. Because it doesn't add up the free blocks until they're
needed, what it in fact returns is the largest size you can be sure you can
allocate. You may, however, be able to allocate a much larger block, because
if malloc() gets a request for a block larger than _memavl(), it goes down
the list of free blocks joining adjacent ones, trying to make a large enough
block. There is no way to force garbage collection analogous to the BASIC
FRE($) function.

Figure 5 is a simple function, _maxavl(), that returns the size of the
largest possible block. We suggest using it instead of _memavl().


Figure 1:  A sample C program and the assembler code it generates.

float a,b,c,d,e;
main()
 {a = (b+c)*(d+e) ; }

lea   bx,WORD PTR _b       ; put address of b into the bx register
call   __flds              ; push b onto fp stack
lea   bx,WORD PTR _c
call   __fadds             ; add c to top of stack
lea   bx,WORD PTR _d
call   __flds              ; push d onto fp stack
lea   bx,WORD PTR _e
call   __fadds             ; add e to top of stack
call   __fmul              ; multiply top two elements of stack
                           ; product replaces previous two
values on stack
lea   bx,WORD PTR _a
call   __fstsp             ; pop stack putting result in variable a


Figure 2:  This program demonstrates how to patch the second word of
           RS232_BASE with COM2. You must run it before you run your
           QuickBASIC application that uses COM2.

def seg _ &h0040
if (peek(0) + 256*peek (1))= &h02f8 then _
poke 2,&hf8: poke 3,&h02:_
print "COM2 patch made"else_
print "COM2 patch NOT made"


Figure 3:  The last _memavl () does not reflect the amount of space now
           available from the prior free () call. This was done by using the
           small compiler model with the two options /Zi and /Od.

printf ("starting available memory = %u\n",_memavl () );
if (NULL == (ptr=malloc ((unsigned) 5000)) )
    printf("Out of memory.");
printf("Available memory after malloc() = %u\n",_memavl () );
free(ptr);
printf("Available memory after free() = %u\n",_memavl () );


Figure 4:  MAKE file template

# MAKE file template -  ###############################
# Assume the application program is called DOIT and the source
# directory is \CUSTOMER\SOURCE. The target directory for all
# obj and exe files is \CUSTOMER\S (which can easily be changed
# to \CUSTOMER\M or CUSTOMER\L by setting M=specific model
# type. The switches are:
#
#         M= model type S, M, or L
#
#          O= Compiler options (set for CodeView)
#
#          L= LINT_ARGS switch (currently set on)
#            To set this off, use L=
#
#          I= Switch to set environment variables
#
#            To set this off, use I=null
#
#          S= Switch to force a compile
#
#            To force a compile, use S=null
#
######################################################
M=S
O=/Zi/Od
L=/DLINT_ARGS
D=\CUSTOMER\$(M)
I=null
S=\include\stdio.h

COMP=\bin\msc $*,$@ /A$(M) $(L) $(0);
LINK=\bin\link $(D)\doit.obj,$(D)\doit.exe,$(D)\doit.map,/M/E
LINKCV=$(LINK)/CO

# Make sure the include and library environ vars are correctly set
$(I):  \bin\inclib.bat
       \bin\inclib $(M)

$(D)\doit.obj:       doit.c $(S)
    $(COMP)

$(D)\doitcv.exe:      $(D)\doit.obj
    $(LINKCV)

$(D)\doit.exe:        $(D)\doitcv.exe
    $(LINK)
    dir $(D)\doit*.*
# End of MAKE file#####################################

The \BIN\INCLIB.BAT file looks something like this:

if "" == "%1" goto :default
set INCLUDE=\include;\gf\%1
set LIB=\gf\%1;\lib
goto :end
:default
set INCLUDE=\include;\gf\s
set LIB=\gf\s;\lib
goto :end
:end


Figure 5:  A simple function, _maxavl (), that returns the size of the
           largest block that malloc () can allocate.

/*
*  _maxavl() - Return the size of the largest block that
*              malloc() can allocate
*/
int _maxavl() {
  register unsigned size;
  register unsigned incr;
  char   *p;

/*
* Do binary search for largest block malloc() can return.
* Start search with a size of 32K. Keep going until the
* increment value is reduced to 0, with size homing in on
* its largest possible value.
*/
 for (size=incr=0x8000; incr; ) {
   incr >>= 1;   /* cut increment in half */
   if ( (p=malloc(size))==NULL )
     size -= incr; /* malloc() failed, so decrease size */
   else {
     free (p); /* malloc() succeeded, so free block */
     size += incr; /* and increase size */
      }
   }
/* If last malloc() failed, return size-1 instead of size: */
  return( (==NULL) ? size-1 : size );
   }


════════════════════════════════════════════════════════════════════════════


Vol. 2 No. 3 Table of Contents


PLEXUS Introduces Windows-based Tools for Building Image Databases

The Plexus Extended Data Processing (XDP) system integrates a UNIX-based
file server with Microsoft Windows-based workstation software. XDP provides
a high-level DBMS environment for creating sophisticated, graphics-oriented
database applications.


Porting MS-DOS Assembly Language Programs to the OS/2 Environment

OS/2's protected mode precludes programs and routines written in assembly
language from taking certain liberties allowed under real mode. This article
offers a guide that can minimize the conversion effort necessary to insure
programs will work under both MS-DOS and MS OS/2.


Microsoft Windows 2.0: Enhancements Offer Developers More Control

Windows 2.0 gives developers over 60 new functions, a new user interface
with overlapping windows, and a substantially improved development
environment. Our author uses some of the new facilities to create TILER,
which supplies one feature missing in Windows 2.0──tiled windows.


Keeping Up With The Real World: Speedy Serial I/O Processing

Serial I/O processing under MS-DOS benefits dramatically when the BIOS
serial communications routines available through INT 14H are replaced.
Strategies for handling the necessary interrupts,  XON/XOFF processing,
and reliable communications up to 38,400 baud are suggested.


BLOWUP: A Windows Utility for Viewing and Manipulating Bitmaps

Using bitmaps in the Windows environment is not trivial. The programmer
must understand memory display contexts, how bitmaps are scaled, and how
to transfer bitmaps via the clipboard. BLOWUP, a useful Windows tool, can
capture and manipulate any part of the windows display.


Increase the Performance of Your Programs  with a Math Coprocessor

The performance of calculations, especially those that make extensive
use of floating-point operations, increases significantly when the CPU is
supplemented with an 8087/80287/80387 coprocessor. Learn how your programs
can take advantage of these coprocessors.


TIFF: An Emerging Standard for Exchanging Digitized Graphics Images

The growing popularity of desktop publishing has created a need for the
different vendors' software to be able to exchange digitized graphics
images. The Tag Image File Format (TIFF) is gaining momentum as the
possible industry standard for scanner-created digital images.


Ask Dr. Bob


EDITOR'S NOTE

The shipment of the Microsoft OS/2 Software Development Toolkit allows
developers to begin programming for the new operating system. In this
issue, and in those to come, MSJ focuses on specific OS/2 programming
topics. Article 2, "Porting MS-DOS(R) Assembly Language Programs to the
Microsoft OS/2 Environment", offers a step by step guide to converting
assembler code so that it will work under OS/2.

The importance of the Windows environment continues to grow. More and
more companies, such as PLEXUS (spotlighted in article 1) rely on the
Windows environment for their workstation software environment. Our article
on the about-to-be-released Windows 2.0 highlights why the new and enhanced
features are of particular interest to developers. And, Charles Petzold
continues his Windows programming series with Blowup, another small but
interesting Windows tool.

One of the dangers a publication encounters when working with prerelease
software is the inevitable changes products go through before they
are released. We do our best to ensure the sample code works when the
magazine is printed. You can find the most current versions of all our
source code, as well as full code listings that we don't have room to
print, on the DIAL system. We will soon be posting the complete code
listings on other popular bulletin boards. Check in upcoming issues for
more information.

Some readers of our last issue commented that our discussion of the OS/2
DOS Environment implied that bypassing MS-DOS, writing directly to the
hardware, and playing games with segment reisters should be punishable
by death. Not at all! We recognize that MS-DOS has certain limitations.
The article merely meant that OS/2 eliminates a good deal of those
limitations and that it will no longer be necessary to bypass the
operating system. If you have any comments, suggestions, or critiques,
please write us and let us know.──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

DIANA E. PERKEL
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

BETSY KAUFER
Administrative Assistant

Copyright(C) 1987 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 XENIX are
registered trademarks of Microsoft Corporation. IBM is a registered
trademard of International Busness Machines Corporation. PageMaker is a
registered trademark of Aldus Corporation. dBase is a registered trademark
of Ashton-Tate. UNIX is a registered trademark of AT&T. Lotus and 1-2-3
are registered trademarks of Lotus Development Corporation. Intel is a
registered trademark of Intel Corporation. Macintosh is a trademark of
of Apple Computer, Inc. Paintbrush is a registered trademark of ZSoft
Corporation. Motorola is a registered trademark of Motorola, Inc.

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

Plexus Introduces Windows-based Tools for Building Image Databases

Kevin Strehlo☼

When personal computers first began to make inroads into the business
environment, database managers such as dBASE(R) were fine for what was then
a text-only world. They gave people who weren't crack programmers the
ability to build custom applications with reasonable, if limited,
interfaces for the management and manipulation of textual data. But
Microsoft(R) Windows, scanners, and laser printers have taken us from that
text-only world into one in which we can generate and manipulate images
intermingled with text. OCR devices even allow us to translate images of
documents into computer graphics and text. Where are the visual database
managers with which we can easily build custom applications in order to
manage huge volumes of this image data?

Plexus(R) thinks it has the answer: its Extended Development Environment
(XDE), which couples Windows with a fourth-generation database language.
Still in its beta-test stage, XDE is designed to run on Plexus's Extended
Data Processing (XDP) System, an elaborate configuration of LAN hardware
and software for handling an Informix-based graphics database. XDE serves
as a tool for creating custom applications that manage graphics data with
a sophisticated visual interface.

The real beauty of XDE is its simplicity for the programmer. Writing a
picture perfect (or rather, a perfect picture) database with it requires
no knowledge of the event-driven mysteries of Windows, the intricacies of
C, or the problem of finding a bitmap in an optical disk stack. It lets
the programmer create a Windows database application without worry about
such things as WinMain functions or WINDOWS.H files.


Hardware and Software

When it comes to system requirements, XDE and standard database managers
part ways, however. XDE will run only on a complete XDP system, which
means a substantial investment in hardware and software. Complete systems
range from $75,000 to more than $1 million.

At the center of XDP is a 68020-based Plexus supermicro that acts as the
database server for an Ethernet(TM) LAN. Hooked up to the LAN are a number
of AT-compatible workstations running Windows on high-resolution, 1,664-
by 1,200-pixel displays. The cabinets of the system can provide as much as
6.7Gb of magnetic disk storage and 8Gb of optical disk storage. If more
is needed, a jukebox of optical laser disk drives can be attached to
handle as much as 280Gb with an average access time of under 10 seconds.
If you think that's an incredible amount of disk capacity, you're right,
but let's face it──at 300Kb or more per compressed image, a database that
replaces a roomful of microfiche or shelves full of technical manuals is
going to require an incredible amount of storage.

To record all of that visual information, you can attach microfiche and
paper scanners to the workstations. To translate captured document images
into ASCII text suitable for manipulation, a powerful OCR device handles a
variety of fonts at a minimum speed of 60 characters per second with
greater than 99 percent accuracy.

For handling large images with dispatch, each AT workstation contains a
board with enough memory to hold and manipulate an uncompressed 400-dpi
image and a graphics coprocessor tailored for pixel manipulation. Having
an entire 1.5Mb bitmap available in memory speeds up image manipulation.
The coprocessor board also minimizes transmission times among the database
server, the workstations, and the OCR subsystem by quickly compressing
each image before transferring it across the Ethernet.

The software at the heart of XDP is as much a mixed bag as the database
that it supports. On the AT-compatible workstations, the system runs MS-
DOS(R) 3.1 because of its networking hooks. The Plexus DataServer runs an
enhanced version of the Informix relational database under UNIX(R) System V.
Meanwhile, communication between server and workstations is handled
according to TCP/IP protocols on an Excelan implementation of Ethernet.
The Informix database running on the server has been extended with data
types for handling images. A similarly extended version of the SQL
standard for database queries (which is dubbed XESQL) allows mixed-mode
(text and graphics) transactions. The custom applications created by XDE
and running on the AT workstations generate the XESQL queries.


How XDE Works

The Plexus XDE consists of two components: an application called UI-Build
(UI stands for user interface) and an extended version of the Informix 4GL
language. UI-Build does for the overall user interface what the Microsoft
Windows Dialog Editor does for dialog boxes (see "Latest Dialog Editor
Speeds Windows Application Development," MSJ, Vol. 1 No. 1). It allows a
sophisticated end-user or relatively inexperienced programmer to define
the Windows front-end of the application interactively. After defining the
elements of the Windows interface, the application developer uses the 4GL
language to write the code behind the drop-down menus, buttons, image
fields, and text fill-in boxes.

The 4GL language is not strictly declarative. It is a high-level
programming language with standard procedural capabilities, including
IF/THEN/ELSE, CASE, FUNCTION, and VARIABLE.

Upon compilation, XDE links that simple 4GL code to the appropriate
elements of the Windows interface. XDE handles all of the tasks associated
with building a full-blown Windows application, which are by no means
trivial.

"We try to cover up all the difficulties that Windows can bring in and
present to the user," says Maurizio Gianola, software tools manager for
Plexus. The visual and event-driven nature of Windows throws the unwary
programmer up against new and strange concepts, such as client area and
messages, he points out. "You have to know so many things to write a
Windows application. What is the window handler? What are all those
messages coming in? What do you do with those messages? Which ones go to
the menu handler? Which ones go to the system?"

Despite its high-level nature, the 4GL language offers rich functionality,
including a number of graphics-oriented extensions to SQL, such as the
capacity to adjust scanner parameters and a "magnifying glass" function
that automatically rescales the on-screen presentation of an image from
100 dpi to 300 dpi. And to accommodate needs that the language's
developers didn't foresee or thought too specialized to implement,
specialized routines can be written in C and dropped into the 4GL
program.


Windows and Houses

To illustrate the general form and function of XDE, Gianola whipped up a
sample real estate application: the user specifies a range of house prices
and asks to see some listings. The application in turn queries a database
on the Plexus server and responds by allowing the user to view matching
listings one at a time. The address, the number of bedrooms, and,
optionally, a scanned image of each house are displayed (see Figure 1☼).

When you invoke UI-Build to begin creating a new application, all you see
above the Windows icon area are the two free-floating menu bars. Figure 2☼
shows the UI-Build environment a step into the process of creating the
sample real estate application. Gianola has already named the title bar
"Real Estate Database." Had he chosen to do so, he could have specified in
the main UI-Build menu that the system box appear in the title bar, that
the window be sizable and movable, and so on. Wishing to keep the
application simple, Gianola merely chooses Menu_Bar from the Palette, adds
the Options menu, and names the only command in that menu Show Picture.

In Figure 3☼, Gianola has filled in the UI-Build dialog box that comes up
when Menu is chosen from the Palette; Figure 4☼ shows the result after he
has added two menu choices.

Gianola next designs the forms that will pop up in the course of running
the application. When users ask to see listings, they must use the form
shown in Figure 5☼ to specify the minimum and maximum prices they wish to
consider. The form is a Group_Box; it is labeled Price Range on screen and
priceRange internally. Within this box Gianola has defined an Entry area,
which he can stretch to the desired size by means of handles. He assigns
Min as an ID for whatever text a user will input; this lets him reference
it easily from within his 4GL program.

Figure 6☼ shows the form that will appear when the user selects Show
Listing from the Commands menu. Gianola has labeled the rectangle
Available Houses, and because it is currently selected, it can be resized
by stretching its handles. Note that he has defined two buttons in the
lower right corner──Next and Done. To define a large area in which the
scanned images of the houses can be displayed, he chose Image from the
Palette. The figure shows Gianola in the process of defining fields in
which to display the number of rooms, price, and address of the current
house listing.

Had he desired, Gianola could have included other means of interacting
with the user. For example, he could have let the user specify the desired
number of rooms by clicking on a toggle to enable a table of numbers drawn
from the database and then vertically scrolling to his or her choice──much
as point sizes are selected in the Type Specifications dialog box of
PageMaker(R).


Writing the Code

After establishing the user interface with UI-Build, the programmer then
uses a text editor to write the 4GL code that goes with it. Figure 7 shows
the code that takes care of the preliminaries. The database is named
"houses," and the predefined table in that database called "house" is used
to define the records that will appear in windows on the AT's screen.
Three variables are declared, one of which will serve as a flag.

The first lines of the MAIN section name the help file and initialize the
showpicture flag. Next, the program diplays the menu defined in Figures 2,
3, and 4☼ and names mainMenuHdl as the routine that will deal with user
selections of items in that menu. The first instance of the modeless form
availHouses is brought up as well.

Figure 8 shows the menu-handling routine for the main menu. When the user
selects Show Picture from the Options menu, that choice (the internal
IDfor it is showHousePicture) is checked by calling the XDE standard Check
function and the showpicture flag is changed if necessary. If the user
chooses Show Listing from Commands, the form defined in Figure 5☼ pops up.
The routine creates a cursor to allow user input and selects all records
in the database that fall within the range specified by the user. It then
calls the function showData, which we will discuss later. Finally, since
the user is now looking at listings, the Show Listing choice in the menu
is disable, and the End Listings choice changes from gray to black to
indicate it is enabled.

Figure 9 shows the form handling routine availHouseHdl, which uses the 4GL
construct called Before Input to initialize the blank form. A message
explaining how to view the picture appears along with the Windows note
icon and an OK box for the user to check. If the user clicks in the Next
button, the showData function is called, while a click in Print calls the
printHouseInfo function. The line ON photo initiates a rather nifty
feature of XDE: if the user clicks on the empty field in which houses are
displayed in an effort to bring up an image, a message tells him or her to
make the appropriate menu selection. Finally, when the user is done, the
application asks if the current listing should be printed before it
disappears.

The routine in Figure 10 performs a simple check to ensure that the user
has filled in minimum and maximum values that make sense. If this is not
the case, a warning message will appear.

Finally, the two functions called in the earlier routines appear in
Figure 11. The first displays the proper image and values within their
respective fields in the current instance (availH) of the form
availHouses. The second prints the listing at the user's request.


Unique Approach

Several unique aspects of the Plexus XDE should appeal to anyone who wants
to build Windows applications quickly. For one thing, the system
automatically sets up everything required by Microsoft Windows. When the
application is compiled, it produces not only a resource file that encodes
everything done with UI-Build, but also a WINDOW.H file that includes the
predefined XDE functions. For example, the functions available for
creating rich menus include gray and ungray──that is, disable and enable
menu choices──and check and uncheck.

Gianola seems especially proud of another unique feature of XDE. "Usually,
every time you change something in the user interface, you have to
recompile a Windows application, but we tried to avoid that." Essentially,
Plexus bound the user interface to the program at link time rather than
during compilation. "In other words, when you go back and touch up your
user interface after you've gotten the application running," Gianola says,
"you will need to link but not to recompile." This feature is actually
almost a necessity in XDE since compilation involves two levels of
translation──first from 4GL to C, and then from C to object code.
Recompilation would thus be a more cumbersome task than usual.


Applications

The Plexus XDP is already in use by some firms even though XDE is not yet
available. Such was their need for a visual approach to managing large
volumes of image data that these companies (with the help of Plexus) wrote
ordinary Windows programs in C to run the system. The applications created
thus far are fairly complex and demonstrate that there will often be a
need to enhance an application roughed out with XDE by embedding at least
a modicum of C code. For example, one customer is using a five-workstation
system for producing CD ROM discs to replace microfiche. One feature of
this system enables the user to exclude parts of a page from being
recorded by the scanner. Such refinements are beyond the capabilities of
XDE, although Plexus plans to enhance the environment in future releases.

Another Plexus customer, the regional phone company US West, uses an XDP
system to manage the placement of ads in its yellow pages publishing
operations. When a customer calls, ad sales representatives can quickly
find the order through a text-oriented database search that's facilitated
by an easy-to-use Windows interface. They can view an image of the
advertisement on-screen.

Desktop publishing ventures involving a large number of technical diagrams
or other images would seem to be another natural application for the
system. Since an application built with XDE resides in the Windows
environment, retrieved images could be transferred directly into a Windows
desktop publishing program──and rich text could be moved just as easily.

Altogether, XDP seems a big improvement over comma-delimited ASCII export
files and pasted photocopies.


Figure 7:  DATABASE houses

DEFINE
{The record prospect is defined like the database table house }
prospect RECORD LIKE house.*
Min, Max INTEGER
showpicture SMALLINT

MAIN
    HELP FILE "houses.hlp"                    { F1 is request for help }
    LET showpicture = FALSE
    SHOW MENU mainMenu USING mainMenuHdl
    OPEN FORM availHouses USING availHousesHdl AS availH  { modeless form }
END MAIN


Figure 8:  mainMenuHdl

MENUHANDLER mainMenuHdl
    ON SHOW MENU
        DISABLE IN MENU endListing
    MENU
        ON showHousePicture
            IF showhouse = FALSE THEN
                CHECK IN MENU showHousePicture
                LET showhouse = TRUE
            ELSE
                UNCHECK IN MENU showHousePicture
                LET showhouse = FALSE
            END IF
        ON showListing
            OPEN FORM priceRange USING priceRangeHdl { modal form }
            DECLARE listingCursor CURSOR FOR
                SELECT * INTO prospect.* FROM house
                    WHERE house.price <= Max AND
                          house.price > Min
            OPEN listingCursor
            CALL showData()
            DISABLE IN MENU showListing
            ENABLE IN MENU endListing
        ON endListing
            ENABLE IN MENU showListing
            DISABLE IN MENU endListing
            CLOSE listingCursor
END MENUHANDLER


Figure 9:  availHouseHdl

FORMHANDLER availHouseHdl()
    DEFINE
        print SMALLINT

    ON OPEN FORM
        DISPLAY   0, "", NULL, 0 TO FIELD price, address, photo, numOfRooms
    INPUT
        ON Done
            CLOSE FORM
        ON Next
            CALL showData()
        ON PrintAllInfo
            CALL printHouseInfo()
        ON photo
            MESSAGE
                "To see the picture of the house check the menu selection"
                NOTE_ICON OK_BUTTON
    END INPUT
    ON CLOSE FORM
        MESSAGE "Do you want to print this listing?"
                NOTE_ICON YES_NO_BUTTONS RETURNING print
        IF print = YES THEN
            CALL printHouseInfo(prospect.code)
        END IF
END FORMHANDLER


Figure 10:  priceRangeHdl

FORMHANDLER priceRangeHdl()
    INPUT BY NAME Min, Max
        ON idOk
            IF Min <= Max THEN
                CLOSE FORM
            ELSE
                MESSAGE "Incorrect price range!!" WARNING_ICON OK_BUTTON
            END IF
    END INPUT
END FORMHANDLER


Figure 11:  Other Functions

FUNCTION showData()
    FETCH listingCursor
    DISPLAY BY NAME prospect.* TO FORM availH
    IF showpicture = TRUE THEN
        DISPLAY prospect.photo TO FORM availH FIELD photo
    END IF
END FUNCTION

FUNCTION printHouseInfo()
    PRINT prospect.price, prospect.address, prospect numOfRooms
    IF showpicture = TRUE THEN
        PRINT prospect.photo
    END IF
END FUNCTION

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

Porting MS-DOS Assembly Language Programs to the OS/2 Environment

Ray Duncan☼

OS/2 the protected-mode operating system for the 80286 microprocessor,
offers the applications programmer a rich variety of system services and
is comparable in many ways to the powerful real-time operating systems
used on minicomputers and superminicomputers. It supports preemptive
multitasking, multiple screen groups, a broad spectrum of inter-process
communication facilities, and virtual memory management. It includes high-
performance device drivers for the video display, keyboard, mouse, and
serial port. It even boasts a true print spooler. In short, OS/2 provides
a sturdy foundation for an entirely new generation of business
applications with vastly expanded features and capabilities.

Yet at first programmers will be more concerned with porting their
existing MS-DOS tools and applications to OS/2's protected-mode
environment than with attempting to exploit the capabilities of the new
system. Those who write code in high-level languages, such as Microsoft(R)
C, can simply relink and run the programs when the appropriate protected-
mode libraries are available. But those of us who still labor in the
vineyards of bit-banging──writing primarily in assembly language or at
least making extensive use of assembly language subroutines to obtain
optimum performance──face a much more extensive revision process.

Here I will outline a useful strategy for porting MS-DOS(R) assembly
language programs to OS/2. Derived from my own experience in working with
the alpha- and beta-test versions of OS/2, this conversion process should
not be construed as an official recommendation from Microsoft, although I
find that it works well. The procedure consists of five steps: segmentation,
rationalization, encapsulation, conversion, and optimization. The first
three should be performed and tested in the MS-DOS environment. It is only
the last two that require OS/2 and the associated programming tools.


Segmentation

The Intel(R) 80286 microprocessor can function in either of two distinct
modes: real mode or protected virtual address mode. In real mode, the
80286 has a 1Mb address space and behaves like an 8086 or 8088 processor,
except that some machine instructions are more efficiently implemented and
a few new instructions are available. MS-DOS runs on the 80286 in real mode.

In protected mode, the personality of the 80286 is considerably different.
Hardware features come into play letting an operating system manage 1Gb of
virtual address space, isolate the memory used by one process from that
used by another, perform fast context switching between processes and
interrupt handlers, intercept certain privileged machine instructions, and
detect the execution of invalid opcodes. OS/2, of course, runs in the
80286's preferred state: protected mode.

Most of the 80286's protected-mode capabilities revolve around a change in
the way memory is addressed. In real mode, the value in a segment register
directly corresponds to a physical memory address. In protected mode, this
correspondence is indirect: a segment register holds a selector, which is
an index into a table of descriptors. A descriptor defines the physical
address and length of a memory segment, its characteristics (executable,
read-only data, or read/write data) and access rights, and whether the
segment is currently resident in RAM or has been swapped to disk. Each
time a program loads a segment register or accesses memory, the 80286
hardware checks the relevant descriptor table entry and the program's
privilege level, generating a hardware interrupt (called a protection
fault) if the selector or memory operation is not valid. Needless to say,
manipulation of the descriptor table itself is reserved for the operating
system's memory manager.

This scheme of memory addressing in protected mode has two immediate
consequences for applications programs. First, they can no longer perform
arithmetic on the contents of segment registers (since selectors are only
index numbers and have no direct relationship to physical memory
addresses) or use segment registers for storing temporary values. A
program must not load a segment register with anything but a legitimate
selector that it received from the OS/2 loader or as a result of an OS/2
memory allocation function call. Second, machine code ("text") and data
must be strictly segregated from each other by placing them in separate
segments with distinct selectors, since an executable selector is not
writable, and vice versa.

Accordingly, the first step in converting a program for OS/2 is to impose
upon it a segmented structure compatible with the protected-mode
environment. The program must have at least one code and one data segment,
and it should use the special name DGROUP to declare a group containing
the "near data" segment, stack, and local heap (if there is one). Figure 1
shows this skeletal structure. It's best to follow the segment naming and
ordering conventions used by the Microsoft compilers (see Figures 2 and
3). At this stage, you should also remove or rewrite any code that
directly manipulates segment values.

Now reassemble and link your program and make sure it still works as
expected under MS-DOS. Changing or adding segmentation often uncovers
hidden addressing assumptions in the code, so it is best to track these
problems down before making any other substantive changes to the program.


Rationalization

Once you've successfully segmented your program so that it can be linked
and executed as an .EXE file under MS-DOS, the next step is to rationalize
your code. This means converting your program into a completely well-
behaved MS-DOS application, ruthlessly eliminating any elements that
manipulate the peripheral device controllers directly, alter interrupt
priorities or edit the system interrupt vector table, depend on CPU speed
or characteristics (such as timing loops), or are incompatible with MS-
DOS's memory management or handle-based file and record management.

When an MS-DOS application directly accesses the hardware, it is usually
in connection with display routines and keyboard handling. Caught between
the user's demand for snappy screen interaction and the regrettably poor
performance of the MS-DOS and IBM(R) PC ROM BIOS video drivers, most
programmers have felt it necessary to go around the operating system and
give the applications program complete control of the video adapter. This
allows the screen to be updated at the maximum speed possible under MS-DOS
but poses obvious difficulties in a multitasking, protected memory
environment such as OS/2. For porting purposes, all routines in your
program that write text to the display, control character attributes, or
affect cursor shape or position should be converted into Write Handle
(Interrupt 21H Function 40H) calls with ANSI escape sequences or ROM BIOS
Interrupt 10H calls. Similarly, convert all keyboard operations to Read
Handle (Interrupt 21H Function 3FH) or ROM BIOS Interrupt 16H calls.

After you've expunged all hardware dependence from your program, your next
priority is to make sure it uses system memory properly. Whereas MS-DOS
typically hands an application all of the unoccupied memory in the system,
this is not true of OS/2. A process is initially allocated only enough
memory to hold its code segment and stack and to meet its declared data
storage needs. You can make the MS-DOS loader behave in the same way that
the OS/2 loader does by linking your application with the /CPARMAXALLOC
switch. Alternatively, your program can execute a Set Memory Block
(Interrupt 21H Function 4AH) call early in its initialization routine to
release any extra memory it may have been allotted by the loader.

When your program has begun its main sequence of operation, it should
dynamically obtain and release any additional memory it may require for
buffers and tables with the MS-DOS Interrupt 21H Functions 48H (allocate
memory block) and 49H (free memory block). The size of any single
allocated block should not exceed 65,536 bytes, even though MS-DOS allows
the allocation of larger blocks; this will ensure compatibility with
protected mode.

Finally, you must turn your attention to file handling. MS-DOS takes a
rather schizoid approach to mass storage by supporting two completely
different sets of file and record management calls. The first set relies
on a data structure known as a file control block (FCB), while the second
and far more powerful group uses null-terminated (ASCIIZ) filename strings
and 16-bit file tokens called handles. The FCB calls became obsolete with
the advent of the hierarchical directory structure in MS-DOS 2.0, but MS-
DOS still supports them in the name of upward compatibility, and
unfortunately there are still programs around that use them. If your
program is one of these, you'll have to replace every FCB file or record
function call with its handle-based equivalent, because OS/2's protected
mode contains no support at all for FCBs.


Encapsulation

Now that you have a well-behaved, segmented MS-DOS application in hand,
the most painful part of the porting process is behind you. To prepare
your program for the actual conversion to protected-mode operation, you
should next isolate all parts of it that are specific to the host
operating system and encapsulate them inside individual subroutines. The
objective here is to localize the program's knowledge of the environment
into small procedures that you can subsequently modify without affecting
the remainder of the program.

As an example of a program component that needs encapsulation, consider a
typical call by an MS-DOS application to write a string to the standard
output device (ordinarily the video display). MS-DOS services are invoked
with a software interrupt, which occupies less space than a subroutine
call, so it is common practice to code MS-DOS calls in-line, as shown in
Figure 4. To facilitate conversion to OS/2, you should replace every
instance of such a write to a file or device with a call to a small
subroutine that hides the mechanics of the actual operating system
function call, as illustrated in Figure 5.

Another candidate for encapsulation, which does not necessarily involve an
operating system function call, is the application's code to gain access
to command line parameters, environment block variables, and the name of
the file it was loaded from. MS-DOS divides this information between the
program segment prefix and the environment block. Nearly every assembly
language programmer has evolved his or her own techniques for obtaining
these parameters when they're needed. My own solution is to write three
subroutines that return the same information as C's argc, argv, and
getenv: the number of command line parameters, a pointer to a specific
parameter, and the string associated with a specific environment block
variable, respectively.

When you've finished encapsulating the system services, subject your
program to thorough testing once more under MS-DOS. Since you are still
working in a familiar milieu and have access to your favorite debugging
tools, this is the best time to catch any subtle errors you may have
introduced during the three steps discussed thus far. You'll have more
trouble trying to uncover them once you move to the OS/2 environment.


Conversion

Now each system-dependent procedure you created during the encapsulation
stage must be rewritten so that your program can execute under OS/2. In
contrast to MS-DOS functions, which are actuated through software
interrupts and use registers for parameter passing, OS/2's Application
Program Interface functions are requested through a "far call" to a named
entry point. Parameters are passed on the stack, along with the addresses
of variables or structures that lie within the calling program's data
segment and will receive any results returned by the function. The status
of an operation is returned in register AX (0 if the function succeeded,
an error code otherwise). All other registers are preserved.

Figures 6, 7, and 8 list OS/2 services that are equivalent to selected
MS-DOS and ROM BIOS Interrupt 21H, Interrupt 10H, and Interrupt 16H calls.
These tables don't include MS-DOS functions related to file control blocks
or program segment prefixes because OS/2 does not support either of these
structures. MS-DOS TSR functions are also omitted. Since OS/2 is a true
multitasking system, there is no need for a process to terminate in order
to stay resident while another process is running.

As you examine each encapsulation subroutine, refer to the entry for the
appropriate function in the MS(R) OS/2 Programmer's Reference manual. All
you have to do is write code that takes the parameters passed into the
procedure and pushes them onto the stack in the correct order, calls the
appropriate OS/2 function, tests the returned status, and loads any
returned values into the appropriate registers.

While working your way through the program, you must remember to declare
each OS/2 function used as a "far external." If the function requires any
additional storage space in the application's data segment, you must also
create this. For some OS/2 calls (such as DOSOPEN), it will also be
necessary to push other parameters that had no counterpart under MS-DOS.
These extra parameters can usually be given reasonable values that will
make their existence temporarily invisible to the remainder of the
application.

Figure 9 illustrates the final form of our sample procedure after its
conversion for OS/2. Note especially the addition of the EXTRN statement
and the wlen variable, as well as the simulation of the MS-DOS function
status. This code may not be elegant, but it is the result of only a
minimum of changes in the source file.

Once you've converted all the encapsulation subroutines and added the
necessary variables and external references, you may delete the STACK
segment declaration from the .ASM source file and change the ASSUME
statement for the SS register to DGROUP. Next, assemble the source file
into a relocatable .OBJ module file with the Microsoft Macro Assembler in
the usual manner. In order to transform the .OBJ file into a protected-
mode .EXE file, however, you will need to supply the linker with two
additional files: DOSCALLS.LIB and a module definition (.DEF) file for
your application.

DOSCALLS.LIB is a special type of object module library, which contains
stub records, called dynamic link reference records, for each OS/2
function call. Each time the linker finds a stub record that matches an
EXTRN declaration within the program, it adds the name of the external
routine to a table in the .EXE file header. When the program is ultimately
loaded for execution, the table is examined, the necessary external
procedures are brought into memory from dynamic link (.DLL) libraries (if
they are not already resident), and the far calls within the program are
fixed up appropriately.

The .DEF file is just an ASCII text file that can be created with any line
or screen editor. It contains directives that control various
characteristics of a protected-mode executable, such as the module name,
whether the file is an application or a dynamic link library, the
attributes of each segment within the program, and the size of the stack
(and for C programs, the size of the local heap). Figure 10 provides a
very simple sample .DEF file.

You will be pleasantly surprised to find that testing and debugging a
newly converted protected-mode application is a relatively straightforward
endeavor. MASM has been upgraded so that it can include line numbers in
object modules, and the Microsoft OS/2 Software Development Kit includes a
protected-mode version of CodeView(R), Microsoft's excellent symbolic
debugger. In addition, the very nature of protected mode means that
although your programs are just as prone to crashing as before, it's
fairly difficult to crash the entire system──you won't need that big red
switch nearly as often as you did with MS-DOS.


Optimization

Now that your program is running properly in protected mode, you will be
anxious to smooth out some of the alterations you made for purposes of
conversion and introduce various optimizations. There are three obvious
categories of optimization to consider: taking advantage of the additional
functionality of the OS/2 calls already being used, exploiting 80286-
specific machine instructions where appropriate, and revamping the
application in order to cash in on OS/2's multitasking, timer, virtual
memory, and inter-process communication facilities.

To make optimum use of the OS/2 function calls, you can quickly modify and
enhance the subroutines that encapsulate them. For example, the OS/2
DOSOPEN function allows the programmer to determine separately what action
will be taken if the named file already exists (open, fail, or truncate to
zero length) or does not exist (create or fail). Similarly, the OS/2 video
driver offers performance and a variety of services far superior to the
screen support under MS-DOS.

In the case of OS/2 functions that are called only once or twice by the
application, it may be more efficient to throw away the encapsulating
subroutine and place the function call in-line, thus eliminating the need
to load parameters into registers outside the subroutine and push them on
the stack inside.

If you do not intend to use the Family Application Program Interface so
that your program will also run on 8086/88-based machines, you can add the
directive .286c to the beginning of the source file and take advantage of
80286-specific machine instructions. The most useful of these are the
instructions that let you shift or rotate by an immediate count other than
one, a three-operand multiply where one of the operands is an immediate
(literal) value, and a push immediate value instruction. The last of these
is particularly handy for setting up OS/2 function calls. For example, in
Figure 9, the sequence

  mov  ax,offset DGROUP:wlen
  push ax

could be replaced by the single instruction

  push offset DGROUP:wlen

If you wish to restructure your application to take full advantage of
OS/2's programmable timers, multitasking, virtual memory, inter-process
communication, dynamic linking, and device monitors you'll have to make a
close examination of both your application and the OS/2 Application
Programming Interface. Such study will often pay off in sizable benefits
in performance, maintainability, and code sharing.

For instance, in many cases, different elements of an application deal
with I/O devices of vastly different speeds, such as the keyboard, disk,
and video display. You can both simplify the application and increase its
performance by separating these elements into threads (subprocesses) that
execute asynchronously, communicating through shared data structures and
synchronizing when necessary via semaphores.

As another example, when several applications are closely related and
contain many identical or highly similar procedures, it makes sense to
extract those procedures from the individual applications and place them
in a private dynamic link library. This reduces the size of each
application's .EXE file, since the dynamic link library routines are
brought into memory and bound to the application when it's loaded. It
allows more efficient use of memory, since the code segments in the
dynamic library can be shared among all of the related applications when
they are running concurrently. Best of all, using private dynamic link
libraries vastly simplifies code maintenance, since you can debug or
improve the routines in the libraries at any time without relinking the
calling applications, which will automatically benefit from the new code
the next time they are executed.


Figure 1:  Skeleton of a properly segmented MS-DOS assembly language
           application being prepared for conversion to OS/2.

        name    myprog
        page    55,132
        title   MYPROG - segmentation skeleton
                                    ∙   ; miscellaneous equates
                                    ∙   ; structures, and other
                                    ∙   ; declarations go here
DGROUP  group   _DATA                   ; 'automatic data group'

_TEXT   segment byte public 'CODE'      ; all executable code
                                        ; goes in this segment

        assume  cs:_TEXT,ds:DGROUP,ss:STACK

main    proc    far                     ; the routine that initially
                                    ∙   ; receives control can be
                                    ∙   ; called anything ...
                                    ∙
        mov     ax,4c00h                ; main routine terminates,
        int     21h                     ; passing return code=0

main    endp
                                    ∙   ; other routines needed by
                                    ∙   ; the program go here
                                    ∙
_TEXT   ends

_DATA   segment word public 'DATA'      ; all read/write or static
                                        ; data items in this segment
                                    ∙
                                    ∙
                                    ∙
_DATA   ends

STACK   segment para stack 'STACK'

        db      256 dup (?)

STACK   ends

        end     main                     ; declares end of module
                                         ; and initial entry point


Figure 2:  These memory models are commonly used in assembly language and
           C programs. Microsoft FORTRAN programs uniformly use the large
           model. Microsoft C also supports a huge model, which allows
           creation of data structures larger than 64Kb.

Model            Code Segments           Data Segments

Small            One                     One
Medium           Multiple                One
Compact          One                     Multiple
Large            Multiple                Multiple


Figure 3:  Microsoft assembler programs use these naming conventions for the
           standard memory models. Microsoft C programs use a superset of
           these segments and classes.

╓┌──────────┌─────────────┌───────┌─────────┌──────────┌─────────────────────╖
Memory     Segment        Align    Combine    Class
Model      Name           Type     Classes    Name        Group

Small      _TEXT          byte     public     CODE
           _DATA          word     public     DATA        DGROUP
           STACK          para     stack      STACK       DGROUP

Medium     module_TEXT    byte     public     CODE
             ∙
             ∙
           _DATA          word     public     DATA        DGROUP
           STACK          para     stack      STACK       DGROUP

Compact    _TEXT          byte     public     CODE
           data           para     private    FAR_DATA
Memory     Segment        Align    Combine    Class
Model      Name           Type     Classes    Name        Group
           data           para     private    FAR_DATA
             ∙
             ∙
           _DATA          word     public     DATA        DGROUP
           STACK          para     stack      STACK       DGROUP

Large      module_TEXT    byte     public     CODE
             ∙
             ∙
           data           para     private    FAR_DATA
             ∙
             ∙
           _DATA          word     public     DATA        DGROUP
           STACK          para     stack      STACK       DGROUP



Figure 4:  This typical in-line code sequence for an MS-DOS service call
           writes a string to the standard output device. Since the
           standard output might be redirected to a file without the
           program's knowledge, it must also check that all of the
           requested characters were actually written. If the returned
           length is less than the requested length, this usually indicates
           that the standard output has been redirected to a disk file and
           the disk is full.

stdin     equ  0              ; handle for standard input
stdout    equ  1              ; handle for standard output
stderr    equ  2              ; handle for standard error
                                    ∙
                                    ∙
                                    ∙
msg       db   'This is a sample message'
msg_len   equ  $-msg
                                    ∙
                                    ∙
                                    ∙
          mov  dx,seg msg     ; DS:DX = address of message
          mov  ds,dx
          mov  dx,offset DGROUP:msg
          mov  cx,msg_len     ; CX = length of message
          mov  bx,stdout      ; BX = file or device handle
          mov  ah,40h         ; AH = function 40h, write
          int  21h            ; transfer to MS-DOS
          jc   error          ; on return, CY=true if error
          cmp  ax,msg_len     ; were all characters written?
          jne  dev_full       ; no, output device is full
                                    ∙
                                    ∙
                                    ∙


Figure 5:  The code in Figure 4 has here been encapsulated. The portion of
           the code that is operating system-dependent has been isolated
           inside a subroutine that is called from other points within the
           application.

stdin     equ  0              ; handle for standard input
stdout    equ  1              ; handle for standard output
stderr    equ  2              ; handle for standard error
                                    ∙
                                    ∙
                                    ∙
msg       db   'This is a sample message'
msg_len   equ  $-msg
                                    ∙
                                    ∙
                                    ∙
          mov  dx,seg msg     ; DS:DX = address of message
          mov  ds,dx
          mov  dx,offset DGROUP:msg
          mov  cx,msg_len     ; CX = length of message
          mov  bx,stdout      ; BX = file or device handle
          call write          ; perform the write
          jc   error          ; on return, CY=true if error
          cmp  ax,msg_len     ; were all characters written?
          jne  dev_full       ; no, output device is full
                                    ∙
                                    ∙
                                    ∙
write     proc near           ; Write to file or device
                              ; Call with:
                              ; BX = handle
                              ; CX = length of data
                              ; DS:DX = address of data
                              ; Returns:
                              ; If successful, Carry clear
                              ;   and AX = bytes written
                              ; If error, Carry set
                              ;   and AX = error code

          mov  ah,40h         ; function 40h = write
          int  21h            ; transfer to MS-DOS
          ret                 ; return status in CY and AX

write     endp
                                    ∙
                                    ∙
                                    ∙


Figure 6:  Selected MS-DOS function calls and their OS/2 counterparts. Note
           that OS/2 functions are generally much more powerful and flexible
           than their MS-DOS ancestors.

╓┌───────────┌────────────────────────────────────┌──────────────────────────╖
MS-DOS
Interrupt
21H
Function    Description                          OS/2 Function

00H         Terminate process                    DosExit
01H         Keyboard input with exho             KbdCharIn
02H         Output character to screen           VioWrtTTY
03H         Auxiliary input                      DosRead
04H         Auxiliary output                     DosWrite
05H         Printer output                       DosWrite
06H         Direct console I/O                   KbdCharIn,
            VioWrtTTY
07H         Unfiltered input without echo        KbdCharIn
08H         Keyboard input without echo          KbdCharIn
09H         Output string to screen              VioWrtTTY
0AH         Buffered keyboard input              KbdStringIn
0BH         Get keyboard Status                  KbdPeek
MS-DOS
Interrupt
21H
Function    Description                          OS/2 Function
0BH         Get keyboard Status                  KbdPeek
0CH         Reset buffer and input               KbdFlushBuffer,
            KbdCharIn
0DH         Disk reset                           DosBufReset
0EH         Select default disk drive            DosSelectDisk
19H         Get default disk drive               DosQCurDisk
1BH         Get information for default drive    DosQFSInfo
1CH         Get information for selected drive   DosQFSInfo
2AH         Get system date                      DosGetDateTime
2BH         Set system date                      DosSetDateTime
2CH         Get system time                      DosGetDateTime
2DH         Set system time                      DosSetDateTime
2EH         Set Verify Switch                    DosSetVerify
30H         Get MS-DOS version                   DosGetVersion
36H         Get free disk space                  DosQFSInfo
38H         Get or select country                DosGetCtryInfo
39H         Create directory                     DosMkdir
MS-DOS
Interrupt
21H
Function    Description                          OS/2 Function
39H         Create directory                     DosMkdir
3AH         Delete directory                     DosRmdir
3BH         Select directory                     DosChdir
3CH         Create or truncate file              DosOpen
3DH         Open file                            DosOpen
3EH         Close file                           DosClose
3FH         Read file or device                  DosRead
40H         Write file or device                 DosWrite
41H         Delete file                          DosDelete
42H         Move file pointer                    DosChgFilePtr
43H         Get or set file attributes           DosQFileMode
44H         Device driver control (IOCTL)        DocDevIOCtl
45H         Duplicate handle                     DosDupHandle
46H         Force duplicate of handle            DosDupHandle
47H         Get current subdirectory             DosQCurDir
48H         Allocate memory block                DosAllocSeg
49H         Release memory block                 DosFreeSeg
MS-DOS
Interrupt
21H
Function    Description                          OS/2 Function
49H         Release memory block                 DosFreeSeg
4AH         Resize memory block                  DosReAllocSeg
4BH         Load and execute chils process       DosExecPgm
4CH         Terminate process with return code   DosExit
4DH         Get return code of child process     DosCWait
4EH         Search for first match               DosFindFirst
4FH         Search for next match                DosFindNext
54H         Get verify flag                      DosQVerify
56H         Rename file                          DosMove
57H         Get or set file date and time        DosQFileInfo,
                                                 DosSetFileInfo
59H         Get extended error information       DosErrClass
5BH         Create unique file                   DosOpen
5CH         Lock or unlock file region           DosFileLock


Figure 7:  OS/2 video services include these equivalents to ROM BIOS
           Interrupt 10H video display driver functions used by MS-DOS
           applications.

ROM BIOS
Interrupt
10H
Function     Description                         OS/2

00H          Select display mode                 VioSetMode
01H          Set cursor type                     VioSetCurType
02H          Set Cursor position                 VioSetCurPos
03H          Get cursor position                 VioGetCurPos
06H          Initialize or scroll window up      VioScrollUp
07H          Initialize or scroll window down    VioScrollDn
08H          Read character and attribute        VioReadCellStr
09H          Write character and attribute       VioWrtNCell
0AH          Write character                     VioWrtNChar
0EH          Write character in teletype mode    VioWrtTTY
0FH          Get current display mode            VioGetMode
13H          Write string in teletype mode       VioWrtTTY


Figure 8:  OS/2 keyboard services include these equivalents to ROM BIOS
           Interrupt 16H keyboard driver functions used by MS-DOS
           applications.

ROM BIOS
Interrupt
16H
Function     Description                OS/2 Function

00H          Read keyboard character    KbdCharIn
01H          Get keyboard status        KbdPeek
02H          Get keyboard flags         KbdGetStatus


Figure 9:  The code in Figure 5 after conversion. The equivalent OS/2
           function call has replaced the MS-DOS function call. Since
           dependence on the operating system has been confined to the
           subroutine by the previous encapsulation step, the surrounding
           program's requests for write operations should run unchanged.
           Note that the OS/2 function had to be declared as an external
           name with the "far" attribute and that a variable named when
           was added to the data segment of the application to receive the
           actual bytes written.

stdin     equ  0              ; handle for standard input
stdout    equ  1              ; handle for standard output
stderr    equ  2              ; handle for standard error

          extrn DOSWRITE:far
                                    ∙
                                    ∙
                                    ∙
msg       db   'This is a sample message'
msg_len   equ  $-msg
wlen      dw   ?
                                    ∙
                                    ∙
                                    ∙
          mov  dx,seg msg     ; DS:DX = address of message
          mov  ds,dx
          mov  dx,offset DGROUP:msg
          mov  cx,msg_len     ; CX = length of message
          mov  bx,stdout      ; BX = file or device handle
          call write          ; perform the write
          jc   error          ; on return, CY=true if error
          cmp  ax,msg_len     ; were all characters written?
          jne  dev_full       ; no, output device is full
                                    ∙
                                    ∙
                                    ∙
write     proc near           ; Write to file or device
                              ; Call with:
                              ; BX = handle
                              ; CX = length of data
                              ; DS:DX = address of data
                              ; Returns:
                              ; If successful, Carry clear
                              ;   and AX = bytes written
                              ; If error, Carry set
                              ;   and AX = error code

          push bx             ; handle
          push ds             ; long address of data
          push dx
          push cx             ; length of data
          push ds             ; address of variable to
          mov  ax,offset DGROUP:wlen    ; receive length written
          push ax
          call DOSWRITE       ; transfer to OS/2
          or   ax,ax          ; did write succeed?
          jnz  writerr        ; jump, error was returned
          mov  ax,wlen        ; no error, OR cleared CY
          ret                 ; and AX := bytes written

writerr:  stc                 ; write error, return CY set
          ret                 ;   and AX = error number

write     endp
                                    ∙
                                    ∙
                                    ∙


Figure 10:  This simple module definition (.DEF) file defines MYPROG.EXE
            as a protected-mode application rather than a dynamic link
            library and declares that the code segment is movable and
            discardable (a fresh copy can be loaded when needed, so
            swapping is not necessary), the data segment is movable and
            swappable but not discardable (it contains read/write data
            items), and the stack size is 4,096 bytes.

NAME MYPROG
PROTMODE
DATA MOVEABLE
CODE MOVEABLE PURE
STACKSIZE 4096

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

Microsoft Windows 2.0: Major Enhancements Offer More Control

───────────────────────────────────────────────────────────────────────────
Also see the related article:
  The Beta Version of Windows 2.0 Update
───────────────────────────────────────────────────────────────────────────

Michael Geary☼

Windows Version 2.0 is Microsoft(R)'s first major revision of its windowed
operating environment. This updated version features the new overlapping-
window user interface that will be used in the OS/2 presentation manager.
Unlike the presentation manager, however, Windows 2.0 uses the same
programming interface as Windows 1.x, albeit with a number of new
functions and messages, and retains, for the most part, upward
compatibility with existing Windows applications. Most .EXE files will run
with no alterations. Yet source code will require some minor revisions to
iron out several incompatibilities and bring it in line with changes made
to the application style.

Once you convert to Windows 2.0, you'll be rewarded with a fine assortment
of new function calls and messages that make Windows a much more flexible,
capable product for the applications developer.


User Interface

The most obvious change in Windows 2.0 is, of course, the new user
interface. (For a closer look at the features of this interface, see "OS/2
Windows Presentation Manager: Microsoft Windows on the Future," MSJ Vol. 2,
No. 2.) For the most part, this won't affect your programs. If an
application ran in a tiled window, it will now simply use an overlapping
window instead──and most likely will not even notice the difference.
However, you should make several changes in the menu structure and
keyboard accelerators so that your application will conform to the new
user interface guidelines.

Applications should no longer add menu items to the System menu. At the
bottom of the File menu, add a separator line and the menu item Exit,
which will allow the user to exit the application. Then move the
"About..." item from the System menu to the end of the File menu,
appending the name of your application here. Help menu items should now be
flush right in the menu bar, and there is a new HELP dialog option to
allow this. The Edit menu items remain unchanged, but the keyboard
accelerators are different and are also shown in a different manner.
Therefore, you should change your resource file and any special code you
may have to handle the editing keys. Figure 1 gives the editing key
assignments.

You can now specify which letter of each menu item can be used in
combination with the Alt key as a shortcut for making a menu selection.
Simply insert an ampersand (&) in the item name preceding the shortcut
letter, and Windows will underline it. For examples of this, see Figure 2,
which shows a portion of an .RC file in the style recommended for the File
and Edit menus.

You can also add keyboard mnemonics to your dialog boxes. As with menu
items, insert an ampersand at the appropriate point in the text of the
dialog item. For push buttons, radio buttons, and check boxes, you can put
a shortcut letter right in the name. You can't do this with an edit
control, of course, but you can instead put a shortcut letter in an
adjacent static text control. When the user presses the shortcut key, it's
interpreted as a tab to the next tab stop after the static control.


MDI

A major new style recommendation for both Microsoft Windows 2.0 and the
OS/2 presentation manager is the Multiple Document Interface (MDI). This
is a method for using child windows in an application that works on
multiple documents or multiple views of one document. Instead of creating
a top-level overlapping window for each document or view, the application
creates a single main window called the application workspace, which
contains a child window for each document or view. These child windows can
overlap and can be moved, sized, minimized, and maximized, just like top-
level windows. The difference, of course, is that all the child windows
are clipped at the edge of the workspace window.

The active child window has its title bar highlighted along with the
parent window's title bar. Each child window has a System menu, but none
can have its own menu bar. The workspace window contains the menu for the
entire application. If different menus are needed for the different child
windows, the application should change or add items in the workspace
window's menu as appropriate.

Any child window can be maximized, that is, enlarged to fill up the
workspace window, hiding all the other child windows. The user can still
flip through the different child windows, but each one in turn becomes
maximized. The workspace window has a pop-up Window menu, which lists all
the child windows so that any one can be easily selected, whether it's
visible or not.

For example, a sequencer program for controlling musical instruments via
the Musical Instrument Digital Interface (MIDI) might have these child
windows in its application workspace:

  ■  conventional music (staff) notation
  ■  piano-roll notation
  ■  a piano keyboard
  ■  synthesizer voice editing
  ■  miscellaneous windows for tempo control, score formatting, key
     signature, MIDI options, and so on.

If these were all independent top-level windows, it would be a real chore
for the user to switch from the sequencer program to, say, Windows Write.
There would be a good half-dozen windows cluttering up the screen. With
the Multiple Document Interface, these windows would all be contained in a
single application workspace window, and the user could get them all out
of the way by simply minimizing the application window (that is, by changing
it to an icon).

Thus, MDI helps organize applications as independent entities, cleanly
separating the windows of different programs. This will become more
important as users really start to make use of the multitasking abilities
of Windows and OS/2.

Although Windows provides several new features that let developers create
possible MDI applications, such applications require a fair amount of code
to support MDI. The Windows 2.0 Application Style Guide details what's
needed in an MDI application.


Scheduling and Input

Windows 2.0 has nearly 60 new functions, and several of the old functions
have been extended with new options. Many of the changes were made to open
up the Windows architecture, giving the application designer more control.
Many things that just couldn't be done in earlier versions of Windows are
now simple function calls.

One change is relatively invisible to applications but quite welcome to
the developer: pulling down a menu no longer stops scheduling, so
applications continue to run when a menu is pulled down. But what of those
cases in which an application really needs to turn off scheduling? For
example, a forms-design program may want to let the user drag a selection
rectangle across the entire screen when creating a pop-up window. The
program can't yield to other applications, because it is temporarily
writing into their screen space.

In Windows 1.x, there was no way to turn off scheduling and still be able
to determine when the mouse button was released. You couldn't use
PeekMessage or GetMessage, because they would yield to other applications.
Nor would GetKeyState work, because this gives you the key state at the
time of the last message, not the real-time key state.

Now, both of these methods are available. A new option in the PeekMessage
function lets you choose not to yield to other applications. Complementing
this is a new GetAsyncKeyState function, which resembles GetKeyState but
gives you the real-time key state that is not synchronized with the
message queue.

A related function for keyboard control is GetKeyboardState, which gives
you the current state of all 256 virtual keys in a buffer you provide. Its
inverse, SetKeyboardState, lets you set the current state of the 256
virtual keys.

Another useful function is GetInputState, which returns a Boolean value
indicating whether any mouse, keyboard, or timer events are waiting in the
system queue. And if the old limit of eight messages in your application's
queue isn't enough, you can call SetMessageQueue to change your queue
size.

Rounding out the input functions are a couple of handy Get functions.
GetCapture tells you which window (if any) has captured the mouse with
SetCapture. GetCaretPos is the complement of SetCaretPos, returning the
current position of the blinking caret. (Actually, GetCaretPos has always
been available in Windows──it just wasn't documented until now.)


Creating Windows

The CreateWindow function has some new style bits. Since a window can now
have Minimize and Maximize icons in its title bar, there are style bits
for these. Also, some of the other style flags have acquired new names. If
you feel silly using WS_TILEDWINDOW when your window is no longer tiled,
you can use the more accurate but functionally identical
WS_OVERLAPPEDWINDOW.

More important, you can now specify the initial location and size of a
top-level window. You're no longer at the mercy of the tiling algorithm.
If you want to create two windows side by side, you can do it. You can
still let Windows assign a default size and location, though. You just use
the special value CW_USEDEFAULT (which happens to be 0x8000) for the
position and size parameters to CreateWindow.

Now we come to a slight anomaly. A Windows application certainly isn't
passing 0x8000 for its position and size. More likely, it's passing zeros
for all four values, thinking it's creating a tiled window. Windows
assigns default values in this case, just as it used to. But, if you try
passing zeros to CreateWindow in your Windows 2.0 application, you'll get
a very tiny window instead. What's going on here? There certainly aren't
two different CreateWindow functions.

The key here is that the Resource Compiler sets a version number flag in
your .EXE file, and Windows uses that to decide how to interpret some
function parameters, such as the position and size in CreateWindow. If it
encounters a Windows 1.x application, it ignores the position and size as
before. But if it sees the 2.0 flag, it pays attention to those parameters.


Window Management

The window management capability of Microsoft Windows 2.0 really made me
happy, since Windows 1.x is frustratingly limiting in this area at times.
When using Windows 1.x, I knew it was maintaining a linked list of my
windows, but I couldn't manipulate the list at all. For one of my own
programs, this was a definite problem. I really needed a way to change the
order of windows in the list and step through the list backwards and
forwards as the dialog manager does. Previous versions of Windows just
don't allow that. The only functions that come close are BringWindowToTop
and EnumWindows.

Windows 2.0 gives you all kinds of direct access to the window list. Let's
start with GetWindow and GetNextWindow. These functions take a window
handle and return your choice of next sibling, previous sibling, first
sibling, last sibling, parent, or first child. And you can navigate in the
same way that the dialog manager does. Next, we have SetWindowPos, an all-
purpose window rearranger. It duplicates some of the functions of
MoveWindow and ShowWindow but also has the capability to change the
ordering of the window list. If you have Windows A and B, for example, you
can place window A after (underneath) window B and even resize and move
the window at the same time.

If that's not enough, there's SetParent. This lets you change the parent
of a child window──you can actually move a child window from one parent to
another.

Next, we have ShowOwnedPopups, which lets you show or hide all the pop-up
windows associated with a window. Similarly, with ShowScrollBar you can
show or hide a scroll bar (or both scroll bars) without having to fiddle
with the scroll bar range.

To round out window management, there are a few more functions.
GetTopWindow returns the topmost child of a parent window, GetWindowTask
returns the task handle for a window, and EnumTaskWindows resembles
EnumWindows but enumerates only windows belonging to a specified task
handle.


Window Drawing

Windows 2.0 contains several new GDI functions. Most important of these
are EnumMetaFile and PlayMetaFileRecord, which together are the equivalent
of PlayMetaFile. EnumMetaFile, however, calls a function you provide
instead of just playing the file. It passes each metafile record to your
function, where you can do whatever you want with it. If you just want to
play it, call PlayMetaFileRecord. The new Microsoft Windows Software
Development Kit documentation gives all the details of the metafile
format, another case in which Microsoft has really opened up the Windows
architecture.

The TextOut function now lets you specify how the text will be aligned
with the x-y starting point. Previously, that point could correspond only
to the top left corner of the text, but now you have the choice of top
left, top right, left center, top center, bottom center, or font baseline.
You can also specify whether TextOut updates GDI's current position. These
options form the text alignment flag and are part of the new SetTextAlign
function. (The calling sequence for TextOut itself isn't changed.) There's
also a GetTextAlign function to retrieve the current text alignment flag.

DrawText has a couple of new options, too. There's a DT_CALCRECT option,
which updates the formatting rectangle to show how much space DrawText
actually used. Also, DrawText now interprets the ampersand as a flag to
underline the next character; if you want to display an ampersand, you
need to use two of them (&&). You can defeat this action with the
DT_NOPREFIX option.

For the font mapper, there's a new SetMapperFlags function. This lets you
alter the font mapper's algorithm, determining how much priority it places
on matching the aspect ratios of the font and the output device.

There's one new graphics output function, Chord, which is the same as Arc
except it draws the line segment as well as the ellipse segment, filling
in the enclosed area with the current brush. For bitmaps, there's
CreateDiscardableBitmap, which duplicates CreateCompatibleBitmap but makes
the bitmap discardable.

A new scrolling function, ScrollDC, is similar to ScrollWindow but allows
more control over the scrolling. Instead of letting the uncovered area
accumulate in the window's update region, it passes the uncovered region
and its bounding rectangle back to the caller.

Finally, there are two new region functions. ExcludeUpdateRgn is related
to ExcludeClipRect, but it removes a window's update region from a
clipping region. This could be useful when you're doing some drawing in
advance of a WM_PAINT message and want to draw only in the region that
will not be taken care of by the WM_PAINT. SetRectRgn is just like
CreateRectRgn, but instead of allocating a new region, it takes an
existing region and sets it to the specified rectangle.


Dialog Boxes

Have you ever wished you could create a dialog box on the fly at run time
instead of having to use a dialog template from the resource file?
Previously you had to give explicit CreateWindow calls for all the child
windows in the dialog box. There was no way you could just set up a dialog
template data structure and let it run.

Now two new functions let you do just that. CreateDialogIndirect and
DialogBoxIndirect correspond to CreateDialog and DialogBox, but instead of
taking a resource from your .EXE file, they take a pointer to a dialog
template in memory. (Actually, DialogBoxIndirect takes a global handle,
and CreateDialogIndirect takes a far pointer.) You are no longer forced to
choose between using a canned dialog box or using dozens of CreateWindow
calls.

Two other functions, GetNextDlgGroupItem and GetNextDlgTabItem, let you
scan items the way the dialog manager does in response to the arrow and
tab keys. In fact, I'd wager that these functions were there all
along──because the dialog manager needed them──but were never made publicly
available. This seems to be yet another example of the architecture being
opened up to let you get at things previously hidden.


Control Windows

There aren't too many changes in the various types of control windows, but
radio buttons have been given additional functionality. You can now create
an automatic radio button. This is just like any other radio button,
except that when clicked it automatically checks itself and unchecks all
the other radio buttons in its group. There's always been a
CheckRadioButton function, of course, but this was for dialog boxes only.
If you wanted to add a group of radio buttons to some other kind of
window, you had to write your own code to check and uncheck them. Now the
buttons are smart enough to take care of themselves.

An enhancement of both radio buttons and check boxes gives you the option
of having label text appear on the left rather than the right side of the
graphic.


Menus

Menus used to be write only, at least in part. For example, given a menu
position, you could get the corresponding text string with GetMenuString,
but there was no way to find out what the menu ID was. This made it
difficult to write helper utilities that would send menu commands to other
applications, a process that required a menu ID.

Three new functions have overcome this limitation. GetMenuItemID takes a
menu position and menu handle and returns the menu item ID. GetMenuItemCount
tells you how many items are in either a top-level or a pop-up menu.
GetMenuState allows you to determine what the menu flags (MF_ENABLED and so
on) are for a menu item.

The new LoadMenuIndirect function replicates LoadMenu, but instead of
loading the menu resource from your .EXE file, it takes a pointer to a
menu resource in memory. This makes it easy to build a menu on the fly
without having to go through a whole series of CreateMenu and ChangeMenu
calls. Like the new indirect dialog box calls, this function provides a
nice middle ground between a static menu from the .EXE file and all the
explicit code it would otherwise take to build a menu.


Window Hooks

Window hooks are now almost fully documented and functional. They are no
longer among the most mysterious aspects of Microsoft Windows. The
SetWindowsHook function can install any of the hook functions listed in
Figure 3.

Two new functions help support hook functions. DefHookProc lets a hook
function chain to the previous hook function, so that there can be more
than one hook of the same type. UnhookWindowsHook removes a hook function
from a hook chain──something that before could only be done by exiting from
Windows.

They haven't taken all the mystery out of Windows, though. Along with the
documented hooks, WINDOWS.H shows three new ones that are undocumented:
WH_CBT, WH_SYSMSGFILTER, and WH_WINDOWMGR.


Memory Management

Windows now comes equipped with a kind of long-term equivalent of
GlobalLock, called GlobalWire. When you allocate global memory and declare
it FIXED, Windows allocates the space at the low end of memory to avoid
fragmentation problems. If you allocate MOVEABLE memory, Windows doesn't
worry about where it's located; then if you issue a GlobalLock, Windows
just locks this memory in its current location for the sake of speed. This
is fine for a short-term lock, but if you lock down memory for a long
time, the memory space becomes fragmented, causing all sorts of memory
management problems. GlobalWire prevents fragmentation by allocating a
MOVEABLE memory block to low memory, as if it had been declared FIXED in
the first place. GlobalUnwire unlocks memory that had been moved and
locked with GlobalWire.

SetSwapAreaSize lets you increase the amount of global memory dedicated to
code rather than data. Normally, Windows allows nearly all global memory
to be allocated for data with GlobalAlloc, reserving just enough space for
the largest single code segment from any application or library. Although
flexible, this arrangement will cause Windows to slow down severely if
memory gets extremely tight, because it has to keep swapping code segments
in. With SetSwapAreaSize, you can increase the amount of memory dedicated
for code. This reduces the amount of global data you can allocate but
prevents Windows from doing a great deal of low-memory thrashing.

ValidateFreeSpaces is a new debugging function that helps you catch wild
pointers. In the debugging version of Windows 2.0, the kernel fills all
free memory with 0x0C. ValidateFreeSpaces scans all the free memory, and
if it finds any byte that's not equal to 0x00C, it returns that address.
If you run into a memory-clobbering problem, frequent calls to this
function could help track it down.


Miscellany

Windows now has global atoms. These are like local atoms, but they are
allocated from the global heap instead of your local heap and are shared
among all applications. Four new functions serve as the global equivalents
of the local atom calls: GlobalAddAtom, GlobalDeleteAtom, GlobalFindAtom,
and GlobalGetAtomName.

A few more miscellaneous new functions: EqualRect simply compares two
rectangles and returns a Boolean value indicating whether they're equal.
GetTickCount returns the number of milliseconds since the system was
started. EnableHardwareInput enables or disables mouse and keyboard input.
Figure 4 lists a number of new or enhanced messages.


Upward Compatibility

Most existing Windows applications will run under Microsoft Windows 2.0,
but a change made to CreateWindow will affect nearly every
application──fortunately, this should be the only real incompatibility for
most programs. The position and size parameters of CreateWindow now have
different meanings. The parameters (0,0,0,0) will still work, but the
window will start out as being very tiny. You must either put in the
CW_USEDEFAULT parameters or give an explicit position and size. If you do
the latter, don't assume a particular screen size──use GetSystemMetrics.
(0,0,0,0) on a Windows 1.x application gives the same result as
CW_USEDEFAULT.

WM_NCxxxxxx messages can cause problems. Most programs pass the nonclient
messages through to DefWindowProc. If you process any of these messages to
do any kind of special nonclient area handling, expect some trouble. You
may also have problems if you have created a pop-up window that the user
can iconize or zoom, since you probably had to fiddle with the WM_NCxxxxxx
messages to get this to work. Since your application will be in an
overlapping window anyway, you can eliminate the problem by just removing
code (the easiest way to fix a bug).

Any code that depended on the order of the System menu items or their
exact functions will probably fail, since these have been changed. It's no
problem if you just added an "About. . ." box to the System menu, although
you will want to move it to the File menu in order to stay consistent with
other applications.

The last parameter to PeekMessage used to be called bRemoveMsg and was
treated like most Booleans──zero meant false and nonzero meant true. If
your code uses a value other than 0 or 1 for this parameter, you could run
into trouble, because the parameter has been extended. It's now called
wRemoveMsg, and the 0 and 1 values work as they always did, but any other
values will have different meanings. For example, a value of 2 would be
interpreted as PM_NOYIELD | PM_NOREMOVE instead of PM_REMOVE (TRUE).
You're not likely to run into this problem, but if you do, it could be
hard to track down. It's worth checking all your PeekMessage calls. (Do
this at the same time you convert your CreateWindow calls.)

Since DrawText now attaches a special meaning to the ampersand, this
function won't do what you want if you have one in the string. You must
either change it to a double ampersand or add the DT_NOPREFIX option to
the call.

A few of the things that Windows 1.x let you get away with aren't
tolerated by Windows 2.0. Strictly speaking, they were errors all
along──they just weren't caught before. I've run into a couple of these so
far, but there are probably more.

The one that gave me a little trouble converting to Windows 2.0 was
GetParent(NULL). It's not legal to pass a NULL window handle to GetParent,
but earlier versions of Windows accepted it and returned a NULL. Windows
2.0 (at least in the debugging version) will reject it.

The other problem occurred in Microsoft's CUBE demo program. This program
loads a binary resource into memory with LoadResource and then locks it
with GlobalLock. That's not kosher at all, but somehow it worked before.
The correct procedure is to use LockResource instead of GlobalLock.


New Documentation

The new programmer's documentation is one of the nicest features of
Windows 2.0. The manuals have been extensively revised, with excellent
tutorial discussions of such areas as window creation, painting, and
scrolling. Unlike the old Windows manuals we've all struggled with, the
new programmer's manual actually explains things. Newcomers to Windows
programming will now have a much easier time learning the ropes.

Besides containing new explanatory and overview material, the manual has
been reorganized to list all the functions and messages in alphabetical
order. No more scratching your head trying to figure out whether SetRect
is a GDI or User function.


TILER

Despite all the enhancements made to Windows 2.0, I really miss one
feature of the old version: tiled windows. As nice as the new overlapping
windows are, tiled windows can be very handy too. So, which will it be,
friends──New Windows or Windows Classic?

Since we're talking about software, not soft drinks, why not have both? To
this end, I wrote a program called TILER, which does just what its name
implies──it moves and resizes your windows into a tiled arrangement. TILER
runs as an icon and gives you two menu items to choose from: Tile Columns
and Tile Rows (see Figure 5☼). When you select either of these menu
options, TILER searches for windows that it can rearrange and then tiles
them. According to which option you choose, TILER will first try to place
your windows either in columns (side by side) or in rows (one above the
other). Figures 6 and 7☼ show the different ways TILER will rearrange
overlapping windows.

This demonstrates the greater versatility of Windows 2.0. Windows 1.x
allowed only a single tiling arrangement, with columns taking precedence.
There was no way you could arrange your windows in rows (see Figure 8☼). A
program such as TILER, which resizes the windows of other applications,
could not even be written under the old Windows, which strictly prohibited
an application from controlling the size of any tiled window, even its
own. Now an application can resize its own windows or those of other
applications, and no particular window arrangement is mandatory.

A good tiling algorithm can be fairly complex. Because of space
limitations, the version of TILER given in Figures 9, 10, 11, and 12
uses a very simplified algorithm that merely divides the screen into equally
sized rows or columns. In fact, TILER will handle no more than four
windows, simply leaving any additional windows where they were, hidden
underneath the tiled windows. Even with this limitation, TILER is fairly
useful. But you can download a more sophisticated version of TILER, which
allows more-flexible window placement, from Microsoft's DIAL service.


Initializing TILER

TILER's WinMain resembles the main programs of most Windows applications.
Yet, while most Windows applications allow several copies of themselves to
be run concurrently, this wouldn't be useful for TILER, so it just beeps
and exits if you try to run more than one instance of it.

TILER calls its Initialize function to create and display its icon. Then
it settles into a GetMessage/DispatchMessage loop, which pulls messages
from the application queue and routes them to TILER's window function,
TilerWndProc. As in most Windows applications, the real work consists of
processing these messages.

You may notice there's no TranslateMessage call in TILER's message loop.
This function is needed only when your application wants to use WM_CHAR
messages to get ASCII keyboard input. Since TILER does not require
WM_CHAR, it doesn't have to call TranslateMessage. Similarly, there's no
TranslateAccelerator call, since TILER doesn't use any keyboard
accelerators.

TILER's Initialize function registers its window class and creates its
window using the RegisterClass and CreateWindow functions found in nearly
every Windows application. It also adds Tile Columns and Tile Rows to
TILER's System menu. Although it's generally not a good idea for
applications to add items to their System menus, this is quite appropriate
for a program like TILER, which has no other menu. (Indeed, since TILER
runs as an icon, there would be no place to display another menu if it had
one.)

Initialize performs a little trick to determine how much of the screen the
tiled windows should take up. We don't want to use the full screen, since
we have to leave room at the bottom for icons, just as in the previous
version of Windows. So TILER first creates a normal (noniconic) but
invisible window and tells Windows to assign a default window size and
position. The bottom edge of this default window happens to be just where
we want the bottom of the tiled windows to be, so TILER saves that value
as the bottom coordinate of TileRect.

Finally, Initialize makes the window visible with the ShowWindow function
call. The SW_SHOWMINIMIZED parameter to ShowWindow converts the window
into an icon. (This parameter is the same as SHOW_ICONWINDOW in Windows
1.x.)


Take a Message

TILER's window function, TilerWndProc, handles only three messages
specially. It passes all others along to DefWindowProc, which performs
Windows' default message processing. This lets TilerWndProc concentrate on
just the things it needs to do.

The most important message for TILER is WM_SYSCOMMAND, since this is sent
when the user selects an item from the System menu, where TILER's two menu
items are located. TilerWndProc checks to see whether the message
originates from a TILER menu item. If so, it processes the message by
calling the TileWindows function. All other WM_SYSCOMMAND messages are
passed on to DefWindowProc so Windows can handle the other System menu
commands.

Another message useful to TILER is WM_QUERYOPEN. Windows sends this
message whenever the user tries to open the icon, that is, change it back
into an open window. Since TILER runs only as an icon, it replies to this
message by returning a 0, so Windows won't open it.

Finally, the WM_DESTROY message tells TILER that the user has selected the
Close item from the System menu. TilerWndProc then calls PostQuitMessage
to tell the main application loop to terminate the next time through.


Tiling Algorithm

TILER really gets down to business with the TileWindows function. First,
TileWindows calls CalcWindowRects, which loops through the active windows,
calculating new sizes and positions for them. CalcWindowRects is the
actual tiling algorithm. If there's just one window, it gets the entire
tiling area, of course. Two windows split the area in half, either
horizontally or vertically, depending on which menu item was selected. If
there are three windows, the first one gets half the tiling area, and the
second and third split the remaining half. Four windows get the four
corners of the tiling area. With more than four windows, TILER just tiles
the first four and leaves the remaining windows hidden underneath them.

In the more sophisticated version of TILER available on DIAL,
CalcWindowRects can handle more than four windows and is sensitive to how
the user had already sized them. For example, if the user makes the
topmost window larger, the enhanced TILER will shrink the other windows to
fit instead of arbitrarily splitting the screen in half (see Figure 13☼).

To keep things simple, TILER attempts to tile only top-level overlapping
windows that are resizable and are not minimized (iconic) or maximized
(zoomed). The IsTileable function determines whether a window fits these
criteria, and CalcWindowRects ignores the window if IsTileable returns the
value FALSE.

CalcWindowRects is much simpler than it would have been in Windows 1.x,
because it uses the slick new GetWindow function. Anyone who has suffered
through having to use the clumsy EnumWindows will appreciate GetWindow,
which lets you write an ordinary C loop to scan through the window list
instead of forcing you to use a "call back" function. Figure 14 makes it
clear just how much easier it is to use GetWindow instead of EnumWindows.

Once the window rectangles are calculated, the TileWindows function moves
the windows into place with the new SetWindowPos function. SetWindowPos
also sends the WM_MOVE and WM_SIZE messages to the windows to notify them
that they have been moved. The old MoveWindow function would have done the
job here, but SetWindowPos does two things at once: besides setting the
window positions, it reorders the window list, so the Alt-Tab and Alt-Esc
key combinations will scan through the tiled windows in a natural order.

TileWindows performs one other function here. It checks each window to see
whether the CS_HREDRAW or CS_VREDRAW bits are set. If they are, it
invalidates the window, causing it to be repainted. This is necessary
because some Windows applications──notably CLOCK──do not properly respond to
the WM_SIZE message but depend on those redraw flags to generate a
WM_PAINT message.

Programs like CLOCK that change their displays to fit the window size
should really respond to the WM_SIZE message and recalculate their
displays at that point. But CLOCK doesn't. If you feel like experimenting,
remove the InvalidateRect call from TileWindows, and watch what happens
when CLOCK is one of the windows you're tiling. You'll get some strange-
looking results.


Building TILER

To compile and link TILER, you will need the Microsoft C Compiler, Version
4.0 or 5.0, and the Windows Software Development Kit, Version 2.0. Put the
TILER.C source code, TILER.RC resource file, TILER.DEF module definition
file, and TILER make-file (see Figures 9, 10, 11, and 12) in a directory,
and make up a TILER.ICO icon file with the Icon Editor. Then issue the
command MAKE TILER to build TILER.EXE.


Figure 1:  Editing Key Assignments

Function      Assignment

Undo          Alt+BkSp
Cut           Shift+Del
Copy          Ctrl+Ins
Paste         Shift+Ins
Clear         Del


Figure 2:  This .RC file establishes the File and Edit menus and their
           accelerators.

MyApp MENU
BEGIN
 POPUP "&File"
    BEGIN
        MENUITEM "&New"                     , CMD_NEW
        MENUITEM "&Open..."                 , CMD_OPEN
        MENUITEM SEPARATOR
        MENUITEM "&Save"                    , CMD_SAVE
        MENUITEM "Save &As..."              , CMD_SAVEAS
        MENUITEM SEPARATOR
        MENUITEM "E&xit"                    , CMD_EXIT
        MENUITEM "A&bout My Application..." , CMD_ABOUT
    END

    POPUP "&Edit"
    BEGIN
        MENUITEM "&Undo\tAlt+BkSp"          , CMD_UNDO
        MENUITEM SEPARATOR
        MENUITEM "Cu&t\tShift+Del"          , CMD_CUT
        MENUITEM "&Copy\tCtrl+Ins"          , CMD_COPY
        MENUITEM "&Paste\tShift+Ins"        , CMD_PASTE
        MENUITEM "C&lear\tDel"              , CMD_CLEAR
    END

    MENUITEM "\a&Help"                      , CMD_HELP, HELP

END

MyApp ACCELERATORS
BEGIN
    VK_DELETE,  CMD_CUT,        VIRTKEY
    VK_DELETE,  CMD_CLEAR,      VIRTKEY, SHIFT
    VK_INSERT,  CMD_COPY,       VIRTKEY, CONTROL
    VK_INSERT,  CMD_PASTE,      VIRTKEY, SHIFT
    VK_BACK,    CMD_UNDO,       VIRTKEY, ALT
END


Figure 3:  Hook Functions Installed by SetWindowsHook

╓┌───────────────────┌───────────────────────────────────────────────────────╖
Hook Function       Description

WH_MSGFILTER        Message filter. Called when a dialog box, message box,
                    or menu has received a message but before it has
                    processed the message.

WH_KEYBOARD         Keyboard filter. Called when the application calls
                    GetMessage or PeekMessage and there is any keyboard
                    input pending.

WH_GETMESSAGE       GetMessage filter. Called from inside the GetMessage
                    function before it returns the message to the
                    application. (Debugging version of Windows only.)

WH_CALLWNDPROC      Window function filter. Called whenever any message is
                    sent to a window function. (Debugging version of
                    Windows only.)

Hook Function       Description

WH_JOURNALRECORD    Journal recording. Called on every event message
                    (keyboard, mouse, or timer message) for any
                    application.

WH_JOURNALPLAYBACK  Journal playback. Plays back messages recorded by
                    WH_JOURNALRECORD. With these two hook functions, you
                    could write a "ProMouseKey" program for Windows.


Figure 4:  New Messages in Windows 2.0

╓┌───────────────────┌───────────────────────────────────────────────────────╖
New Messages        Description

BM_SETSTYLE         Alters the style of a button. With this message, you
                    can change a button control from its current style to
                    any other style. The main purpose of the message is to
                    allow the new user interface trick where any push
                    button you tab to becomes the default push button.
New Messages        Description
                    button you tab to becomes the default push button.

BN_DOUBLECLICKED    Notifies the parent window when the user double-clicks
                    a button control.

EM_LINEFROMCHAR     Returns the line number for a given character position
                    in an edit control.

EM_SETWORDBREAK     Lets you specify a word-break function to replace the
                    default word-break function (which breaks on a blank).

EN_UPDATE           Just like EN_CHANGE, except that it is sent before the
                    edit control displays the changed text instead of
                    after. The text has already been formatted, so you
                    can issue an EM_LINELENGTH or EM_GETLINECOUNT and
                    resize the edit control accordingly. This makes it
                    easy to create an edit control that expands and
                    shrinks to fit its text. With this message, I was
                    able to replace about 50 lines of very tricky code in
                    one of my programs with three lines of utterly simple
New Messages        Description
                    one of my programs with three lines of utterly simple
                    code.

WM_CHILDACTIVATE    Sent to a child window's parent when the SetWindowPos
                    function moves the child.

WM_GETMINMAXINFO    By default, when you Maximize a window, it fills the
                    full screen. By replying to this message, you can
                    specify a location and size that your window will
                    take on when it's Maximized. You can also specify the
                    minimum and maximum tracking size (for when the user
                    sizes the window).

WM_NCHITTEST        Returns new values to indicate when the mouse is in
                    one of the thick borders or in the Minimize or
                    Maximize icons.

WM_SHOWWINDOW       Several new options to support the new Minimize,
                    Maximize, and Restore commands.

New Messages        Description

WM_SYSCOMMAND       New option to support the Restore command.


Figure 9:  Make-File for TILER.EXE

tiler.obj:  tiler.c
    msc -AS -Gcsw -Ox -u -W3 -Zdp $*;

tiler.res:  tiler.rc  tiler.ico
    rc -r tiler.rc

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


Figure 10:  DEF File for TILER.EXE

TILER.DEF

NAME         Tiler

DESCRIPTION  'Windows Tiler by Michael Geary'

STUB         'WINSTUB.EXE'

CODE         MOVEABLE
DATA         MOVEABLE MULTIPLE

HEAPSIZE     1024

STACKSIZE    4096

EXPORTS


Figure 11:  RC File for TILER.EXE

TILER.RC

/* Tiler.RC - resource file for TILER.EXE */

#include <style.h>

Tiler!  ICON    tiler.ico


Figure 12:  Code for TILER.C

TILER.C

/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - *\
 *  Tiler.c                                                        *
 *  Windows 2.0 Tiling Utility                                     *
 *  Written by Michael Geary                                       *
\* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

#include <windows.h>

/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

#define MAXINT      32767

/* Menu command definitions */

#define CMD_TILECOLS    1
#define CMD_TILEROWS    2


/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

typedef struct {
    HWND    hWnd;
    RECT    rect;
} WINDOW;

WINDOW      Window[4];        /* Window info for each tiled window */
int         nWindows;         /* How many windows we will tile */

HANDLE      hInstance;        /* Our instance handle */
int         hWndTiler;        /* hWnd of our icon */
RECT        TileRect;         /* Overall tiling rectangle */

char        szClass[] = "Tiler!";         /* Our window class name */
char        szTitle[] = "Tiler";          /* Our window title */
char        szTileCols[] = "&Tile Columns";
char        szTileRows[] = "Tile &Rows";

/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */


/*  Declare full templates for all our functions.  This gives us
 *  strong type checking on our functions.
 */

void                CalcWindowRects( BOOL );
BOOL                Initialize( void );
BOOL                IsTileable( HWND );
long    FAR PASCAL  TilerWndProc( HWND, unsigned, WORD, LONG );
void                TileWindows( BOOL );
int         PASCAL  WinMain( HANDLE, HANDLE, LPSTR, int );

/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */


/*  Calculate window rectangles for the four topmost tileable windows
 *  and set up the Window array for them.  This is a simple-minded
 *  tiling algorithm that simply divides the tiling area into equal
 *  rows and columns.
 */

void CalcWindowRects( bColumns )
    BOOL        bColumns;
{
    HWND        hWnd;
    int         n;

    n = 0;
    for(
        hWnd = GetWindow( hWndTiler, GW_HWNDFIRST );
        hWnd;
        hWnd = GetWindow( hWnd, GW_HWNDNEXT )
    ) {
        if( ! IsTileable( hWnd ) )  continue;

        Window[n].hWnd = hWnd;
        CopyRect( &Window[n].rect, &TileRect );

        switch( n ) {
            case 0:
                break;
            case 1:
                if( bColumns ) {
                    Window[0].rect.right = TileRect.right / 2;
                    Window[1].rect.left = Window[0].rect.right - 1;
                } else {
                    Window[0].rect.bottom = TileRect.bottom / 2;
                    Window[1].rect.top = Window[0].rect.bottom - 1;
                }
                break;
            case 2:
                if( bColumns ) {
                    Window[2].rect.left = Window[1].rect.left;
                    Window[1].rect.bottom = TileRect.bottom / 2;
                    Window[2].rect.top = Window[1].rect.bottom - 1;
                } else {
                    Window[2].rect.top = Window[1].rect.top;
                    Window[1].rect.right = TileRect.right / 2;
                    Window[2].rect.left = Window[1].rect.right - 1;
                }
                break;
            case 3:
                if( bColumns ) {
                    Window[3].rect.right = Window[0].rect.right;
                    Window[0].rect.bottom = TileRect.bottom / 2;
                    Window[3].rect.top = Window[0].rect.bottom - 1;
                } else {
                    Window[3].rect.bottom = Window[0].rect.bottom;
                    Window[0].rect.right = TileRect.right / 2;
                    Window[3].rect.left = Window[0].rect.right - 1;
                }
                break;
        }
        if( ++n == 4 )  break;
    }
    nWindows = n;
}

/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */


/*  Initialize TILER.  Assumes a single instance.
 *  Returns TRUE if initialization succeeded, FALSE if failed.
 */

BOOL Initialize()
{
    WNDCLASS    Class;        /* Class structure for RegisterClass */
    HMENU       hMenu;        /* Menu handle of system menu */

    /* Register our window class */
    Class.style          = 0;
    Class.cbClsExtra     = 0;
    Class.cbWndExtra     = 0;
    Class.lpfnWndProc    = TilerWndProc;
    Class.hInstance      = hInstance;
    Class.hIcon          = LoadIcon( hInstance, szClass );
    Class.hCursor        = LoadCursor( NULL, IDC_ARROW );
    Class.hbrBackground  = COLOR_WINDOW + 1;
    Class.lpszMenuName   = NULL;
    Class.lpszClassName  = szClass;

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

    /* Create our window but don't iconize it yet */
    hWndTiler = CreateWindow(
        szClass, szTitle,
        WS_OVERLAPPED | WS_SYSMENU,
        CW_USEDEFAULT, 0,
        CW_USEDEFAULT, 0,
        NULL, NULL, hInstance, NULL
    );
    if( ! hWndTiler )  return FALSE;

    /* Since we took the default size, the bottom of our window is
     * the base Y coordinate for tiling */
    GetWindowRect( hWndTiler, &TileRect );
    TileRect.top = TileRect.left = -1;
    TileRect.right = GetSystemMetrics( SM_CXSCREEN ) + 1;

    /* Add our menu items to the System (Control) menu */
    hMenu = GetSystemMenu( hWndTiler, FALSE);
    ChangeMenu( hMenu, 0, NULL, MAXINT, MF_APPEND | MF_SEPARATOR );
    ChangeMenu( hMenu, 0, szTileCols, CMD_TILECOLS,
                MF_APPEND | MF_STRING );
    ChangeMenu( hMenu, 0, szTileRows, CMD_TILEROWS,
                MF_APPEND | MF_STRING );

    /* Now display our window as an icon */
    ShowWindow( hWndTiler, SW_SHOWMINIMIZED );

    return TRUE;
}

/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */


/*  Tells whether a window can be tiled, returns TRUE if so.
 *  We will tile only top level, resizable windows that are not
 *  minimized and not maximized.
 */

BOOL IsTileable( hWnd )
    HWND        hWnd;
{
    DWORD       dwStyle;

    dwStyle = GetWindowLong( hWnd, GWL_STYLE );
    return(
        ! ( dwStyle & ( WS_POPUP | WS_MINIMIZE | WS_MAXIMIZE ) )  &&
          ( dwStyle & WS_SIZEBOX )  &&
          ( dwStyle & WS_VISIBLE )
    );
}

/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */


/*  Tiler's window function.
 */

long FAR PASCAL TilerWndProc( hWnd, wMsg, wParam, lParam )
    HWND        hWnd;            /* Window handle */
    unsigned    wMsg;            /* Message number */
    WORD        wParam;          /* Word parameter for the message */
    LONG        lParam;          /* Long parameter for the message */
{
    RECT        rect;            /* A rectangle */

    switch( wMsg ) {

    /* Destroy-window message - time to quit the application */
        case WM_DESTROY:
            PostQuitMessage( 0 );
            return 0L;

    /* Query open icon message - don't allow icon to be opened! */
        case WM_QUERYOPEN:
            return 0L;

    /* System menu command message - process the command if ours */
        case WM_SYSCOMMAND:
            switch( wParam ) {
                case CMD_TILECOLS:
                    TileWindows( TRUE );
                    return 0L;
                case CMD_TILEROWS:
                    TileWindows( FALSE );
                    return 0L;
                default:
                    break;
            }
            break;

    /* For all other messages, we pass them on to DefWindowProc */
        default:
            break;
    }
    return DefWindowProc( hWnd, wMsg, wParam, lParam );
}

/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

/*  This function actually tiles the windows. First, it calls
 *  CalcWindowRects to determine the window positions. Then, it
 *  loops through the windows and moves them into place.
 */

void TileWindows( bColumns )
    BOOL        bColumns;     /* TRUE = tile columns; FALSE = rows */
{
    int         n;
    HWND        hWnd = NULL;

    CalcWindowRects( bColumns );  /* Assign window rectangles */

    if( nWindows == 0 ) {
        MessageBox(
            hWndTiler,
            "There are no windows that can be tiled.",
            szTitle,
            MB_OK | MB_ICONEXCLAMATION
        );
        return;
    }

    /* Move, size, and reorder windows */
    for( n = 0;  n < nWindows;  ++n ) {
        SetWindowPos(
            Window[n].hWnd,
            hWnd,
            Window[n].rect.left,
            Window[n].rect.top,
            Window[n].rect.right  - Window[n].rect.left,
            Window[n].rect.bottom - Window[n].rect.top,
            SWP_NOACTIVATE
        );
        hWnd = Window[n].hWnd;
        if( GetClassWord( hWnd, GCW_STYLE ) &
                        ( CS_HREDRAW | CS_VREDRAW ) )
            /* Make sure it's redrawn */
            InvalidateRect( hWnd, NULL, TRUE );
    }
}

/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

/*  Application main program. */

int PASCAL WinMain( hInst, hPrevInst, lpszCmdLine, nCmdShow )

    /* Our instance handle */
    HANDLE      hInst;
    /* Previous instance of this application */
    HANDLE      hPrevInst;
    /* Pointer to any command line params */
    LPSTR       lpszCmdLine;
    /* Parameter to use for first ShowWindow */
    int         nCmdShow;
{
    MSG         msg;                /* Message structure */

    /* Allow only a single instance */
    if( hPrevInst ) {
        MessageBeep( 0 );
        return 0;
    }

    /* Save our instance handle in static variable */
    hInstance = hInst;

    /* Initialize application, quit if any errors */
    if( ! Initialize() )  return FALSE;

    /* Main message processing loop */
    while( GetMessage( &msg, NULL, 0, 0 ) ) {
        DispatchMessage( &msg );
    }

    return msg.wParam;
}


Figure 14:  EnumWindows vs. GetWindow

/* UsingEnumWindows to loop through the windows */

void LoopThroughWindows()
{
    FARPROC lpEnumProc;

    lpEnumProc = MakeProcInstance( MyEnumProc, hInstance );
    EnumWindows( lpEnumProc );
    FreeProcInstance( MyEnumProc, 0L );
}

BOOL MyEnumProc( hWnd, lParam )
    HWND    hWnd;
    LONG    lParam;
{
    /* do something with hWnd here */
}

And don't forget to EXPORT MyEnumProc, or Windows will crash!

Now, with GetWindow:

void LoopThroughWindows()
{
    HWND    hWnd;

    for(
        hWnd = GetWindow( hWndMyWindow, GW_HWNDFIRST );
        hWnd;
        hWnd = GetWindow( hWnd, GW_HWNDNEXT ))
    {
        /* do something with hWnd here */
    }
}


───────────────────────────────────────────────────────────────────────────
The Beta Version of Windows 2.0: An Update
───────────────────────────────────────────────────────────────────────────

Just at press time, a new release of Windows 2.0 has come along that forces
me to (happily) eat a few of my words. One of the joys of working with
alpha- and beta-test software is that things change right under your feet as
you're writing about them.

In this case, I don't mind, because Microsoft has put in the GlobalNotify
function. I call it GlobalNifty, because it lets you implement swappable
data for your application. You are able to mark global memory as
discardable, but if you set the GMEM_GNOTIFY flag on it, Windows will call
your notification function when it wants to discard a memory block. You can
then write it to disk before it gets discarded, which is much cleaner than
the kludges we had to use before.

There's also a LocalShrink function, which compacts your application's local
heap and shrinks down your data segment to match. Before, if your local heap
grew past the initial allocation determined by the .DEF file, your data
segment grew as needed, but it never shrank back again; LocalShrink allows
you to do that.

RegisterWindowDestroy is an interesting new function. It's easy to find out
when one of your own windows is destroyed, since it receives a WM_DESTROY
message. You may want to find out when another application's window gets
destroyed, such as in an add-on program that works with an existing
application and should exit when that application exits. You can do this
with a RegisterWindowDestroy call, which sets──actually increments──a
destroy-notification flag on a window. Then, when that window is destroyed,
Windows sends a WM_DESTROYOTHERWINDOW to all top-level windows to let
everyone know about the window being destroyed.

For text output, the new ExtTextOut function is like a TextOut with two
added parameters: a rectangle that can be used for background erasing, text
clipping, or both, and an optional array of character horizontal spacing
values. This array lets you set your own spacing, character by character.
You can find out the default horizontal spacing for a font with the
GetCharWidth function, which loads up an integer array of spacing values for
a range of characters in a font.

GetDCOrg gives you the offset from screen to client coordinates for a
display context. GetUpdateRgn copies a window's update region into an actual
hRgn that you can then manipulate. IsZoomed tells you if a window is zoomed.
(Zoomed is still zoomed, even if they do call it Maximized.) Finally,
GetNumTasks returns the number of tasks that are currently running.

One of the undocumented SetWindowsHook options I mentioned is now
documented: WH_SYSMSGFILTER. This is the same as WH_MSGFILTER, except that
it gives you the DialogBox/MessageBox/menu messages for all windows, not
just your own window. A warning to those who would use the hook functions:
when Windows calls your hook function, your DS register is set up for your
data segment, but you're running on the stack of whatever application
triggered the hook. Therefore, you will run into the infamous DS != SS
problem, which means that you will have to compile that part of your
code with the -Aw switch and cannot call certain C library functions. The
Windows documentation lists which C run-time functions will and won't work
in the DS != SS environment. This affects all the Windows hooks except for
WH_MSGFILTER and WH_KEYBOARD.

Windows 2.0 directly supports LIM/EMS and EEMS memory. These bank-switched
memory schemes provide one or more "windows" (not related to Windows'
windows) within the 1Mb real-mode address space. Any area of memory on the
bank-switched card can be mapped into these memory windows. Windows 2.0
supports this by assigning each application its own bank(s) of memory. A
single application doesn't get any more memory than it used to, but you can
run more applications concurrently.

The larger the banked memory window(s), the better. Windows will keep
application code and data in banked memory if possible, or else will put as
much code there as will fit. EEMS memory can do a much better job here than
straight LIM/EMS memory, because of its larger bank windows.

For the most part, the memory banking is transparent to applications, but
it's important to note that the banking is done on an application-by-
application basis, and that code and data belonging to other applications
can be banked out. This means, for example, that you can't give another
application a global handle to your default data segment, expecting the
other app to do a GlobalLock on it. Nor can you attempt to get a window
function address from another app and call it directly, bypassing
SendMessage. That never was a good idea, but now it's a sure way to crash.

The only methods for intertask communication that will work under the
EMS/EEMS environment are the clipboard, a shared library's data segment,
and the Dynamic Data Exchange (DDE) protocol. Memory allocated with the
GMEM_DDESHARE global memory flag is treated specially, so any app can get to
it. You can also force global memory to be allocated from the shared area
with GMEM_LOWER. Avoid this if possible, since the shared memory area is a
scarce resource.

There is one new Windows function for EMS support──LimitEMSPages. This
simply lets you set a limit on the amount of EMS memory that Windows will
assign to your application. At the cost of some extra disk access (because
all your code won't fit in memory), you can reduce the total memory used by
your application. You can still explicitly allocate more memory with
GlobalAlloc calls, regardless of the limit set with LimitEMSPages.

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

Keeping Up With the Real World: Speedy Serial I/O Processing

───────────────────────────────────────────────────────────────────────────
Also see the following related articles:
  MS-DOS(R) Comm Port Syntax Problems
  Status of the Transmit Shift Register Versus the Transmitter Holding
  XON/XOFF
  RS-232C: A Standard Problem
  Sample Code Fragments for TSRCOMM.ASM
  Combining Interrupts and Polling: An Adventure in Programming
───────────────────────────────────────────────────────────────────────────

Ross M. Greenberg☼

At first glance, writing a serial communications program for an MS-DOS(R)
system looks fairly straightforward. Most high-level languages can interface
easily with the BIOS, and the BIOS, by definition, knows how to interface
with the hardware. After you've designed your program with pop-up windows,
pull-down help screens, fancy sounds, and flashy colors, you'll probably
build a stub to test your ideas. You may be quite surprised to learn that
you're limited to under 1200 baud.

Turning to the back of the nearest BIOS Technical Reference manual, you'll
soon discover how few of the services that you consider basic are provided:
there is no XON/XOFF processing, little hardware handshaking is performed,
the baud rates that are allowed seem arbitrarily low, and, most important of
all, there is no true asynchronous interrupt processing.

Whether you are writing a modem communications program or just attempting to
get proper XON/XOFF processing of your serial printer in your favorite word
processing program, some enhancement of the system services available under
MS-DOS in the handling of serial communications is necessary. This article
examines the writing of a replacement handler for Interrupt 0x14, the serial
communications interrupt.

My design goals arose from real-life needs I encountered when writing a
terminate-and-stay-resident (TSR) communications program. These goals
included:

  ■  asynchronous interrupt-driven I/O of baud rates up to 38,400
  ■  the  ability to handle COM ports 1 through 4
  ■  XON/XOFF flow control, on both incoming and outgoing flow
  ■  proper error detection of parity, overrun, and other errors
  ■  transparency for existing software
  ■  extensibility as requirements changed


Getting Started

Due to the inherent limitations of the BIOS serial communications service
routines, MS-DOS must be bypassed to address the hardware directly. The
moment you interface directly with the hardware within your computer, you
lose that most precious of commodities, transportability between machines.
However, compatibility is less of an issue now than it was in the past, and
these routines work on all IBM-compatible machines, as well as on many
clones.

Although the real-world applications for the BIOS routines soon outstrip
their abilities, the programmers' interface to these routines should be
preserved: they are more or less a standard, and without them existing
programs would not work properly. They permit you to initialize the comm
port, send or receive a byte to or from the comm port, and return the status
of the comm port. These interfaces are through Interrupt 0x14. (For the rest
of this article, I'll refer to software interrupts as "calls.") Maintaining
compatibility with the old interface while expanding above and beyond it led
to some interesting design decisions.


Precursor

Writing code to stay resident after termination can be tricky, and adding in
asynchronous interrupt processing can make things even trickier. When your
code is called, the only things you know for sure are the code segment (CS)
and instruction pointer (IP) of the CPU. Registers, including the Data
Segment (DS) register, are pointing to unknown values. They must all be
restored to their original values when you return from the interrupt service
routine, with the CS and IP, as well as the flags of the CPU, restored
immediately upon execution of the final IRET instruction.

There are a variety of programming practices to get around this
inconvenience. One method is addressing all data off the code segment using
what is called a segment override in the assembler code itself. This,
however, makes the code a little bigger and more cumbersome and requires a
couple of extra clock cycles for any instruction that uses segment
overrides, causing the code to execute more slowly.

The method used in this program differs from that approach: immediately upon
the service of an interrupt, the data segment is saved on the stack, and the
code segment itself is used for data addressing. It's faster in the long
run, both in the design cycle and in the actual execution of the code.
Although there are portions of the code that deal directly with the hardware
attached to the bus, whenever possible I've used transportable features of
MS-DOS, including the MS-DOS calls for intercepting and taking over various
BIOS interrupts.


Overall Structure

The code is structured logically into three areas. The invocation code is
run only once; it sets up the interrupt structure and initializes data
areas. The second component handles the actual asynchronous interrupts
themselves, properly handling the two different types of interrupts: receive
and transmit interrupts. Error interrupts are not generated in this scheme.
Also, if a hardware interrupt is generated and does not come from the serial
device itself, the interrupt is handed off to the original interrupt service
routine, permitting devices that use the same IRQ (Interrupt Request) to be
processed properly.

Finally, the third portion of the code is a replacement for the original
BIOS Interrupt 0x14. It allows for a great deal of extensibility to be
added, though at the price of having some additional programming overhead.


Saving Parameters

One of the requirements is the ability to remove this program from memory
when desired. This implies that the original parameters of the hardware
must be saved and then restored upon program termination. Therefore, a
structure for each comm port has been designed to hold all relevant initial
data. In addition, this structure contains pointers to the beginning and end
of the transmit and receive interrupt buffers. All of this data must be
initialized properly, with interrupts turned off, before the actual BIOS
interrupt vector is taken over and made to point to our own interrupt
service routines.

Once all of the parameters are saved and interrupt services are enabled, the
preamble code executes a DOS Terminate and Stay Resident call, reserving
memory before exiting to COMMAND.COM.


Replacing Services

Interrupt 0x14 is the entry point into the BIOS service routines for serial
communications. By setting register half AH equal to a given function
number and register DX to the desired comm port (starting with COM1 equal to
zero), comm software then generates a software interrupt to invoke the given
service (see Figure 1). I've extended the BIOS services with a new call,
AH=4, and used the BX register to allow for some of the special operations
that the new comm driver permits (see Figure 2).

Although the interface to the user program looks the same, there are
substantial internal differences. When a call is made to initialize the comm
port, character ready interrupts are enabled, buffer pointers are reset, and
then the actual hardware is reset to the proper baud rates, parity, word
length, etc. When a call is made to return the status of the comm port, the
comm port status is read and processed to reflect the combined status of
both the comm port and buffers. This status can be further monitored by
earlier calls with AH=4. However, since multiple interrupts can be caused by
errors for each "get status" call you make, you may lose some of the actual
error information. The method that most software employs to deal with error
status is preserved, however: when an error is apparent, all errors are set
in the status word if possible.

When reading a character from the comm port, whether the character you're
receiving is directly from the communications port or from the interrupt
buffer will be transparent. One particularly interesting aspect of the code
is its ability to detect an error, store the apparent character, and return
an error condition in the high bit of AH.

The code is also able to set a variable to specify a looping constant. This
permits you to wait in the "get a character" routine until a character
appears in the receive buffer or until the specified number of loops have
been used up. As a replacement to the original BIOS routines, this is far
superior to the constant usually specified. Again, you use the AH=4
extension to the serial communications command set to set this constant. If
you want, you can set this looping constant to 1/18 of a second and use the
hardware timer tick interrupt as a countdown timer for error status.

When transmitting a character, there are two options (set with AH=4):
transmit the characters with the use of interrupts and the transmit buffer
or attempt to output each character one at a time. The interrupt-driven
transmit method is much faster, of course, but it has certain disadvantages.


Processing Interrupts

The most important feature of the code is its ability to properly handle
interrupts that occur very frequently──as often as two interrupts for each
character transmitted or received. Some background information on how
interrupts are generated and how they are handled by the hardware should
help you to understand how this code works.

The chip at the heart of the serial communications board in your MS-DOS
machine is an 8250. The 8250 requires eight contiguous I/O ports, which are
used to read and write serial data and to read and set various parameters
(see Figure 3).

One of these parameters determines under what conditions the 8250 should
generate an interrupt signal on the bus. Depending on how the parameter is
set, an interrupt can be generated when a new character has been received,
when the transmit buffer is empty and ready for a new character, or when an
error condition has been determined. Initially, the code is set to generate
an interrupt only on error conditions or when a new character is ready to be
placed in the buffer.

However, when a character is to be transmitted and a check of the status of
the 8250 shows it is busy, the possibility of queuing characters up in a
transmit buffer, which transmits the characters asynchronously to any other
process, becomes very attractive. This is made possible through one of the
settings of the BIOS call with AH=4, which allows you to determine if a
character should be queued into the transmit buffer. Otherwise, the code
will be set to continue testing for a clear transmit buffer or a time-out
and will not return until either condition has been satisfied.

When an interrupt is received, it is routed through one of two different
interrupt vectors, namely the interrupt handler of COM1 or COM2. They merely
set the correct header and then jump to the actual interrupt handler. The
interrupt handler first determines which kind of interrupt was generated and
then jumps to the appropriate routine. There is a priority to each
interrupt, with error conditions having the highest priority, followed first
by data becoming apparent at the serial port, then by the transmit buffer
being empty, and finally by a change in other portions of the modem status.
Also, if this routine determines that the interrupt was not generated by the
8250, it will revector the interrupt to the original interrupt service
routine.

If the interrupt is determined to be the result of a character being
received, the next character is read from the physical hardware port and
stored in a circular buffer. If the circular buffer is more than 80 percent
full and XON/XOFF processing is turned on, then an XOFF character is
sent──if one hasn't already been sent. Even at high baud rates, the
remaining 20 percent of the buffer should be sufficient for the remote end
to notice the XOFF and cease sending characters before the receive buffer
runs out of room.

If the remote end does not stop sending characters in response to the XOFF,
or if XON/XOFF processing was not turned on, characters that were received
while the buffer was full will be discarded, and a BELL character will be
sent to the remote machine. If the flag is set to indicate that an XOFF
character was sent due to a high level of data, then an XON is sent when the
amount of space left in the buffer increases to some predetermined level
(set as an equate, all XON/XOFF high- and low-water marks can be modifed
easily). This should allow for processing of enough characters to prevent
the remote end from timing out.

Error conditions are generated by a fault in the incoming stream of
characters, which is detected by the 8250, and include such errors as an
overrun (the character in the transmit buffer was not read before the next
character arrived), parity (even, off, mark, or space parity was set, and
the parity of the incoming character did not match what was expected), or
framing error (a stop bit was not detected by the hardware when one was
expected──usually indicative of wrong baud-rate errors).

For each character received, both the error flag and error status of the
chip are read, if the appropriate flag is set with AH=4. If an error
condition is determined to exist, a bit is set in the error array.
Subsequent reads of the character will return the actual character read
(which may not be accurate), along with an error condition status set in AH
in the same way that the current BIOS calls will set an error condition. For
space considerations, the actual cause of the error condition is not saved,
and the return code is set to indicate that all errors were apparent──a
brute-force method that saves as much as 4 bits for each byte in your
receive buffer.

Only one of these errors requires any special handling: that is the
recognition of a Break having been generated from the remote end of a serial
connection. By an appropriate setting of the call extensions with AH=4, this
can either be considered an error condition or can safely be ignored.

When a Transmit Buffer Empty interrupt is processed, a quick check is made
to determine whether any outstanding characters to be transmitted are in the
transmit buffer. If there are no outstanding transmit requests, transmit
interrupts are turned off, the interrupt is reset, and control is returned
to the interrupted program. If there are outstanding characters to be
transmitted, the character at the head of the circular transmit buffer is
sent out the appropriate port; pointers, counters, and interrupts are reset;
and control is returned to the interrupted program by means of the IRET
instruction. This approach generates an additional interrupt after the final
character is transmitted, but this is an acceptable trade-off for the
advantage that you get from transmit interrupt processing.


Programming the 8259

When the 8250 generates an interrupt, it is, in fact, only raising a signal
on the interrupt line of the bus. Attached directly to these lines is the
interrupt controller on the motherboard, the 8259. This chip is the
interface between all interrupt generators and the CPU and is a rather
intelligent and programmable device. The 8259 distinguishes which interrupt
has been raised and then does some internal processing to determine what
actions it should take.

The concept of interrupts is a powerful one, and the 8259 expands on this
power, allowing the programmer to rearrange certain parameters so as to
practically rewire the hardware in his or her computer. Unfortunately, much
of the power of the interrupt system is lost in IBM-compatible machines in
which the ROM BIOS programs the 8259 and causes it to be reset in a less-
than-polite way.

One of the rules of the interrupt system is that interrupts usually have to
be reset. Upon receipt of an interrupt on the bus, the basic architecture
of the 8259 sets bits on an internal register to correspond to each
interrupt. Each bit is usually prioritized so that the highest priority
interrupt (IRQ 0) is bit 0, the next highest is bit 1 (IRQ 1), and so on.
This priority can be modified as desired and is one of the most powerful
features of the Interrupt Controller Chip.

Once an interrupt has been received and entered into this register, the
controller determines if the CPU is already handling an interrupt of equal
or higher priority. If it is not, and if the CPU has allowed interrupts at
all with an STI or Enable Interrupts instruction, a CPU interrupt is
generated, and the actual interrupt vector (0-7) is placed on the data bus.
Immediately upon receipt of an interrupt, the CPU turns interrupts off
(which disables the 8259 from raising further interrupts), saves the flags
and the current instruction address, and jumps to the appropriate vectored
address as set in the interrupt table. Until the interrupt service routine
(ISR) executes an STI or an IRET, which resets the flags from the stack,
interrupts are effectively disabled. Generally, the flag word on the stack
will have interrupts enabled, since they had to be enabled in order for the
hardware interrupt to be processed in the first place.

Even when the IRET or the STI instruction clears the way for the CPU to
process interrupts, the 8259 will not generate any interrupts of lower or
equal priority until it is told to do so by an End of Interrupt (EOI)
instruction.

The 8259 allows for a number of different End of Interrupt instructions to
be sent to it. Among these instruction types is the ability to reset a
specific interrupt or to automatically allow the 8259 to reset the interrupt
as soon as it is acknowledged by the CPU. The easiest one to implement is a
nonspecific End of Interrupt; this tells the 8259 to reset the highest
priority interrupt, which is usually the one that caused the last interrupt
to be generated.

However, since the concept of "highest priority level" can be reset from
inside an ISR, generating a nonspecific EOI would cause the highest
outstanding interrupt to be reset, which may or may not have been the
interrupt currently being processed. If an interrupt in the 8259 is reset,
it is as if that interrupt had never existed, and an important hardware
interrupt could be missed. So, using nonspecific EOIs effectively disables
many useful features of the 8259.

Unfortunately, each and every EOI instruction in the IBM(R) ROM BIOS (and in
most of the IBM-compatible machines as well) is a nonspecific one, making it
difficult to use some of the power of the 8259. COM2 will almost always have
priority over COM1, and many interesting features that you may wish to take
advantage of in the 8259 are off-limits, unless you play a few programming
tricks.

Interrupts are reenabled with the STI instruction as soon as possible after
interrupt processing has begun. And, due to the problems outlined above,
nonspecific EOIs are generated at the end of each interrupt. Why was the EOI
not generated earlier in the code? Once interrupts are reenabled with the
STI instruction, only interrupts of a higher priority than the interrupt
currently being processed will be acted upon. Typically, these interrupts
are acted upon and returned from rapidly, and while they're being acted on,
further interrupts of a lower priority will only be queued while waiting for
an EOI. This is safe, in keeping with limited stack space. However, if an
EOI is generated within the interrupt routine, and another interrupt routine
takes over for some undetermined time, it is possible to lose the character
ready or transmit buffer empty interrupt entirely. By using the belt-and-
suspenders approach of reenabling interrupts immediately upon the start of
interrupt processing, disabling interrupts before generating the EOI, and
then executing the IRET instruction, you won't have to worry about the
system crashing and the snake eating its own tail.


Next Version

What might the next version of this program do that this version doesn't?
Perhaps being able to turn on and off an internal Xmodem protocol would be
helpful, enabling the protocol layer of file transfers to be transparent.
Once we get started with that, there are several protocols we can start
playing with: Ymodem and Zmodem, X.25, UUCP, and maybe even some of the
synchronous protocols, which usually require an external clock. Now, if we
add in device driver I/O and a little bit of modem-dialing ability, the
possibility of having the disk drive of a remote machine be considered local
isn't all that farfetched, is it?


Figure 1:  The Original Interrupt 14 Calling Sequence

Set AH  to one of these values, and DX to 0 for COM1 or 1 for
COM2. The requested value is either sent or received in AL.

AH = 0      Port Initialization
 AL = Parameters to Set, AH will return status as in AH = 3

     Bits 7   6   5  │    4  3    │       2        │   1   0
      Baud Rate      │ Parity     │ No. of Stopbits│ Word Length
      000 -  110     │ 00 -  None │ 0 - 1 Stopbit  │ 00 - 5 Bits
      001 -  150     │ 01 -  Odd  │ 1 - 2 Stopbits │ 01 - 6 Bits
      010 -  300     │ 10 -  None │                │ 10 - 7 Bits
      011 -  600     │ 11 -  Even │                │ 11 - 8 Bits
      100 - 1200     │            │                │
      101 - 2400     │            │                │
      110 - 4800     │            │                │
      111 - 9600     │            │                │

AH = 1      Send Character
  AL = Character to Send, AH will return status as in AH = 3
       (Bit 7 set indicates unable to send character)

AH = 2      Get a Character
 AL returns character from comm port within time-out period
        (AH returns status as in AH = 3)

AH = 3      Return Status of Comm Port
 AH =
 Bit 7 = Time-Out (Couldn't Send or Receive Character)
 Bit 6 = Transmitter Shift Register Empty
 Bit 5 = Transmitter Holding Register Empty
 Bit 4 = Break Detected
 Bit 3 = Framing Error Detected
 Bit 2 = Parity Error Detected
 Bit 1 = Overrun Error Detected
 Bit 0 = Data Ready

 AL =
 Bit 7 = Data Carrier Detect
 Bit 6 = Ring Indicator
 Bit 5 = Data Set Ready
 Bit 4 = Clear to Send
 Bit 3 = Delta (Change in) DCD
 Bit 2 = Delta RI
 Bit 1 = Delta DSR
 Bit 0 = Delta CTS


Figure 2:  Additions to the BIOS for Interrupt 14

All of these new functions require that AH = 4. The desired new
function is loaded into AL. The comm port to operate on is loaded
into DX as in the original interrupt. Statuses are returned in
AX, if possible.

AL = 0        Return 0ff0 in AX to determine load status
 AL = 1       Initialize ports, desired mode in CX, clear buffers
 AL = 2       Initialize comm port with baud rate and other
              parameters in CL

     Bits 7   6   5   │    4  3    │        2        │    1  0
      Baud Rate       │ Parity     │ No. of Stopbits │ Word Length
      001 - 19.2K     │ 00 -  None │ 0 - 1 Stopbit   │ 00 - 5 Bits
      010 - 38.4K     │ 01 -  Odd  │ 1 - 2 Stopbits  │ 01 - 6 Bits
                      │ 10 -  None │                 │ 10 - 7 Bits
                      │ 11 -  Even │                 │ 11 - 8 Bits

 AL = 3 Set the time-out value to the number of 1/18ths
        of a second in CX

 AL = 4 Clear the Input Buffer (Reset its pointers)

 AL = 5 Return count in Input buffer in AX

 AL = 6 Clear the Transmit Buffer (Reset its pointers)

 AL = 7 Return the count in the Transmit Buffer in AX

 AL = 8 Uninstall the TSR driver, then release memory


Figure 3:  Register Usage in the 8250 UART

All register usage is based on the offset from the base port address.
For COM1 this is 3F8, for COM2 it is 2F8.

BPA + 0   Data
          When read from, the current character will be returned from the
          Receive Buffer Register.
          When written to, the byte is transferred first to the Transmitter
          Holding Register, then to theTransmitter Shift Register, where
          it is actually transmitted.

          If the Divisor Latch Access Bit (DLAB and bit 7 of the Line
          Control Register) is set, then this port is the Least Significant
          byte for the Baud Rate Divisor.

BPA + 1   Interrupt Enable Register
          Bits 7──4    NA
          Bit 3        Enable Modem Status Interrupts
          Bit 2        Enable Receive Line Status Interrupts
          Bit 1        Enable Xmit Holding Register Empty Interrupts
          Bit 0        Enable Data Available Interrupts

          If the DLAB is set, then this port is the Most Significant
          byte for the Baud Rate Divisor.

BPA + 2   Interrupt Identification Register
          Bits 7──3    NA
          Bits 2──0    (in descending priority)
          110          Receiver Line Status Interrupt
          100          Data Available Interrupt
          010          Transmitter Holding Buffer Empty Interrupt
          000          Modem Status Interrupt
          001          No Interrupt Pending (Not an Interrupt)

BPA + 3   Line Control Register
          Bit 7        Divisor Latch Access Bit
          Bit 6        Set Break (1 turns break on, 0 off)
          Bit 5&4      Parity
          0 0          Odd Parity
          0 1          Even Parity
          1 0          Space (Zero) Parity
          1 1          Mark (Ones) Parity
          Bit 3        Parity Enable
          Bit 2        Number of Stopbits (0=1 Stopbit, 1=2)
          Bit 1&0      Word Length
          0 0          5 Bits
          0 1          6 Bits
          1 0          7 Bits
          1 1          8 Bits

BPA + 4   Modem Control Register
          Bit 7──5     NA
          Bit 4        Loop Enable (Turns on Test Conditions)
          Bit 3        OUT2 (Hardware AND to Interrupts)
          Bit 2        External Connection, Not Used
          Bit 1        Request to Send
          Bit 0        Data Terminal Ready

BPA + 5   Line Status Register
          Bit 7        NA
          Bit 6        Transmitter Shift Register Empty
          Bit 5        Transmitter Holding Register Empty
          Bit 4        Break Interrupt Received
          Bit 3        Framing Error
          Bit 2        Parity Error
          Bit 1        Overrun Error
          Bit 0        Data Ready

BPA + 6   Modem Status Register
          Bit 7        Data Carrier Detect
          Bit 6        Ring Indicator
          Bit 5        Data Set Ready
          Bit 4        Clear to Send
          Bit 3        Delta (Change in) DCD
          Bit 2        Delta RI
          Bit 1        Delta DSR
          Bit 0        Delta CTS


───────────────────────────────────────────────────────────────────────────
MS-DOS(R) Comm Port Syntax Problems
───────────────────────────────────────────────────────────────────────────

There are certain problems when dealing with MS-DOS, which are caused by
confusion in syntax. For example, the first communications port found by
the POST code in the BIOS is loaded into a set memory location (40:0).
What is actually stored is the base address of the communications
port──each 8250 requires eight consecutive port addresses. Subsequent
communications ports are loaded at 40:2, 40:3, and 40:4. (Yes, MS-DOS
allows for more than two communications ports.)

The problem arises when the first communications port found by the POST is
really that of an 8250 set up as a physical COM2. Since this is the first
communications port found, it will be stored at 40:0, which is the address
reserved for COM1. However, COM2 generates an IRQ 3, whereas COM1
generates an IRQ 4. Programs that rely on the data found at 40:0 to
determine which comm ports exist and what their physical interrupt
characteristics are may have a problem unless the data stored at that
address is examined and interpreted, which thereby prohibits device
independence.

───────────────────────────────────────────────────────────────────────────
Status of the Transmit Shift Register Versus the Transmitter Holding
───────────────────────────────────────────────────────────────────────────

The 8250 chip has some inherent problems that you should be aware of. One
of them can cause a routine that you might write to appear to transmit
characters improperly.

When you direct the 8250 to output a character by writing to the
Transmitter Holding Buffer (a write instruction out to the Base Port of
the 8250), it immediately transfers that byte to the Transmitter Shift
Register (an internal register within the 8250) and starts transmitting
it a serial bit at a time.

However, the moment the Transmitter Holding Buffer is empty, an interrupt
will be generated to indicate that the transmitter buffer is empty. If you
have no bytes left to transmit, you might think this is a safe time to
reset the 8250 as required. There is a good chance, however, that the last
character is still being transmitted. If you were to reset the 8250, you
might cause the last character to be transmitted improperly, if it is
transmitted at all.

It's a good idea to check the status of both the Holding Buffer and the
Shift Register before you consider a transmission to be over.
Fortunately, the folks at National Semiconductor have made this very easy:
when you check the status of the Transmitter Shift Register, you're
actually checking the status of both registers. The secret of getting the
highest output rate in the safest manner is for you to check only the
status of the Transmitter Holding Register when you are attempting to
output characters, but to check the status of the Transmitter Shift
Register to determine when the last character has been sent.

───────────────────────────────────────────────────────────────────────────
XON/XOFF
───────────────────────────────────────────────────────────────────────────

The XON/XOFF protocol, which is often used during non-error-checking
transmissions of ASCII files, permits the receiving station in a
communications session to indicate to the transmitting station to stop
sending characters until it is told to continue.

Even though the code allows you to define a buffer as large as you want
(which will receive characters as long as there is space in the buffer and
hardware interrupts), you'll eventually run across the problem of receiving
characters more rapidly than you can handle them. An example of this is when
the buffer fills up and you now must write the captured data to disk. This
takes a finite amount of time, during which interrupts may be turned off for
more than one character receive interval. If you don't use some protocol,
you'll lose characters.

The XON/XOFF protocol defines two characters, a Ctrl-S for XOFF and a Ctrl-Q
for XON, that tell the transmitting side to cease transmission by sending an
XOFF and to restart transmission by sending an XON.

Life is seldom as easy as it might seem, and this protocol is no exception.
First, it has been very loosely implemented in many current programs to
determine that the XON character may be any character. If you design a
protocol that waits only for the XON character, you might wait a long, long
time. (The terminal driver in your MS-DOS machine has been loosely
implemented with an XOFF protocol. Try hitting a Ctrl-S during a long
listing and then any other character-generating key as if it were an XON
character.)

Second, this protocol is rather timing-sensitive, especially with the
increasing popularity of some of the packet-switching networks. This is
because when the buffer is 80 percent full, an XOFF character is sent to the
transmitter. It may take a few seconds for the character to be received and
acted on by the transmitter and for the transmitter to cease sending
characters. Until the transmitter does receive the XOFF, it is merrily
dumping characters out at character rates that can be as high as almost
1,000 cps. You might have to leave as much as 5 seconds of character space
in your buffer to handle situations like this. A 5Kb high-water mark is
probably too much in most situations, however, so the code lets you modify
both the buffer size and the high-water mark as you require.

You can only send one XOFF character, even if it looks as if the remote end
is ignoring your first XOFF request. There is always the possibility that
the packet-switch network has delayed your first XOFF and that subsequent
XOFFs may actually be taken as XON characters.

───────────────────────────────────────────────────────────────────────────
RS-232C: A Standard Problem
───────────────────────────────────────────────────────────────────────────

RS-232C is the shorthand abbreviation for Recommended Standard 232,
Revision C, from the engineering department of the Electronic Industries
Association. Its full name is Interface between Data Terminal Equipment
and Data Communication Equipment Employing Serial Binary Data Interchange.

RS-232C defines the wiring at each end of a connection between two
devices, such as a modem (the Data Communication Equipment, or DCE) and
your computer (the Data Terminal Equipment, or DTE). It requires that the
DCE have a female connection and the DTE a male connection. RS-232C
defines the supposed purpose of 25 different wires, which pins they should
terminate on and even what voltage levels and current drains and loads are
allowed, but that's about it (see Figure A).

So, if you have two computers that you want to hook up to one another,
which one is the DTE and which is the DCE? That's not defined, but the
standard seems to work only if there is one of each, since it defines the
"direction" of the flow of information──whether a particular signal is
generated from the DTE or the DCE. Therefore, you must define one or the
other as DTE and fool the other side into thinking that it too is a DTE.

You can do this with a null-modem cable; think of it as almost having two
modems in between two machines. Of the 25 defined pins/wires, only 11 of
them are used for asynchronous communications──the others are used for
synchronous communications or reserved for future use (see Figure B).

Usually, for each transmitted data or control function, there is a
complementary receive data or control function. By properly wiring these
two mutually exclusive sets of wires, you have designed a cable that can
trick the other side into thinking it is a modem (see Figure C for a
complete picture).

Of course, when one of the serial cards mounted within your machine does
not even follow what little defined standard there is, we've all got a
problem. It shouldn't surprise you to find that many of the serial cards
out there are following their own versions of RS-232C. Remember that the R
stands for "Recommended."


Figure A:  Pin Usage Table

╓┌──────┌─────────┌──────────────────────────────────────────────────────────╖
Pin    Circuit   Name

 1     AA        Protective Ground
 2     BA        Transmitted Data
 3     BB        Received Data
Pin    Circuit   Name
 3     BB        Received Data
 4     CA        Request to Send
 5     CB        Clear to Send
 6     CC        Data Set Ready
 7     AB        Signal Ground or Common
 8     CF        Received Line Signal Detect (Data Carrier Detect)
 9     -         Reserved/Unassigned
10     -         Reserved/Unassigned
11     -         Reserved/Unassigned
12     SCF       Secondary Received Line Signal Detect
13     SCB       Secondary Clear to Send
14     SBA       Secondary Transmitted Data
15     DB        Transmission Signal Element Timing
16     SBB       Secondary Received Data
17     DD        Receiver Element Timing
18     -         Reserved/Unassigned
19     SCA       Secondary Request to Send
20     CD        Data Terminal Ready (DTR)
21     CG        Signal Quality Detector
22     CE        Ring Indicator
Pin    Circuit   Name
22     CE        Ring Indicator
23     CH/CI     Data Signal Rate Detector
24     DA        Transmit Signal Element Timing
25     -         Reserved/Unassigned


Figure B:  Asynch Set of Pins

Pin  Abbrev.  Name                 Direction  Function

 1   -        Protective Ground    None       Ground
 2   TD       Transmitted Data     to DCE     Outbound Data
 3   RD       Received Data        to DTE     Incoming Data
 4   RTS      Request to Send      to DCE     DTE wants to send data
 5   CTS      Clear to Send        to DTE     DCE okays send request
 6   DSR      Data Set Ready       to DTE     DCE is ready to communicate
 7   -        Signal Common        None       Common Ground
 8   DCD      Data Carrier Detect  to DTE     Carrier (Linkup) between
                                              DTE/DCE
20   DTR      Data Terminal Ready  to DCE     Enable DCE (say DTE is ready)
22   RI       Ring Indicator       to DTE     Phone is ringing
23   DSRD     Data Sig. Rate       Undefined  Complicated, but used to
              Detect                          negotiate data rates between
                                              DCE and DTE


Figure C:

               │                                            │
             ┌─┴┐ Pin No.                          Pin No. ┌┴─┐
             │  └─────┐             ┌──┐             ┌─────┘  │
             │TD     2╞═══──────────┘┌─│──────────═══╡2     TD│
             │RD     3╞═══───────────┘ └──────────═══╡3     RD│
             │RTS    4╞═══──┬─────────┐        ┌──═══╡4    RTS│
             │CTS    5╞═══──┘      ┌──│────────┴──═══╡5    CTS│
             │DSP    6╞═══────┐    │  │        ┌──═══╡6    DSP│
             │COMMON 7╞═══─────────│───────────│──═══╡7 COMMON│
             │DCD    8╞═══────│────┘  └────────│──═══╡8    DCD│
             │DTR   20╞═══──┐ ├────────────────│──═══╡20   DTR│
             │RI    22╞═══──│─┘                ├──═══╡22    RI│
             │  ┌─────┘     └──────────────────┘     └─────┐  │
             └─┬┘                                          └┬─┘
               │                                            │


───────────────────────────────────────────────────────────────────────────
Sample Code Fragments for TSRCOMM.ASM
───────────────────────────────────────────────────────────────────────────

;;       TSRCOMM.ASM - Written by Ross M. Greenberg
;;       Sample Code Fragments Follow
                                    ∙
                                    ∙
                                    ∙
P1_INLEN        equ     400h    ; Define sizes of input and output
P2_INLEN        equ     400h    ; buffers. High-water and low-water
P1_OUTLEN       equ     400h    ; marks are a direct reflection of
P2_OUTLEN       equ     400h    ; these values

;; Be careful with these settings if yours have different lengths for
;; each of the COM_INBUF's: these only play off COM1_INBUF

HIGH_MARK equ (P1_INLEN/10 * 8) ; send XOFF when buffer is 80% full
LOW_MARK  equ (P1_INLEN/10 * 2) ; send XON when buffer is 20% full

;;      Definitions of all 8250 Registers and individual bit meanings

DATA            equ     0h      ; DATA I/O is from the base

IER             equ     1h      ; Interrupt Enable Register
 IER_RDA        equ     1h      ; Received Data Available int bit
 IER_THRE       equ     2h      ; Transmitter Hold Reg. Empty int bit
 IER_RLS        equ     4h      ; Receive Line Status int bit
 IER_MS         equ     8h      ; Modem Status int bit

IIR             equ     2       ; Interrupt Identification Register
 IIR_RLS        equ     5h      ; *equal* to if Receiver Line Status int
 IIR_RDA        equ     4h      ; *equal* to if character ready
 IIR_THRE       equ     2h      ; *equal* to if TX Buffer empty
 IIR_PEND       equ     1h      ; set to zero if any interrupt pending
 IIR_MS         equ     0h      ; *equal* to if Modem Status int

LCR             equ     3h      ; Line Control Register
 LCR_WLS0       equ     0h      ; Word Length Select Bit 0
 LCR_WLS1       equ     1h      ; Word Length Select Bit 1
 LCR_STOPBITS   equ     4h      ; number of stop bits
 LCR_PARITYEN   equ     8h      ; Enable Parity (see SPARITY & EPARITY)
 LCR_EPARITY    equ     10h     ; Even Parity Bit
 LCR_SPARITY    equ     20h     ; Stick Parity
 LCR_BREAK      equ     40h     ; set if break is desired
 LCR_DLAB       equ     80h     ; Divisor Latch Access Bit

MCR             equ     4h      ; Modem Control Register
 MCR_DTR        equ     1h      ; Data Terminal Ready
 MCR_RTS        equ     2h      ; Request To Send
 MCR_OUT1       equ     4h      ; Output 1 (nobody uses this!)
 MCR_OUT2       equ     8h      ; Out 2 (Sneaky Int enable)
 MCR_LOOP       equ     10h     ; Loopback enable

LSR             equ     5       ; Line Status Register
 LSR_DATA       equ     1h      ; Data Ready Bit
 LSR_OVERRUN    equ     2h      ; Overrun Error Bit
 LSR_PARITY     equ     4h      ; Parity Error Bit
 LSR_FRAMING    equ     8h      ; Framing Error Bit
 LSR_BREAK      equ     10h     ; Break Detect (sometimes an error!)
 LSR_THRE       equ     20h     ; Transmit Holding Register Empty
 LSR_TSRE       equ     40h     ; Transmit Shift Register Empty

MSR             equ     6       ; Modem Status Register
 MSR_DEL_CTS    equ     1h      ; Delta Clear To Send
 MSR_DEL_DSR    equ     2h      ; Delta Data Set Ready
 MSR_EDGE_RI    equ     4h      ; Trailing Edge of Ring Indicator
 MSR_DEL_SIGD   equ     8h      ; Delta Receive Line Signal Detect
 MSR_CTS        equ     10h     ; Clear To Send
 MSR_DSR        equ     20h     ; Data Set Ready
 MSR_RI         equ     40h     ; Ring Indicator - during entire ring
 MSR_DCD        equ     80h     ; Data Carrier Detect - on-line
CTRL_PORT       equ     20h     ; The 8259 lives here
INT_EOI         equ     20h     ; The End of Interrupt reset value
INT_MASK_PORT   equ     21h     ; The mask for the 8259 lives here
COM1_MASK       equ     0efh    ;
COM2_MASK       equ     0f7h    ;
INTNO_COM1      equ     0ch     ; The physical interrupt number for
INTNO_COM2      equ     0bh     ; Comm1 and for Comm2

PORT1   equ     3f8h            ; Physical ports where Comm1 & Comm2
PORT2   equ     2f8h            ; should be. See sidebar 1 for info.
                                    ∙
                                    ∙
                                    ∙
;;                      COMM PORT BLOCK   (CPB)
;;      Comm Port Block defines information unique for each comm port
;;      and includes information such as what the original interrupt
;;      vector pointed to, which parameters are set, etc.
CPB     struc

cpb_base        dw      ?       ; base port of comm port (2F8, 3F8)
cpb_nint_off    dw      ?       ; new interrupt offset address
cpb_pic_mask    db      ?       ; mask for enabling ints from 8259
cpb_int_no      db      ?       ; what interrupt we are
cpb_mode        dw      ?       ; whatever modes we have turned on
cpb_timeout     dw      ?       ; time-out value off timer tick
cpb_in_xoff     dw      0       ; true if we output an XOFF
cpb_out_xoff    dw      0       ; true if an XOFF was sent to us
cpb_inbase      dw      ?       ; start of input buffer
cpb_inlen       dw      ?       ; length of input buffer allocated
cpb_inhead      dw      ?       ; pointer to next input char location
cpb_intail      dw      ?       ; pointer to last input char location
cpb_incnt       dw      0       ; count of how many inp chars outstanding
cpb_inerrors    dw      ?       ; pointer to the error bits
cpb_outbase     dw      ?       ; start of output header
cpb_outlen      dw      ?       ; total length of output buffer allocated
cpb_outhead     dw      ?       ; pointer to next output char location
cpb_outtail     dw      ?       ; pointer to last output char location
cpb_outcnt      dw      0       ; count of how many outp chars outstanding
cpb_outend      dw      ?       ; ptr to the end of the output buffer
cpb_tx_stat     dw      0       ; set to no interrupts turned on
cpb_oint_add    dw      ?       ; original int offset:segment order
                dw      ?
CPB     ends
                                    ∙
                                    ∙
                                    ∙
;;                      HANDSHAKING OPTIONS
BREAK_IS_ERROR_OPTION   equ     01h     ; Set these bits (OR combination)
DSR_INPUT_OPTION        equ     02h     ; to set desired options. Then
DCD_INPUT_OPTION        equ     04h     ; set CX to this value, DX to the
CTS_OUTPUT_OPTION       equ     08h     ; comm port desired, and generate
XOFF_INPUT_OPTION       equ     10h     ; an INT0x14 with AH=4 and AL=1
XOFF_OUTPUT_OPTION      equ     20h
DTR_OPTION              equ     40h
XON_IS_ANY_OPTION       equ     80h
TX_INTS_ON_OPTION       equ     100h
;; Masm has a 256-byte static initialization limit.  NC is shorter than
;; NO_CHARS....
NC              equ     0

;; WARNING!      Do not move the error array away from its approriate
;;               error array, or you'll probably crash at some point!

com1_inbuf      db      P1_INLEN dup(0)          ; Allocate space for
com1_errs       db      (P1_INLEN/8) + 1 dup(0)  ; both comm ports:
com2_inbuf      db      P2_INLEN dup(0)          ; input, error and
com2_errs       db      (P2_INLEN/8) + 1 dup(0)  ; output buffer
com1_outbuf     db      P1_OUTLEN dup(0)
com2_outbuf     db      P2_OUTLEN dup(0)

;; CPB1 and CPB2
;; Allocate space and initialize COMM PORT BLOCKS for com1 and com2
cpb1 CPB <PORT1,offset com1_isr,COM1_MASK,INTNO_COM1,DEF_FLAGS,
        DEF_T_OUT,NO_XON,NO_XON,offset com1_inbuf,P1_INLEN,
        offset com1_inbuf,offset com1_inbuf,NC,offset com1_errs,
        offset com1_outbuf,P1_OUTLEN,offset com1_outbuf,
        offset com1_outbuf,NC>
cpb2 CPB <PORT2,offset com2_isr,COM2_MASK,INTNO_COM2,DEF_FLAGS,
        DEF_T_OUT,NO_XON,NO_XON,offset com2_inbuf,P2_INLEN,
        offset com2_inbuf,offset com2_inbuf,NC,offset com2_errs,
        offset com2_outbuf,P2_OUTLEN,offset com2_outbuf,
        offset com2_outbuf,NC>
                                    ∙
                                    ∙
                                    ∙
xmit_int        proc    near
        cmp     cpb_outcnt[si], 0        ; any work to do?
        jnz     xmit1                    ; yep!
        call    till_clear               ; wait for transmitter to clear
        call    tx_off                   ; turn xmit interrupts off
        ret                              ; and return
xmit1:
        mov     bx, cpb_outtail[si]      ; get next character to xmit
        inc     bx                       ; now point right on it
        cmp     bx, cpb_outend[si]       ; cmp to the end
        jnz     xmit2                    ; if not past the end, jump
        mov     bx, cpb_outbase[si]      ; past end, reset to the head
xmit2:
        cli                              ; don't get interrupted now
        dec     cpb_outcnt[si]           ; decr. count of chars to go
        mov     cpb_outtail[si], bx      ; save a pointer to next char
        mov     al, [bx]                 ; get the character in al
        cmp     cpb_outcnt[si], 0        ; any work left to do?
        jnz     out_it                   ; yep!
        call    till_clear               ; wait for transmitter to clear
        call    tx_off                   ; turn xmit interrupts off
out_it:
        call    out_char                 ; output the character
        ret
xmit_int        endp
                                    ∙
                                    ∙
                                    ∙
  interrupt_table label word               ;
        dw      offset ms_int            ; modem status int (ret only)
        dw      offset xmit_int          ; transmitter int
        dw      offset rda_int           ; character ready int
        dw      offset err_int           ; receiver line error int
                                    ∙
                                    ∙
                                    ∙
com1_isr        proc far ; Entry point for comm1 interrupts
        push    ax
        lea     ax, cpb1
jmp     short   common_isr
com1_isr        endp

com2_isr        proc far ; Entry point for comm2 interrupts
        push    ax
        lea     ax, cpb2
        jmp     short   common_isr
com2_isr        endp
                                    ∙
                                    ∙
                                    ∙
common_isr      proc    near             ; IRQ's come to here. If not
        push    bx                       ; ours jump to old int vector
        push    cx
        push    dx
        push    si
        push    di
        push    ds
        push    cs                       ; addressing off ds as cs
        pop     ds                       ; makes it easier to think
        mov     si, ax                   ; move in the cpb
        mov     di, cpb_base[si]         ; get the base port

        lea     dx, IIR[di]              ; and then the int ID Reg
        in      al, dx                   ; get the interrupt type
        test    al, IIR_PEND             ; is there a pending int?
        jz      is_mine                  ; interrupt on *this* chip!
other_int:
        cli                              ; turn off ints since this
                                         ; is non-reentrant
        mov     ax, cpb_oint_add[di]     ; grab the old int out of
        mov     old_int, ax              ; the structure
        mov     ax, cpb_oint_add[di][2]
        mov     old_int[2], ax
        pop     ds                       ; pop everything back
        pop     di
        pop     si
        pop     dx
        pop     cx
        pop     bx
        pop     ax
        jmp     dword ptr cs:[old_int]   ; jump to whatever was there
polling_loop:                            ; this is required to be sure
                                         ; we haven't lost any ints
        lea     dx, IIR[di]              ; load the int ID Reg
        in      al, dx                   ;
        test    al, IIR_PEND             ; is there a pending int?
        jnz     clear                    ; no. time to return

is_mine:
        and     ax, 06h
        mov     bx, ax
        mov     bx, interrupt_table[bx]
        push    di                       ; save di for polling loop
        call    bx
        pop     di
        jmp     polling_loop             ; time to check for more work
clear:                                   ; no further int processing
        pop     ds                       ; pop everything back
        pop     di
        pop     si
        pop     dx
        pop     cx
        pop     bx
        cli                              ; interrupts off, then reset
        mov     al, INT_EOI              ; interrupts on the 8259
        out     CTRL_PORT, al
no_eoi:
        pop     ax
        iret                             ; iret will turn ints back on
common_isr      endp
                                    ∙
                                    ∙
                                    ∙
int14_functions label   word             ;
        dw      offset  init14           ; initialize the port
        dw      offset  send14           ; send the character in al
        dw      offset  get14            ; return next char in al,
                                         ; status in ah
        dw      offset  stat14           ; get serial status,return in ax
        dw      offset  newfuncs14       ; all of the new functions

get_baud        proc    near             ; AX is the offset,
        shl     ax, 1                    ; divisor returned in AX
        push    bx                       ; make the table offset
        mov     bx, ax
        mov     ax, baudrate_table[bx]   ; and get the divisor
        pop     bx
        ret
get_baud        endp

baudrate_table  label   word
        dw      1047                     ; 110 baud
        dw      768                      ; 150 baud
        dw      384                      ; 300 baud

        dw      192                      ; 600 baud
        dw      96                       ; 1200 baud
        dw      48                       ; 2400 baud
        dw      24                       ; 4800 baud
        dw      12                       ; 9600 baud
        dw      6                        ; 19200 baud
        dw      3                        ; 38400 baud
                                    ∙
                                    ∙
                                    ∙
funcs_table     label   word             ;
        dw      offset  new00            ; Each function corresponds to
        dw      offset  new01            ; AL value used for
        dw      offset  new02            ; subfunction
        dw      offset  new03            ;
        dw      offset  new04            ;
        dw      offset  new05            ;
        dw      offset  new06            ;
        dw      offset  new07            ;
        dw      offset  new08            ;

newfuncs14      proc    near
        cmp     al, 08h                  ; out of bounds?
        jle     dispatch                 ; no
        mov     ax, 0ffffh               ; yes, error code
        ret
dispatch:
        call    get_cpb                  ; get si to point to proper cpb
        mov     di, cpb_base[si]         ; point the ports!
        xor     bx, bx
        mov     bl, al
        shl     bx, 1
        mov     bx, funcs_table[bx]
        call    bx
        ret
newfuncs14      endp

new00   proc    near
        mov     ax, special_return_value
        ret
new00   endp

new01   proc    near
        mov     cpb_mode[si], cx         ; move the new mode in
        call    init_buffers             ; and reset the pointers
        ret
new01   endp

new02   proc    near
        lea     dx, LCR[di]              ; get the Latch
        in      al, dx
        or      al, LCR_DLAB             ; turn on the divisor
        out     dx, al                   ; in the chip
        push    cx
        mov     ax, cx
        and     ax, 00e0h                ; only the highest three bits
        mov     cl, 5
        shr     ax, cl
        add     ax, 7                    ; let offset start at 8 (19200)
        call    get_baud                 ; then get the correct divisor
                                         ; allows higher than 9600
        pop     cx
        lea     dx, DATA[di]             ; get the base address
        out     dx, ax                   ; output the whole word
        lea     dx, LCR[di]              ; get the Latch
        mov     al, cl                   ; get the other parameters and
        and     al, 01fh                 ; mask only parity, stop bits,
                                         ; word length
        out     dx, al                   ; set the params
        ret
new02   endp

new03   proc    near
        mov     cpb_timeout[si], cx
        ret
new03   endp

new04   proc    near
        cli
        mov     cpb_incnt[si], NO_CHARS
        mov     ax, cpb_inbase[si]
        mov     cpb_inhead[si], ax
        mov     cpb_intail[si], ax
        sti
        ret
new04   endp

new05   proc    near
        mov     ax, cpb_incnt[si]
        ret
new05   endp

new06   proc    near
        cli
        mov     cpb_outcnt[si], NO_CHARS
        mov     ax, cpb_outbase[si]
        mov     cpb_outhead[si], ax
        mov     cpb_outtail[si], ax
        sti
        ret
new06   endp

new07   proc    near
        mov     ax, cpb_outcnt[si]
        ret
new07   endp

new08   proc    near
        mov     si, offset cpb1          ; set up for port1
        cmp     cpb_oint_add[si], 0      ; com port installed?
        jz      new0801                  ; no
        call    unset_up                 ; and kill this comm port
new0801:
        mov     si, offset cpb2          ; set up for port2
        cmp     cpb_oint_add[si], 0      ; com port installed?
        jz      new0802                  ; no
        call    unset_up                 ; and kill this comm port
new0802:
        cli
        mov     dx, old_int14
        mov     al, 014h
        push    ds
        mov     ds, old_int14[2]
        DOSINT  25h                      ; reset the serial port int
        pop     ds
        mov     dx, orig_timer
        mov     al, TIMER_TICK_INT_NO
        push    ds
        mov     ds, orig_timer[2]
        DOSINT  25h                      ; reset the timer_tick int
        pop     ds
        push    cs
        pop     es                       ; free up our own memory
        DOSINT  49h                      ; the environment
        sti
        ret
new08   endp
                                    ∙
                                    ∙
                                    ∙


───────────────────────────────────────────────────────────────────────────
Combining Interrupts and Polling: An Adventure in Programming
───────────────────────────────────────────────────────────────────────────

One of the things that made writing a program such as TSRCOMM so
interesting was the amount of time spent thinking, "But, that should have
worked!"

When initially contemplated, TSRCOMM was going to be fully interrupt
driven; no polling  was to be included at all. A good look at the
hardware, confirmed by experimental programming, showed that it was
possible for a "dual interrupt" system, one capable of handling transmit
buffer empty and data interrupts, to actually lose an interrupt while
processing another.

Therefore, much to my chagrin, you'll find a polling loop right in the
middle of the COMMON_ISR interrupt routine. The loop causes the code to
continually cycle once an interrupt is generated until, by polling the
hardware and reading the appropriate ports, it determines that there is no
more work to be done during this cycle.

Fortunately, once compiled, this ugliness is rarely noticed.

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

BLOWUP: A Windows Utility for Viewing and Manipulating Bitmaps

Charles Petzold☼

When you first start programming for Microsoft(R) Windows, the concept of a
bitmap seems rather easy to grasp. A bitmap is simply a collection of bits
that correspond directly to the scan lines and pixels of a rectangular
display image.

However, working with bitmaps is not quite as easy as understanding them.
Even the seemingly simple chore of displaying a bitmap on the client area
of a window can be puzzling. If you search for a Windows function by the
name of DrawBitmap, you do so in vain. The function does not exist, and by
the time you learn that a "memory display context" is necessary for the
job, you may be nostalgically recalling how easy and simple life was when
you programmed strictly in character mode.

BLOWUP is a program designed to dispel some of the mysteries of bitmaps
and memory display contexts and provide a little fun as well. With BLOWUP
you can use the mouse to transfer almost anything that appears on the
screen to the Windows clipboard in a bitmap format. BLOWUP is also a
"clipboard viewer" of bitmaps and will display any bitmap currently in the
clipboard. As its name implies, BLOWUP will blow up or shrink down the
bitmap to fit its own client area. Figure 1☼ shows BLOWUP's client area and
the contents of the clipboard after BLOWUP has been used to capture part
of the MS-DOS(R) Executive window.

Once the bitmap is in the clipboard, you can transfer the image to any
Windows program that can handle bitmaps. (Note that some programs, such as
WRITE, PAINT, and CARDFILE, will convert a color bitmap to monochrome,
while others, such as Aldus Corp.'s PageMaker(R), will not accept color
bitmaps at all.)

Although the image in the clipboard is the actual size of the image as it
appeared on the screen, you can use BLOWUP a second time to transfer the
expanded image in BLOWUP's client area to the clipboard. BLOWUP thus
becomes a tool to manipulate bitmaps manually──you can crop them, turn them
upside down or left to right, shrink them, and blow them up. The BLOWUP
source code can also help you learn how to create bitmaps, display them on
the screen, write a clipboard viewer, transfer bitmaps between your
program and the clipboard, "capture" and track the mouse, and you can even
draw outside the client area of your window.

With an installed Microsoft(R) Windows Software Development Kit and
Microsoft(R) C Compiler Version 4.0, you can create BLOWUP.EXE by
executing:

  MAKE BLOWUP


Capturing Images

BLOWUP requires a mouse. The program is simple to use once you get the
hang of it, but here are the precise instructions:

  1. The first step is to click the mouse in BLOWUP's client area. The
     cursor will then be changed to a crosshair.

  2. Move the mouse cursor to the upper left-hand corner of the screen area
     you want to capture. Press on the mouse button.

  3. Drag the mouse (with the button depressed) to the lower right-hand
     corner of the screen area you want to capture. As you move the mouse,
     BLOWUP briefly displays the blocked-out image in reverse video. Release
     the mouse button. The image will be transferred to the clipboard and
     then to BLOWUP's client area.

If you block out the image starting at the lower left-hand corner, the
image will then be turned upside down, both in the clipboard and in
BLOWUP's client area. Starting at the upper right-hand corner will flip
it around the vertical axis; starting from the lower right-hand corner
will flip it both horizontally and vertically.


Capturing the Mouse

The first problem BLOWUP has to solve is how to track the mouse when it is
outside of BLOWUP's client area. Normally a Windows program receives mouse
messages only when the mouse cursor is positioned over the program's
window. In order to get around this restriction, BLOWUP uses a technique
called "capturing the mouse," which requires one Windows call:

  SetCapture (hWnd) ;

After this call, all mouse movement messages and all mouse button messages
will be directed to the window function whose handle is hWnd. The mouse
capture is ended by a call to ReleaseCapture.

The lParam parameter that accompanies mouse messages contains the current
position of the mouse relative to the upper left-hand corner of the
window's client area. The x coordinate is in the low word of lParam, and
the y coordinate is in the high word. After you capture the mouse, one or
both of these coordinates will be negative if the mouse is to the left of
or above your window's client area.

BLOWUP maintains two Boolean variables, bCapturing and bBlocking, which
are used to keep track of what mode it is in while it is processing the
WM_LBUTTONDOWN, WM_LBUTTONUP, and WM_MOUSEMOVE messages. During the first
button-down message, BLOWUP sets the bCapturing flag, captures the mouse,
and displays the crosshair mouse cursor. On the second button-down
message, BLOWUP sets the bBlocking flag and saves the position of the
mouse in org ("origin"), a structure of type POINT. That's the first
corner of the rectangle you'll be blocking out.

When BLOWUP receives a WM_MOUSEMOVE message while bBlocking is set, it
retrieves the new mouse position in another POINT structure called len
("length") and subtracts from that the origin. The result is the size of
the image measured from the origin. (The values can be negative.) BLOWUP
also calls the routine InvertBlock twice, once to invert the colors of the
blocked-out image, and the second time in order to change it back to
normal.


Painting

The inversion of the blocked-out rectangle may shock some Windows
programmers because it requires that BLOWUP paint outside its client area,
which is normally impossible. When a Windows program prepares for
painting, it obtains a handle to a display context using the GetDC or
BeginPaint function. This display context allows a program to paint only
within its client area.

But BLOWUP's InvertBlock routines use the less common CreateDC function to
obtain a display context. This function is normally used for obtaining a
display context for a printer, but it can also obtain a display context
for the entire screen. The first parameter is the string "DISPLAY", and
the other three parameters are set to NULL. The origin of this display
context is the upper left-hand corner of the display.

Because the coordinates of the org structure are relative to the upper
left-hand corner of BLOWUP's client area, the point must be converted to
screen coordinates by using the ClientToScreen function first. BLOWUP
then calls PatBlt ("pattern block transfer") with a raster operation code
of DSTINVERT to invert the rectangular area of the display.

Painting outside a program's window is generally not a polite thing to do,
which is why BLOWUP restores the area right away with another call to
InvertBlock. This is a good compromise──it gives you a visual indication of
the area you're blocking out with BLOWUP, but it doesn't permanently
affect the windows of other programs. And because the MS-DOS version of
Windows is nonpreemptive, there is positively no chance of anything
changing the screen between the two consecutive calls to InvertBlock.


Creating the Bitmap

When BLOWUP receives a WM_LBUTTONDOWN message while the bBlocking flag is
set, it must create a bitmap containing the blocked-out image and transfer
it to the clipboard. This job requires us to approach that strange animal
called the "memory display context" and attempt to make friends with it.

A display context is a data structure that describes a physical display
device, such as a screen or a printer. When a program obtains a handle to
a display context, the program is also getting permission to paint on the
device.

A memory display context is very similar to a normal display context
except that the display "surface" is a block of memory. When you first
obtain a handle to a memory display context through the CreateCompatibleDC
function, this display surface is very small──exactly one pixel wide, one
pixel high, and monochrome, which is not very useful. What you must do
before actually working with a memory display context is select a bitmap
into it by using SelectObject.

When you do this, the bitmap becomes the display surface of the memory
display context. The upper left-hand corner of the bitmap corresponds to
the display context coordinate of (0,0). Any image previously in the
bitmap becomes part of the memory display context's display surface. Any
drawing you do on that memory display context is actually performed on the
bitmap. When you delete the memory display context, you are then left with
a bitmap containing everything that you painted on it.

There are a variety of methods for creating bitmaps; the one BLOWUP uses
is the CreateCompatibleBitmap call. This function creates a bitmap with
the same number of color planes and color bits per pixel as the display
context specified in the first parameter of the function──which in this
case is the video display context. In BLOWUP, the height and width of the
bitmap are the absolute values of the two sizes in the len structure. The
bitmap itself is uninitialized, which means that it contains random data.

Once BLOWUP selects the bitmap into the memory display context, all it
needs to do is call StretchBlt to transfer the blocked-out area of the
screen display context to the memory display context. The image winds up
in the bitmap.

Because the bitmap is the same height and width as the blocked-out display
area, you may be wondering why I use StretchBlt rather than BitBlt for
this job. BitBlt would work fine if you only blocked out the image
starting at the upper left-hand corner; otherwise BLOWUP must flip the
image, and BitBlt cannot flip images whereas StretchBlt can.


Copying the Bitmap

Getting the bitmap into the clipboard is the easy part of the job. BLOWUP
simply calls OpenClipboard, EmptyClipboard, SetClipboardData, and
CloseClipboard, and it's done.

Normally a program would delete a bitmap after it has finished using it.
However, when a bitmap is transferred to the clipboard, the bitmap becomes
the responsibility of Windows itself. Windows will delete the bitmap the
next time a program transfers something else to the clipboard.


Becoming a Viewer

BLOWUP is also a clipboard viewer, which means that it is notified
whenever the contents of the clipboard change and will display the
clipboard contents. Unlike the indiscriminate CLIPBRD.EXE program that
comes packaged with Microsoft Windows, BLOWUP is very selective──it will
only display bitmaps and will ignore the other clipboard formats.

Becoming a clipboard viewer requires very little overhead. BLOWUP first
makes a call to SetClipboardViewer while it is processing the WM_CREATE
message. SetClipboardViewer returns the window handle of the previous
clipboard viewer. BLOWUP then saves this as a static variable that is
called hWndNext. Saving this window handle is very important──Windows
maintains only one window handle as a "current clipboard viewer," and it
relies on other programs to participate in the "clipboard viewer chain."

Here's how it works: when the contents of the clipboard change, Windows
sends the current clipboard viewer (the most recent program to register
itself as a clipboard viewer) a WM_DRAWCLIPBOARD message. The program that
receives this message is responsible for sending the message to the next
clipboard viewer, which is the window whose handle was returned from the
SetClipboardViewer call. Every clipboard viewer will at that point see the
message WM_DRAWCLIPBOARD as it ripples down the clipboard viewer chain.

When a program wants to get out of the clipboard viewer chain, it must
call ChangeClipboardChain. (BLOWUP does this during the processing of the
WM_DESTROY message right before it terminates.) Notice that the two
parameters to this function are the program's own window handle and
hWndNext. Windows will then respond by sending to the current clipboard
viewer a WM_CHANGECBCHAIN message with wParam equal to the window handle
of the program removing itself from the chain and the low word of lParam
equal to the window handle of the next clipboard viewer──the same two
values that are passed along to ChangeClipboardChain. If BLOWUP finds that
the clipboard viewer that is removing itself from the chain is the next
clipboard viewer after BLOWUP──in which case hWndNext will be equal to
wParam──then it must change its own stored value of hWndNext in order to
effectively skip over the departing program in future WM_DRAWCLIPBOARD
calls.

That's the extent of the overhead required for being a clipboard viewer.
Of course, a clipboard viewer will also do a little something while
processing the WM_DRAWCLIPBOARD message to display the new clipboard
contents. After sending the WM_DRAWCLIPBOARD message down the clipboard
viewer chain, BLOWUP simply invalidates its own client area. This is what
causes Windows to generate a WM_PAINT message for BLOWUP to recreate its
client area.


Processing the Message

When BLOWUP receives a WM_PAINT message, it must get the bitmap out of the
clipboard and display it in its client area. BLOWUP opens the clipboard
with a call to OpenClipboard and then uses GetClipboardData to get a
handle to the bitmap currently stored in the clipboard. It's possible that
the clipboard will not contain a bitmap, in which case the function will
return NULL. If that's the case, BLOWUP closes the clipboard and leaves
its client area unpainted.

If BLOWUP is successful in getting a bitmap from the clipboard, it creates
another memory display context. Earlier, when it was processing the
WM_LBUTTONUP message, BLOWUP used SelectObject to select an uninitialized
bitmap into a memory display context in order to transfer an image from
the screen to the bitmap. Now while it is processing WM_PAINT, BLOWUP
selects the clipboard's bitmap into the memory display context in order to
transfer the bitmap image to its own client area.

BLOWUP gets the dimensions of the bitmap by a call to GetObject. This
function copies information about the bitmap into the bm variable, which
is a structure of type BITMAP. At this point, the only things that we are
interested in are the bm.bmWidth (the width) and bm.bmHeight (the height)
fields of the bitmap.

It is the StretchBlt call again that does the transfer, this time from
the memory display context to the screen display context. The source width
and height are the dimensions of the bitmap; the destination width and
height are the dimensions of BLOWUP's client area. StretchBlt then blows
up or shrinks the image appropriately. StretchBlt takes a little time to
execute, particularly when working with large display surfaces. It is for
this reason that BLOWUP sets the cursor to the IDC_WAIT cursor, also known
as the hourglass, during the transfer.


The Stretching Mode

You'll notice in BLOWUP.C that immediately before the call to StretchBlt,
there is a call to SetStretchBltMode.The "stretching mode" is an attribute
of the display context and governs what Windows does when it uses
StretchBlt to reduce the size of an image. You might think that Windows
simply throws away rows and columns of pixels, but that's not what happens
in the default case.

By default, Windows uses a stretching mode, which is called BLACKONWHITE.
When an image is reduced, Windows combines rows or columns of pixels by
performing a logical AND operation between adjacent bits. A particular
pixel ends up as white (a 1-bit) only if all the pixels being combined
into that pixel are also white. This will preserve a black image on a
white background.

The opposite of this is the stretching mode which is called WHITEONBLACK.
Windows here performs a logical OR operation between adjacent pixels.
The result is a black pixel (a 0-bit) only if all the adjacent pixels are
also black. This preserves white images on a black background.

These two stretching modes can have some strange effects. For instance,
suppose you had a display context that was colored gray, which for most
display adapters is accomplished by alternating black and white pixels. If
you used StretchBlt to reduce the size of the image in half, then the
default stretching mode, known as BLACKONWHITE, would cause the result to
be entirely black. The WHITEONBLACK stretching mode would make it white.

The third option is COLORONCOLOR. This is the stretching mode that causes
Windows to do what you might have thought it was doing anyway──throw away
rows and columns of pixels. A gray image is copied as gray. This is
probably the best approach when a program has no knowledge of the
type of image it will be dealing with. (Alternatively, you could add a
menu option in BLOWUP to allow changing the stretching mode yourself.)


A Few Restrictions

I've been careful to state that BLOWUP can capture almost anything you see
on the screen. If a pull-down menu is displayed, clicking the mouse in
BLOWUP's client area to start the capture will shift the input focus to
BLOWUP and the menu will go away. If a system modal dialog box (a dialog
box that does not allow the user to switch to another application) is
displayed, then the dialog box must be exited before you can use BLOWUP.

Microsoft Windows Version 1.04 can't create bitmaps larger than 64Kb. If,
however, you attempt to capture an entire 8-color high-resolution 84Kb EGA
screen to the clipboard, the call CreateCompatibleBitmap will fail, and
BLOWUP will beep to indicate the problem.

You'll be pleased to know that Windows Version 2.0 doesn't have this
limitation, and Figure 5☼ proves it. Figure 5 may look like a typical
Windows 2.0 desktop, but it's actually BLOWUP maximized to use the full
screen after capturing an 84Kb screen image.


Figure 2:  BLOWUP make-file

blowup.obj : blowup.c
       cl -c -d -D LINT_ARGS -Gsw -Os -W2 -Zdp blowup.c

blowup.exe : blowup.obj blowup.def
       link4 blowup, /align:16, /map, /line, slibw, blowup
       mapsym blowup


Figure 3:  BLOWUP.C source code file

/* BLOWUP.C -- Capture Screen Image to Clipboard by Charles Petzold */

#include <windows.h>
#include <stdlib.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 [] = "Blowup" ;
     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         = NULL ;
          wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
          wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ;
          wndclass.lpszMenuName  = NULL ;
          wndclass.lpszClassName = szAppName ;

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

     hWnd = CreateWindow (szAppName, szAppName,
                    WS_TILEDWINDOW,
                    0, 0, 0, 0,
                    NULL, NULL, hInstance, NULL) ;

     ShowWindow (hWnd, nCmdShow) ;
     UpdateWindow (hWnd) ;

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

void InvertBlock (hWnd, org, len)
     HWND  hWnd ;
     POINT org, len ;
     {
     HDC   hDC ;

     hDC = CreateDC ("DISPLAY", NULL, NULL, NULL) ;
     ClientToScreen (hWnd, &org) ;
     PatBlt (hDC, org.x, org.y, len.x, len.y, DSTINVERT) ;
     DeleteDC (hDC) ;
     }

long FAR PASCAL WndProc (hWnd, iMessage, wParam, lParam)
     HWND           hWnd ;
     unsigned       iMessage ;
     WORD           wParam ;
     LONG           lParam ;
     {
     static BOOL    bCapturing, bBlocking ;
     static HWND    hWndNext ;
     static POINT   org, len ;
     static short   xClient, yClient ;
     BITMAP         bm ;
     HDC            hDC, hMemDC ;
     HBITMAP        hBitmap ;
     PAINTSTRUCT    ps ;

     switch (iMessage)
          {
          case WM_CREATE:
               hWndNext = SetClipboardViewer (hWnd) ;
               break ;

          case WM_SIZE:
               xClient = LOWORD (lParam) ;
               yClient = HIWORD (lParam) ;
               break ;

          case WM_LBUTTONDOWN:
               if (!bCapturing)
                    {
                    bCapturing = TRUE ;
                    SetCapture (hWnd) ;
                    SetCursor (LoadCursor (NULL, IDC_CROSS)) ;
                    }
               else if (!bBlocking)
                    {
                    bBlocking = TRUE ;
                    org = MAKEPOINT (lParam) ;
                    }
               break ;

          case WM_MOUSEMOVE:
               if (bCapturing)
                    SetCursor (LoadCursor (NULL, IDC_CROSS)) ;

               if (bBlocking)
                    {
                    len = MAKEPOINT (lParam) ;
                    len.x -= org.x ;
                    len.y -= org.y ;

                    InvertBlock (hWnd, org, len) ;
                    InvertBlock (hWnd, org, len) ;
                    }
               break ;

          case WM_LBUTTONUP:
               if (!bBlocking)
                    break ;

               bCapturing = bBlocking = FALSE ;
               SetCursor (LoadCursor (NULL, IDC_ARROW)) ;
               ReleaseCapture () ;

               if (len.x == 0 || len.y == 0)
                    break ;

               hDC = GetDC (hWnd) ;
               hMemDC = CreateCompatibleDC (hDC) ;
               hBitmap = CreateCompatibleBitmap (hDC,
                               abs (len.x), abs (len.y)) ;
               if (hBitmap)
                    {
                    SelectObject (hMemDC, hBitmap) ;
                    StretchBlt (hMemDC, 0, 0, abs (len.x), abs (len.y),
                         hDC, org.x, org.y, len.x, len.y, SRCCOPY) ;

                    OpenClipboard (hWnd) ;
                    EmptyClipboard () ;
                    SetClipboardData (CF_BITMAP, hBitmap) ;
                    CloseClipboard () ;
                    }
               else
                    MessageBeep (0) ;

               DeleteDC (hMemDC) ;
               ReleaseDC (hWnd, hDC) ;
               break ;

          case WM_PAINT:
               hDC = BeginPaint (hWnd, &ps) ;
               OpenClipboard (hWnd) ;

               if (hBitmap = GetClipboardData (CF_BITMAP))
                    {
                    SetCursor (LoadCursor (NULL, IDC_WAIT)) ;

                    hMemDC = CreateCompatibleDC (hDC) ;
                    SelectObject (hMemDC, hBitmap) ;
                    GetObject (hBitmap, sizeof (BITMAP), (LPSTR) &bm) ;

                    SetStretchBltMode (hDC, COLORONCOLOR) ;
                    StretchBlt (hDC, 0, 0, xClient, yClient,
                                hMemDC, 0, 0, bm.bmWidth, bm.bmHeight,
                                                  SRCCOPY) ;

                    SetCursor (LoadCursor (NULL, IDC_ARROW)) ;
                    DeleteDC (hMemDC) ;
                    }
               CloseClipboard () ;
               EndPaint (hWnd, &ps) ;
               break ;

          case WM_DRAWCLIPBOARD :
               if (hWndNext)
                    SendMessage (hWndNext, iMessage, wParam, lParam) ;

               InvalidateRect (hWnd, NULL, TRUE) ;
               break;

          case WM_CHANGECBCHAIN :
               if (wParam == hWndNext)
                    hWndNext = LOWORD (lParam) ;

               else if (hWndNext)
                    SendMessage (hWndNext, iMessage, wParam, lParam) ;
               break ;

          case WM_DESTROY:
               ChangeClipboardChain (hWnd, hWndNext) ;
               PostQuitMessage (0) ;
               break ;

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

MAKE File for BLOWUP

blowup.obj : blowup.c
     cl -c -d -D LINT_ARGS -Gsw -Os -W2 -Zdp blowup.c

blowup.exe : blowup.obj blowup.def
     link4 blowup, /align:16, /map /line, slibw, blowup
     mapsym blowup


Figure 4:  BLOWUP.DEF Module Definition File

NAME           BLOWUP
DESCRIPTION    'Capture Screen Image to Clipboard by Charles Petzold'
STUB           'WINSTUB.EXE'
CODE           MOVABLE
DATA           MOVABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      4096
EXPORTS        WndProc

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

Increase the Performance of Your Programs with a Math Coprocessor

Marion Hansen and Lori Sargent☼

The microprocessor in your personal computer's CPU is powerful, but it
wasn't designed to handle complex math operations rapidly. Whether it's an
8086, 8088, 80286, or 80386, your microprocessor will perform floating-
point and transcendental calculations far more quickly and with greater
precision if a math coprocessor is linked to it. Coprocessors also have
capabilities useful for business computing. A coprocessor can process
binary coded decimal numbers up to 18 digits long without round-off
errors, perform arithmetic on integers from 2 x 10-9 through 2 x 109, and
carry out math functions on real numbers as small as 3.4 x 10-4932 or as
large as 1.1 x 104932.

When software written to use a coprocessor performs certain types of math,
it engages the coprocessor rather than the microprocessor. The coprocessor
performs the calculation and returns the answer to the microprocessor.
This entire process takes a fraction of the time required by the
microprocessor executing alone. To give you an idea of how fast a
coprocessor is, Figure 1 compares spreadsheet recalculation times with and
without an 8087 math coprocessor.

Besides performing certain kinds of math faster, coprocessors also save
programming time. Because trigonometric, logarithmic, and exponential
functions are built into the coprocessor's hardware, the programmer
doesn't have to write these routines. And with the routines in the chip
instead of in the code, programs are smaller. Coprocessors generate
instructions for many numeric operations such as number conversions,
arithmetic operations, and transcendental functions (tangents, exponents,
and logarithms).

A coprocessor is the most cost-effective way to increase number-crunching
power. For a fraction of the cost of an accelerator board, a coprocessor
can dramatically speed up floating-point calculations. And it won't
consume a precious expansion slot (or two), since it fits in a socket
already on the motherboard.

Math coprocessors come in three varieties: the 8087 (for 8086- and 8088-
based computers), the 80287 (for 80286-based computers), and the 80387
(for 80386-based machines). The 8087 and 80287 are both available in three
different speeds. The speed you need depends on how fast your computer
drives the coprocessor socket, not on the speed of your microprocessor.
For example, some 10-MHz computers drive the socket at 8 MHz and thus need
a coprocessor that runs at 8 MHz, not 10 MHz. If you aren't sure which
speed is correct for your computer, contact the manufacturer.

Hundreds of application programs have been written to take advantage of
the coprocessor's speed and precision, including business, engineering,
graphics, statistical, and econometric packages. Many compilers and
assemblers can benefit from a coprocessor as well. Using a coprocessor
with one of these programs couldn't be easier, because all interfacing
between microprocessor and coprocessor is built in. The only difference
you'll notice is the increased speed.


Development Tools

Most of today's compilers and assemblers can generate coprocessor code.
This includes all recent versions of Microsoft C, Pascal, and FORTRAN
compilers, as well as Borland's Turbo Pascal. No matter which of these
languages you're writing in, incorporating complex math into programs is
not difficult with a coprocessor.

In a high-level language, using the coprocessor is quite painless.
Coprocessor instructions such as sine, square root, hyperbolic tangent,
and log are built into manufacturer-supplied library routines.

Assembly language programmers using Microsoft's Macro Assembler Version
1.25 or later have the option of writing code that explicitly references
coprocessor instructions or implicitly does so by linking in a math
library such as those supplied with the Microsoft C, Pascal, or FORTRAN
compilers. In addition, a number of other software vendors market
specialized math libraries that perform many math functions and can be
linked to programs written in a variety of languages.

Although programs can usually call math library routines with or without a
coprocessor, programs running on systems with a coprocessor will execute
significantly faster. Figure 2 illustrates how much faster an 8-MHz
computer performs floating-point instructions on typical spreadsheet data
when a coprocessor is installed.

Most high-level languages link an emulation library into any program that
contains floating-point instructions or data. Code to check for the
presence of the coprocessor is generated at run time. If a coprocessor is
detected, it is used. If a coprocessor is not present, the emulation
library is used. This way, programs written to take advantage of a
coprocessor can run on systems without one.

Debugging code that contains coprocessor instructions is not much
different from debugging code written for the microprocessor alone. A good
debugger, such as the CodeView facility included in Microsoft's C Compiler
Version 4.0, lets you examine and change all the coprocessor registers,
including status and control registers. CodeView displays data register
contents in both their 80-bit internal hexadecimal form and their decimal
equivalents. This makes debugging floating-point instructions no more
difficult than debugging microprocessor instructions.


Synergy

The coprocessor is an extension of the microprocessor. (In fact, Intel
calls the coprocessor the numeric processor extension, or NPX.) They share
the same buses and memory. The microprocessor's status lines and queue
status lines are directly connected to the coprocessor, so the coprocessor
is able to track the microprocessor's instruction queue. The coprocessor
monitors and decodes instructions without any overhead. It reads each
instruction into its queue but executes only its own instructions,
treating each microprocessor instruction as a no-operation (NOP) command.
In turn, the microprocessor treats each coprocessor instruction as a NOP
and executes only its own instructions. The microprocessor controls
program execution, and the coprocessor controls numeric operations.

Instead of the 8-bit registers in the 8088, the 16-bit registers in the
8086 and 80286, or the 32-bit registers in the 80386, the coprocessor has
80-bit data registers, which allow it to hold more information. The
coprocessor's registers were designed to hold specific types of data and
are significantly different from the microprocessor's general-purpose
registers. Nonetheless, the two chips can still share data through common
memory.


Data Types

Coprocessor registers were designed to store 80-bit floating-point
numbers. This format, which Intel calls temporary real, is compatible with
the proposed IEEE 754 floating-point standard. A temporary real number is
composed of a sign bit, a 15-bit exponent, and a 64-bit significand.
Although the coprocessor stores all data as temporary real numbers, it can
also read and write data in six other formats: packed decimal, long real,
long integer, short real, short integer, and word integer (see Figure 3).
Coprocessor load and store instructions automatically convert the other
six data types to temporary real format and back again. Microsoft's Macro
Assembler allows these formats to be declared with the directives DW (word
integer), DD (short integer and short real), DQ (long integer and long
real), and DT (packed binary coded decimal and temporary real).

The coprocessor stores numbers in normalized format (scientific notation).
A number is normalized by shifting the 1 that's furthest to the left up or
down until it occupies bit 63. The coprocessor assumes the number in the
significand is a real number between 1 and 2. The exponent field specifies
how far the digits must be shifted to get the correct number back. Because
the exponent is stored as an unsigned value, an offset (bias) is added to
it so negative numbers can be represented. This lets the coprocessor
compare the magnitude of two numbers without first performing arithmetic
on the exponents, and execution time is thus shortened.


Registers

Coprocessor computations occur in eight data registers. The registers can
be accessed as a LIFO (last-in-first-out) stack, with instructions
operating on the top one or two stack elements. Or the registers can act
as a fixed register set, with instructions operating on explicitly
designated registers.

Unlike those of the microprocessor, the coprocessor's data registers don't
have unique names. They're treated as indexed entries in a stack, with the
top of the stack designated as register ST(0) and the others designated
ST(1) and so on. Values are loaded into the coprocessor by pushing them
onto the stack, and some (but not all) are retrieved by popping them off.
Many coprocessor instructions operate only on the top of the stack. Most
other instructions default to operating on the stack's top. All register
addresses are relative to the top of the stack.

A 3-bit top-of-stack pointer in another type of register──the status word
register──identifies the current top-of-stack register. A push decrements
the value in this pointer by 1 and loads a value into the new top
register. A pop increments the value in the pointer by 1 and removes the
value from the register currently at the top. The stack is circular and
can be overwritten if not managed properly.

All the coprocessor's numeric opcodes (as opposed to control opcodes) use
the top of the stack as at least one operand. Some instructions operate
only on the top of the stack, while others operate on both the top and the
second stack register. Some take their second operand from another stack
register, and others can take their second operand from memory.

Besides the eight data registers, the 8087 has five other registers
accessible to the programmer, each 16 bits in size: status word, control
word, tag word, operand pointer, and instruction pointer.

The status word can be thought of as a flag register (see Figure 4). It
contains a busy indicator, a top-of-stack pointer, condition codes, and
exception indicators. To read the status word from Microsoft C, call the
built-in _status87 function. To read the status word from the coprocessor
in assembler, execute an FSTSW instruction to write the status word to
memory where the microprocessor can examine it.

The control word defines how the coprocessor should react to different
exception conditions (see Figure 5). It also defines the precision, how
the results will be rounded, and whether signed or unsigned infinity will
be used. The control word register has three control fields and six
exception masks. Masking the exception bit tells the coprocessor to handle
all occurrences of this exception; leaving it unmasked means that the
programmer will have to handle the exceptions.

In assembly language, control words are sent to the coprocessor by writing
them to a memory location and having the coprocessor execute an instruction
that reads in the control word from memory. Programmers using a high-level
language can check their library reference guide to see how this is
implemented in the library they are using. For programmers who do not care
to set these fields, Intel provides a set of default control conditions. The
default settings are: exceptions masked, interrupts masked, 64-bit
precision, rounding to the nearest number, and projective infinity.

The tag word contains information about the contents of each data register
(see Figure 6). This information is used by the coprocessor primarily to
optimize performance. The coprocessor stores 2 bits for each data register,
for a total of four possible tag values.

The coprocessor uses the tag word to keep track of the contents of each of
its data registers and to report invalid results. The coprocessor also
uses the tag word to maintain stack integrity information. For example, if
a register tagged as empty (tag value = 11) is popped from the stack, the
coprocessor detects stack underflow. Similarly, the coprocessor uses the
tag word to detect stack overflow when new data is stored in a register
that wasn't previously empty. Stack underflow and overflow trigger an
invalid operation exception. Programmers can mask or unmask this exception
(the default is masked). If either stack underflow or overflow occur and
the invalid operation exception is masked, the coprocessor adjusts the
stack pointer and returns a standard result to indicate that the value is
not meaningful.

The operand pointer and instruction pointer registers provide information
about the instruction and data that caused an exception and are used with
user-written error handlers. Most programmers do not employ these
registers, however, preferring to let the coprocessor handle exceptions.

Unlike the status word and control word, the tag word, operand pointer,
and instruction pointer cannot be accessed directly. These registers are
accessed indirectly by writing to memory either the coprocessor's
environment (using FSTENV) or the coprocessor's state (using FSAVE). The
14-byte coprocessor environment consists of the status word, control word,
tag word, instruction pointer, and operand pointer. The 94-byte
coprocessor state includes everything in the environment plus the eight
coprocessor data registers (see Figure 7). The format of the coprocessor
state and environment depends on the coprocessor's operating mode.

When an exception occurs while the coprocessor is in real mode, it
supplies the 20-bit addresses of the offending instruction and its memory
operand (if any), plus the 11 low-order bits of the opcode. In protected
mode (with the 80287 and 80387 only), the coprocessor supplies the
selectors and offsets of the offending instruction and its memory operand
(if any). Although the 80287/80387 real-mode exception pointers have the
same format as the 8087 exception pointers, the 80287/80387 instruction
pointer indicates any prefixes preceding the opcode. In contrast, the 8087
instruction pointer indicates the escape (ESC) instruction opcode.


Exceptions

The coprocessor recognizes six exception conditions: invalid operation,
denormalized operand, division by zero, numeric overflow, numeric
underflow, and inexact result. The coprocessor's exception masks give
programmers the choice of trapping exceptions themselves or having the
coprocessor return a fixed value. When an exception occurs during
execution of a coprocessor instruction, the coprocessor sets the
appropriate bit in its status register. The coprocessor then checks its
control register to determine whether or not that type of exception is
masked. If the exception is masked, then the coprocessor uses its on-chip
logic to return a result. The exception indicator bits in the status
register will hold their value until they are explicitly cleared with
either a FINIT or FCLEX instruction. Consequently, with exceptions masked,
programmers do not have to check the status register after every
instruction. Checking the exception indicator bits periodically ensures
accurate results.

The other method of handling exceptions is to unmask one or more of the
exception bits and clear the coprocessor's interrupt enable mask. Under
these conditions, an exception will trigger an interrupt request. It is up
to the programmer to write the interrupt handler that will respond to such
requests. The coprocessor contains a lot of built-in support for writing
such routines.


Instructions

Coprocessor instructions fall into six categories: data transfer, loading
constants, transcendental calculations, comparison, arithmetic, and
processor control. A coprocessor instruction can be written in assembler
in either of two ways: as a microprocessor ESC instruction followed by a
number (for example, ESC 0BH) or as a specific coprocessor mnemonic
(FSTP). All versions of Microsoft assemblers later than 1.25 accept
coprocessor mnemonics; ESC instructions are needed only for older
assemblers that do not, and are thus rarely used. Programmers writing in
high-level languages needn't worry about the format of coprocessor
instructions──the compiler will take care of everything.

A coprocessor mnemonic takes the form of a sequence of letters beginning
with an F. Figures 8 and 9 give examples of these instructions
incorporated into assembly language programs. Figure 10 is a sample of
code created by Microsoft's C Compiler Version 4.0. While the coprocessor
instructions are not apparent in the source code, you will see them if you
compile the program with the /Fc option and look at the .COD file.

If an instruction starts with 11011 the microprocessor recognizes it as a
coprocessor instruction and responds by generating any necessary operand
addresses, putting them on the address bus, and ignoring the coprocessor
opcode. The microprocessor then continues fetching and executing
instructions unless it is instructed to wait for the coprocessor to
complete its task.

Since the microprocessor and the coprocessor can work on separate tasks
simultaneously, they can overwrite each other's data or miss instructions
unless they're synchronized. All high-level languages automatically
synchronize the activity of the two chips, while assembly language
programmers must do so explicitly. In exchange for the extra programming
effort, however, assembly language programmers get more flexibility
(carefully managed concurrency) and faster performance.

The 80286 and 80386 have instruction synchronization built in, but this is
not true of the 8088 and 8086 or any of the coprocessors. Consequently,
programmers must on occasion insert an FWAIT after a coprocessor store
instruction.

When you are using escape sequences, specify the FWAIT instruction when
the microprocessor must wait for data from the coprocessor. All floating-
point mnemonics have an FWAIT as their first byte, so it isn't necessary
to code one explicitly. (A few coprocessor instructions assume an FN<op>
form, which keeps the assembler from generating an FWAIT instruction.)

In addition to synchronizing data, the 8086 and 8088 must also synchronize
8087 instructions. Because the coprocessor gets its instructions by
monitoring them as they go into the microprocessor prefetch queue, the
8087 can miss an instruction if it is busy executing while the 8086/8088
is fetching and executing.

Any program that uses a coprocessor should also be able to run without
one. Before the software tries to use the coprocessor, it should check to
see if there is one. It can easily do this by attempting to initialize the
coprocessor and then attempting to read the coprocessor's control word
after the initialization. (If a coprocessor is present, the control word
will be set to the default value specified by Intel. Many software
libraries have this checking function built in. If a coprocessor is not
found, the program should call an emulation library to handle coprocessor
instructions or should gracefully exit. Figure 11 provides an example of
this type of program.


Real Vs. Protected

The 8087 operates only in real mode, while the 80287 and 80387 can operate
in either real or protected mode. All programs written to use the 8087 are
compatible with the 80287 and 80387 in real mode. Executing the privileged
SETPM instruction will place the 80287 or 80387 in protected mode. They
can then be returned to real mode only by a hardware reset.

The microprocessor's operating mode affects coprocessor code in two areas:
exception handling and memory accesses. The memory image of the
instruction pointer and data pointer following an FSTENV or FSAVE
instruction depends on the coprocessor's operating mode (see Figures 8 and
9). Any code that examines this information must consider the operating
mode for accurate interpretation. In protected mode, Interrupt Vector 16
is dedicated to the numeric exception handling routine. Coprocessor
instructions that result in exception conditions will trigger an Interrupt
16 if the exception is unmasked. Protected mode also has a built-in
mechanism for handling coprocessor instructions when a coprocessor is not
present (or if its absence is being emulated). Interrupt 7 is
automatically triggered if a coprocessor ESC sequence is executed and the
emulation bit (EM) of the microprocessor's machine status word is set.
This built-in trapping can help programmers systematically include
emulation code in their programs.

MS(R) OS/2, Microsoft's new protected-mode version of MS-DOS(R), offers basic
exception handling for coprocessors by supporting the exception handling
capabilities of the 80287 and 80387. It doesn't supply a standard
emulation library for coprocessors; this must be provided by the
compiler.

When in protected mode, the microprocessor checks all memory accesses
(including coprocessor operands) for violations of protection rules.
Coprocessor applications running in protected mode must comply with
protected-mode memory management regulations. Any violations cause either
dedicated Interrupt 13 (when the violation occurs on the first word of the
numeric operand) or dedicated Interrupt 9 (when the violation occurs on
subsequent words).

If you want to port an 8087 program to a protected-mode system, consider
reassembling the program on an 80286/80386 assembler. This removes the
redundant FWAITs and usually gives you a more compact code image. In
addition, make the following changes to the 8087 program:

  ■  Delete interrupt-controller-oriented instructions in numeric exception
     handlers.

  ■  Delete 8087 instructions FENI/FNENI (enable interrupts) and
     FDISI/FNDISI (disable interrupts). The 80287 and 80387 ignore these
     instructions, so none of the 80287/80387 internal states will be
     updated.

  ■  Be sure Interrupt Vector 16 points to the numeric exception handling
     routine.

  ■  Include a microprocessor exception handler for an Interrupt 7, which
     will occur during the execution of coprocessor instructions if the
     microprocessor's machine status word contains the settings TS=1 (task
     switched) or EM=1 (emulation).

  ■  Include a microprocessor exception handler for Interrupt 9 (which
     occurs when the second or later word of a floating-point operand falls
     outside a segment) and Interrupt 13 (caused by the starting address of
     a numeric operand falling outside a segment).


Figure 1:  Recalculating Lotus 1-2-3(tm) spreadsheets on an IBM(r) PC can
           usually be done much more quickly with an 8087 coprocessor.

  Standard│░░
 Deviation│▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
          │
 Exponents│░░░
          │▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
          │
 Multiply/│░░░░░░░░░░░░░░░░░                 ┌────────────────────┐
    Divide│▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓               │ ░░ With an 8087    │
          │                                  │                    │
      Add/│░░░░░░░░░░░░░░░░░░░               │ ▓▓ Without an 8087 │
  Subtract│▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓               └────────────────────┘
          ├──┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
          0  10  20  30  40  50  60  70  80  90 100 110 120 130


Figure 2:  Calculation times for floating-point instructions decrease
           dramatically when a coprocessor is added to an 8-MHz IBM PC.

Instruction                Approximate Execution Time (in microseconds)

                               With an 8087        Without an 8087

Add/Subtract                       10.6                 1,000.0
Multiply (short real nos.)         11.9                 1,000.0
Multiply (temporary real nos.)     16.9                 1,312.5
Divide                             24.4                 2,000.0
Compare                             5.6                   812.5
Load (long real nos.)               6.3                 1,062.5
Store (long real nos.)             13.1                   750.0
Square Root                        22.5                12,250.0
Tangent                            56.3                 8,125.0
Exponentiation                     62.5                10,687.5


Figure 3:  The coprocessor can recognize seven numeric formats, which make
           use of up to 80 bits.

 79   78    64 63                                                0
┌────┬────────┬───────────────────────────────────────────────────┐
│Sign│Biased  │                     Significand                   │Temporary
│Bit │Exponent│                                                   │Real
└────┴(3FFFH)─┴───────────────────────────────────────────────────┘

 79   78  72                                                     0
┌────┬────┬───────────────────────────────────────────────────────┐
│Sign│Not │ d                Packed Decimal Digits            d   │Packed
│Bit │Used│  17 │  •  •  •  •  •  •  •  •  •  •  •  •  •  •  │  0 │BCD
└────┴────┴─────┴────────────────────────────────────────────┴────┘

              63   62    52 51                                   0
             ┌────┬────────┬──────────────────────────────────────┐
             │Sign│Biased  │              Significand             │Long Real
             │Bit │Exponent│                                      │
             └────┴─(3FFH)─┴──────────────────────────────────────┘

              63                                                 0
             ┌────────────────────────────────────────────────────┐
             │                          2's                       │Long
             │                      Compliment                    │Integer
             └────────────────────────────────────────────────────┘

                              31   30     23 22                  0
                             ┌────┬─────────┬─────────────────────┐
                             │Sign│ Biased  │     Significand     │Short
                             │Bit │ Exponent│                     │Real
                             └────┴──(7FH)──┴─────────────────────┘

                              31                                 0
                             ┌────────────────────────────────────┐
                             │                  2's               │Short
                             │              Compliment            │Integer
                             └────────────────────────────────────┘

                                                  15             0
                                                 ┌────────────────┐
                                                 │                │Word
                                                 │                │Integer
                                                 └────────────────┘


Figure 4:  The coprocessor's 16-bit status word register serves as a flag
           register.

Reserved───────────────────────────┐
Condition                          │ Exception Flags (1=Exception occurred)
  Codes──────┬─────────┬──┬──┐     │
Stack-Top    │         │  │  │     │
  Pointer───────┬─┬──┐ │  │  │     │
Busy───────┐ │  │ │  │ │  │  │     │
       15 ╔╧╤╧═╤╧╤╧═╤╧╤╧═╤╧═╤╧═╤══╤╧╤══╤══╤══╤══╤══╤══╗ 0
          ║B│C3│ │ST│ │C2│C1│CO│IR│X│PE│UE│OE│ZE│DE│IE║
          ║ │  │ │  │ │  │  │  │ES│ │  │  │  │  │  │  ║
          ╚═╧══╧═╧══╧═╧══╧══╧══╧═╤╧═╧═╤╧═╤╧═╤╧═╤╧═╤╧═╤╝
                                 │    │  │  │  │  │  └─Invalid Operation
                                 │    │  │  │  │  └────Denormalized Operand
                                 │    │  │  │  └───────Divide By Zero
                                 │    │  │  └──────────Numeric Overflow
                                 │    │  └─────────────Numeric Underflow
                                 │    └────────────────Precision
                                 │
                                 │                   ┌─{Interrupt Request
                                 │                   │   (8027)
                                 └───────────────────┤ {Error Summary Status
                                                     └─  (80287)


Figure 5:  The control word register governs how the coprocessor reacts to
           exception conditions.

Infinity Control───┐               Exception Masks (1=Exception masked)
Rounding Control───────┐
                   │   │
Precision Control────────────┐
            15     │   │     │                          0
            ╔═╤═╤═╤╧═╤═╪══╤══╪══╤═══╤═╤══╤══╤══╤══╤══╤══╗
            ║X│X│X│IC│R│C │ P│C │IEM│X│PM│UM│OM│ZM│DM│IM║
            ╚╤╧╤╧╤╧══╧═╧══╧══╧══╧╤═╤╧═╧═╤╧═╤╧═╤╧═╤╧═╤╧╤═╝
             ├─┴─┴───────────────┘ │    │  │  │  │  │ │
             │                     │    │  │  │  │  │ └─Invalid Operation
Reserved─────┘                     │    │  │  │  │  └───Denormalized Operand
                                   │    │  │  │  └──────Divide By Zero
                                   │    │  │  └─────────Numeric Overflow
                                   │    │  └────────────Numeric Underflow
                                   │    └───────────────Precision
                                   │
                                   │                  ┌─{Interrupt Enable
                                   └──────────────────┤  (8087)
                                                      └─{Reserved (80287)


Figure 6:  The tag word holds information about the contents of each data
           register.

┌───────────────────────────────────────────────────────────────────────┐
│                                                                       │█
│  15                                                                0  │█
│  ╔════════╤═══════╤═══════╤═══════╤═══════╤═══════╤═══════╤════════╗  │█
│  ║ Tag (7)│Tag (6)│Tag (5)│Tag (4)│Tag (3)│Tag (2)│Tag (1)│Tag (0) ║  │█
│  ╚════════╧═══════╧═══════╧═══════╧═══════╧═══════╧═══════╧════════╝  │█
│                                                                       │█
│           Tag Values:   00 = valid (i.e., any finite nonzero number)  │█
│                         01 = zero                                     │█
│                         10 = invalid (i.e., NaN or infinity)          │█
│                         11 = empty                                    │█
│                                                                       │█
└───────────────────────────────────────────────────────────────────────┘█
  ████████████████████████████████████████████████████████████████████████


Figure 7:  The coprocessor environment consists of the contents of all
           registers but the data registers. The coprocessor state includes
           all registers.


                                 Real Mode
                         ┌─────────────────────────────────────┐
                         │             Control Word            │
                         ├─────────────────────────────────────┤
                         │             Status Word             │
                         ├─────────────────────────────────────┤
         Coprocessor     │              Tag Word               │
                         ├─────────────────────────────────────┤
            State        │     Instruction Address (15-0)      │
                         ├───────────────┬─────┬───────────────┤
                         │  Instruction  │  0  │  Instruction  │
                         │Address (19-16)│     │ Opcode (10-0) │
                         ├───────────────┴─────┴───────────────┤
                         │        Operand Address (15-0)       │
                         ├──────────────────┬──────────────────┤
                         │     Operand      │        0         │
                         │ Address (19-16)  │                  │
          ┌──────────────┴15──────────────11┴12───────────────0┤
          │                        ST (0)                      │
          ├────────────────────────────────────────────────────┤
          │                        ST (1)                      │
          ├────────────────────────────────────────────────────┤
          │                        ST (2)                      │
          ├────────────────────────────────────────────────────┤
          │                        ST (3)                      │
          ├────────────────────────────────────────────────────┤
          │                        ST (4)                      │
          ├────────────────────────────────────────────────────┤
          │                        ST (5)                      │
          ├────────────────────────────────────────────────────┤
          │                        ST (6)                      │
          ├────────────────────────────────────────────────────┤
          │                        ST (7)                      │
          └79─────────────────────────────────────────────────0┘

                               Protected Mode
                         ┌─────────────────────────────────────┐
                         │             Control Word            │
                         ├─────────────────────────────────────┤
                         │             Status Word             │
                         ├─────────────────────────────────────┤
        Coprocessor      │              Tag Word               │
                         ├─────────────────────────────────────┤
        Environment      │          Instruction Offset         │
                         ├─────────────────────────────────────┤
                         │         Instruction Selector        │
                         ├─────────────────────────────────────┤
                         │            Operand Offset           │
                         ├─────────────────────────────────────┤
                         │           Operand Selector          │
          ┌──────────────┴15──────────────────────────────────0┤
          │                        ST (0)                      │
          ├────────────────────────────────────────────────────┤
          │                        ST (1)                      │
          ├────────────────────────────────────────────────────┤
          │                        ST (2)                      │
          ├────────────────────────────────────────────────────┤
          │                        ST (3)                      │
          ├────────────────────────────────────────────────────┤
          │                        ST (4)                      │
          ├────────────────────────────────────────────────────┤
          │                        ST (5)                      │
          ├────────────────────────────────────────────────────┤
          │                        ST (6)                      │
          ├────────────────────────────────────────────────────┤
          │                        ST (7)                      │
          └79─────────────────────────────────────────────────0┘


Figure 8:  This assembly language program uses coprocessor instructions
           to calculate the circumference of a circle with a given radius.
           Each of these instructions begins with an F.

title   circumference

.287        ; Tell MASM there are coprocessor instructions
            ; in the program.

data    segment
    radius              DD  2.468
    circumference       DD  ?
data    ends

code    segment
    assume cs:code, ds:data
start:
    mov     ax, data        ; Initialize data segment
                            ; register
    mov     ds, ax
    finit                   ; Initialize coprocessor
    fldpi                   ; ST = pi
    fadd    st, st          ; ST = 2pi
    fld     radius          ; ST = radius
                            ; ST(1) = 2pi
    fmul    st, st(1)       ; ST = radius*2pi
    fstp    circumference   ; store result and pop
    fwait                   ; wait for store to complete
    mov     ah, 4ch         ; return to DOS
    int     21h
code ends
    end start


Figure 9:  This program uses coprocessor instructions to calculate the
           root of each element in an array of binary coded decimal
           integers. The results are stored in another binary coded
           decimal array and can easily be converted to ASCII strings for
           output.

.287                ; Indicate to MASM program contains npx code.

bcd_data    segment
    array_1 DT  1234567890, 82, 769823, 84165
            DT  246809, 1526374859, 199, 41290
            DT  98654210, 340126, 2400, 371849
    array_2 DT 12 DUP (?); storage for results
bcd_data    ends

code segment
    assume  cs:code, ds:bcd_data
start:
    mov ax, bcd_data
    mov ds, ax
    finit                   ; initialize coprocessor
                            ; assume default control word

    mov cx, length array_2  ; initialize loop counter
    mov si, 0               ; initialize index

process_array:
    fbld    array_1[si]     ; st(0) = array_1[index]
    fsqrt                   ; st(0) = sqrt (st(0))
    frndint                 ; round st(0) to integer
    fbstp   array_2[si]     ; store bcd result in
                            ; array_2[index] and
                            ; pop coprocessor stack
    add     si, 10          ; increment index to point
                            ;      to next DT array element
    loop    process_array   ; DO WHILE
                            ;      loop counter <= length array_2
exit:
    fwait                   ; make sure last store completed
    mov ah, 4ch             ; exit to dos
    int 21h

code    ends
    end start


Figure 10:  Using Microsoft C library functions to calculate sine and
            cosine, this program in C draws a circle on the screen of a
            system equipped with a graphics adapter. The coprocessor
            instructions are apparent only after compilation.

#include "stdio.h"
#include "math.h"

extern set_graph_mode();
extern set_text_mode();
extern plot_point();

#define VERTICAL_CENTER 99.5
#define HORIZONTAL_CENTER 319.5
#define PI 3.1415927

main()
    {
    char ch;
    float radians,radius,aspect_ratio;

    aspect_ratio=2.1; /* adjusts horizontal scaling to account */
                      /* for PC's "tall and skinny" pixels     */
    radius=90;


    set_graph_mode(); /* set screen to 640x200 graphics mode */

    /* step around the circle in 1/100th radian increments */
    for (radians=0; radians < 2*PI; radians=radians + 0.01)
        {
        long x,y;

        x=HORIZONTAL_CENTER+radius*aspect_ratio*cos(radians);
        y=VERTICAL_CENTER+radius*sin(radians);

/* call routine to write a pixel on the screen */
        plot_point((int)x,(int)y);
        }

/* wait for user to hit a key before erasing screen  */
    ch=getchar();

/* restore user's screen to text mode */
    set_text_mode();
    }


Figure 11:  This routine performs the same multiplication function with or
            without a coprocessor. First the program checks for the
            presence of a coprocessor. If it finds one, it executes the
            imul_32 procedure. If not, it jumps to the emulation
            procedure, emulate_imul_32.

title        math_module

.287                    ; Tell MASM that there are coprocessor
                        ; instructions here.
public  init_math
public  imul_32

    present     EQU     0
    missing     EQU     1

code    segment     public      'code'
        assume  cs:code

    cp_flag         DB   1      ; local flag
    ctrl_word   DW  0           ; for storing '87 control word

;-------------------------------------------------------------;
;  init_math: Detects math coprocessor and sets global flag   ;
;      which is used to determine whether or not to use       ;
;      coprocessor instructions or emulation code.            ;
;                                                             ;
;  This procedure must be called before the coprocessor can   ;
;  be used by math routines in this module.                   ;
;-------------------------------------------------------------;

init_math       PROC      FAR

    fninit                                ; initialize coprocessor
    fnstcw  cs:ctrl_word                  ; store '87 control word
    test    byte ptr cs:[ctrl_word+2], 03 ; if bits 8 and 9 are set
    je      yes_cp                        ; then coprocessor present
    mov     cs:cp_flag, missing           ; else no coprocessor
    jmp     init_math_exit

yes_cp:
    mov    cs:cp_flag, present

init_math_exit:
    ret
init_math   ENDP

;-------------------------------------------------------------;
;  imul_32:  Performs signed multiplication on two 32 bit     ;
;            integers. (Note: can also be used to perform     ;
;            fixed point 32 bit decimal multiplication)       ;
;                                                             ;
;        Input:   Two 32 bit integers                         ;
;                 ds:si pointer to integer A                  ;
;                 ds:di pointer to integer B                  ;
;                                                             ;
;       Output:   64-bit result returned at [es:bx]           ;
;                                                             ;
;-------------------------------------------------------------;

imul_32     PROC    FAR

    cmp     cs:cp_flag, missing      ; IF coprocessor missing
    je      emulate_imul_32          ; THEN emulate
                                     ; ELSE use coprocessor
    fild    dword ptr [si]           ; st(0)= A
    fimul   dword ptr [di]           ; st(0)=A*B
    fistp   qword ptr es:[bx]        ; store result and pop stack
    fwait                            ; wait for store to complete
    jmp     imul_32_exit             ; coprocessor done, exit

emulate_imul_32:
;--------------------------------------------------------------------;
;                                                                    ;
;  The following code computes A x B where                           ;
;                                                                    ;
;         A is a 32 bit integer composed of                          ;
;              a low word (A0) and a high word (A1) and              ;
;         B is a 32 bit integer composed of                          ;
;              a low word (B0) and a high word (B1)                  ;
;                                                                    ;
;  The result is calculated by summing the partial products of       ;
;  individual 16 bit unsigned multiplies.  The final result is       ;
;  sign adjusted.                                                    ;
;                                                                    ;
;--------------------------------------------------------------------;
;
    push    ax                      ; save caller's state
    push    cx
    push    dx
    push    bp

A0_x_B0:
    mov     ax, [si]                ; ax=A0
    mul     word ptr [di]           ; dx=A0B0H, ax=A0B0L
    mov     es:[bx], ax             ; store A0B0L - 4th column sum
    mov     cx, dx                  ; cx=A0B0H
A1_x_B0:
    mov     ax, [si+2]              ; ax=A1

    mul     word ptr [di]           ; dx=A1B0H, ax=A1B0L
    push    bx                      ; running out of registers,
                                    ;    reuse bx
    mov     bx, ax                  ; bx=A1B0L
    mov     bp, dx                  ; bp=A1B0H


A0_x_B1:
    mov     ax, [si]                ; ax=A0
    mul     word ptr [di+2]         ; dx=A0B1H, ax=A0B1L
    add     cx, bx                  ; cx=A0B0H+A1B1L
    adc     cx, ax                  ; cx=A0B0H+A1B1L+A0B1L+carry
    pop     bx                      ; need pointer back
    mov     es:[bx+2], cx           ; store 3rd column sum
    push    bx                      ; still short of register space
    xor     bx, bx
    adc     bx, 0                   ; save carry information
    mov     cx, dx                  ; cx=A0B1H
A1_x_B1:
    mov     ax, [si+2]              ; ax=A1
    mul     word ptr [di+2]         ; dx=A1B1H, ax=A1B1L
    add     cx, bx                  ; cx=A0B1H+stored carry
    adc     cx, bp                  ; cx=A0B1H+A1B0H
    adc     cx, ax                  ; cx=A0B1H+A1B0H+A1B1L+carry
    pop     bx                      ; restore pointer
    mov     es:[bx+4], cx           ; store 2nd column sum
    adc     dx, 0                   ; dx=A1B1+carry
    mov     es:[bx+6], dx           ; store 1st column sum
; now adjust for negative numbers
test_A:
    mov     ah, [si+2]              ; ah=high byte of A
    or      ah, ah                  ; IF A is negative
    js      subtract_B              ; THEN subtract B from
                                    ;        high DD of result
test_B:
    mov     ah, [di+2]              ; ah=high byte of B
    or      ah, ah                  ; IF B is negative
    js      subtract_A              ; THEN subtract A from
                                    ;        high DD of result
    jmp     emulate_done
subtract_B:
    mov     ax, [di]                ; ax=B0
    mov     cx, [di+2]              ; cx=B1
    sub     es:[bx+4],ax            ; adjust the two high words
    sbb     es:[bx+6],cx
    jmp     test_B
subtract_A:
    mov     ax, [si]                ; ax=A0
    mov     cx, [si+2]              ; cx=B1
    sub     es:[bx+4], ax           ; adjust the two high words
    sbb     es:[bx+6], cx
emulate_done:
    pop     bp                      ; restore caller's state
    pop     dx
    pop     cx
    pop     ax

imul_32_exit:
    ret
imul_32 ENDP

code ends
    end

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

TIFF: An Emerging Standard for Exchanging Digitized Graphic Images

───────────────────────────────────────────────────────────────────────────
Also see the related article:
  The CCITT/3 Scheme in TIFF
───────────────────────────────────────────────────────────────────────────

Nancy Andrews and Stan Fry☼

In the early days of microcomputers, PC users considered themselves lucky to
have a paint program with basic drawing tools. Users could create simple
drawings for documents, print them, and paste them in by hand.
Unfortunately, the end results looked amateurish and, for many purposes,
were barely acceptable.

The difficulty was what if users wanted something more complex than what the
available tools and their drawing skills allowed? What if they wanted to use
existing line art or photographs? Users really needed to have the ability to
digitize the professionally drawn art they had on paper and then
electronically paste it into their documents.

The arrival of scanner hardware solved this problem. Most scanners could
scan an existing piece of art or a photograph at an amazing 300 dots per
inch (dpi). This sounded like the answer to a document producer's dreams,
but the puzzle had one missing piece. Users still needed a standard file
format so that they could use any scanner to digitize an image, edit it with
the paint or graphics program of their choice, and than electronically paste
the image into their documents. Furthermore, having the whole process work
quickly would be very helpful.


Enter TIFF

Aldus Corp., creator of PageMaker for the Macintosh and PC, recognized the
need for a standard way to exchange digital data, took the initiative, and,
with the assistance of several vendors, developed Tag Image File Format
(TIFF). TIFF provides features that support most input devices, supports a
variety of data compression techniques, and has the flexibility to add new
features easily in a controlled fashion.

TIFF's features can support scanners, point programs, and cameras (see
Figure 1). It works just as well with a simple Windows Paint document as it
does with a complex medical imaging scanner. TIFF files contain tags that
describe not only the height and width of an image, but also provide
resolution information, information about gray scale or color, and the type
of compression scheme being used.

Currently TIFF supports a modified form of the run-length compression, CCITT
Group 3. CCITT/3 provides reasonable compression on black-and-white images
that are 200 or 300 dpi by identifying continuous runs of all black or all
white pixels. It then replaces these with a unique code word that can be
used to reconstruct the original continuous run of data. You get the
compression because the code words are typically shorter than the run of
data they are replacing. CCITT/3's compression on dithered gray scale or
color images isn't a good because redundancy isn't found in the same way as
it is in standard black-and-white images. For this reason, TIFF also
supports a simple packed scheme in which data is packed into bytes as
tightly as possible with no unused bits, except at the end of a row.
Specific compression schemes for gray and colored images will be added;
Microsoft, the current keeper of the TIFF specification, will welcome you
suggestions.

TIFF makes it possible to add new features without having to rewrite
supporting software each time a new feature is added. For example, if one
application wants to add a "date-of-creation" tag to an image, it can be
included as a tag when the application is written. Other applications that
read TIFF files don't need to be rewritten unless they intend to make use of
this new date-of-creation tag. The TIFF specification claims that "very high
priority has been given to structuring the data in such a way as to minimize
the pain of future additions." When additional capabilities, such as
scanning color images, are available, TIFF should be able to accommodate
them as well.


How TIFF Works

Traditional painting file formats, such as PC Paintbrush, use a fixed format
organization (see Figure 2). The organization and location of each value is
known prior to reading the file. For example, the x resolution information
in this example is always stored in bytes 2 and 3 of the record.

TIFF is not positionally based. Instead it uses tagged information, a style
typically used in database design. Tags define the attributes of the image
and how the actual image is stored. Each image has a header followed by tags
that describe information contained elsewhere in the file. Tags consist of
the name of the tag information, the size and length of the information
described in the tag, and a pointer to the actual information (see
Figure 3). Tags can expand to contain as much information as needed about
the image, so that you can add as many tags as you need. You'll only need a
few tags for simple images; you may need many tags for complex images.

The header and tags for an image are called the Image File Directory.
Because of its structure, you can store multiple versions of the same image
in one file. This could be useful if, for example, you wanted to store a
full-resolution version of an image for editing and printing and a
subsampled version for better display performance, as shown in Figure 4.
Each version has its own IFD with a unique set of attributes and pointers to
the actual data. TIFF permits you to store the image data in variable-length
strips or scan lines, giving you random access to any part of the image. If
you just need one section of a very large image and you've stored the image
in strips, you can decompress and load only the strips you need. And when
you modify an image, you need to restore only the modified strips, not the
entire image.


Advantages

For software and hardware developers, the advantages of a standardized
format for exchanging digital data are obvious. Rather than support a
proprietary file format for each scanner vendor, desktop publishing
application, and paint or draw program, developers need to support just one
file format. Even with a complex file format like TIFF, this is better than
supporting a constantly growing number of different formats.

TIFF is considered to be a "rich" file format. Rather than reduce all images
to the lowest common denominator, TIFF is designed to handle many different
kinds of images, line art, gray scale, and color, and different resolutions
as well.


TIFF

supports different compression schemes and is flexible enough to adapt the
compression algorithm to fit the type of data. It will expand to handle more
complex images and compression schemes as the technology advances. Adding
more features does not require rewriting existing applications if those
applications don't want to take advantage of the expanded capability.
Besides handling complex images well, TIFF also works with such simple
images as those produced by paint programs, because the application can use
only the parts of TIFF it requires.

Another advantage of TIFF is its machine independence. You can use it with
Intel or Motorola processors with UNIX, MS-DOS, or Macintosh operating
systems, which encouraged Mitch Murphy, project manager at OptiGraphics
Corp., to convert to TIFF.

OptiGraphics, which makes engineering workstations and large-document
scanners, were looking for a file format to use for all raster images for
both its PC and UNIX machines. Murphy said it chose TIFF so it could
"gracefully upgrade and maintain compatibility as products mature." Products
from Murphy's group, such as View! and Markup!, are Microsoft Windows
applications that allow you to view and make minor changes to large
engineering and architectural drawings. The scanner outputs the drawings in
TIFF, and View! and Markup! read and display the TIFF output. Later, if a
drawing needs more extensive work, there are products that automatically
vectorize the TIFF file so it can be used with a CAD system.


Limitations

TIFF's richness, which is one of its major advantages, is also the source of
its limitations. Although it's not terribly difficult for a scanner to write
a TIFF image file, it is reasonably complicated to write a TIFF reader. Marc
Cantor, who is president of MacroMind, faults Aldus and Microsoft for not
including a decoding algorithm with the TIFF specification. As it stands
now, each company has to write its own parser; Cantor's company, whose
product Graphic Works reads a scanned TIFF document of any size, wrote its
own parser and is selling it to others. To remedy the problem, Microsoft
will be making TIFF tools available.

Another limitation is that there is not a standard TIFF file, nor a common
subset of features that everyone using the name TIFF must support. Different
vendors can support different Tag fields of TIFF. OptiGraphics' Murphy says
that "TIFF is so rich that dialects will emerge." To prevent this, he and
others suggest using common tools, which are becoming available. Murphy has
a library of routines to access TIFF files in an orderly way; this helps to
keep track of all the levels of indirection.

If you contact him at OptiGraphics at (619) 292-6060, he'll send them to you
for free. However, he cautions that you "use them at your own risk." The
development engineers at Hewlett-Packard have made a significant
contribution to the TIFF spec and have written a lucid Guide to the Tagged
Image File Format. Hewlett-Packard also has some source code for reading,
writing, compressing, decompressing, and debugging TIFF files. For
information, call HP's ISV marketing department at (303) 350-4000. Dest
Corp., a scanner hardware manufacturer, also has a library of routines it
will license to desktop publishing vendors free of charge. The phone number
is (408) 946-7100. And Tim Davenport at Aldus will send you sample TIFF
files; for information, call (206) 622-5500.


The New Standard?

TIFF is rapidly becoming the standard for handling bit-mapped images.
Companies like HP, Aldus, and OptiGraphics have put a significant amount of
research and development into TIFF. Most echo Murphy's sentiments: now that
they've committed to TIFF, they want others to commit as well, and they're
willing to help. David Rosenbloom of Cygnet Technologies, a high-end
telecommunications firm, chose TIFF rather than a proprietary format for its
FAX product because it had what it needed, a generic descriptive header with
an unlimited number of fields and :a shot at becoming some sort of accepted
standard." Other firms that have publicly announced support for TIFF are
Dest, DataCopy, Microsoft, Microtek, Media Cybernetics, New Image
Technology, and Software Publishing. Support for TIFF is bridging the
traditional rift between the PC and Macintosh camps.

Although Steve Carlson at Aldus is the originator of the TIFF specification,
Microsoft is now the keeper of TIFF. With Microsoft, a neutral player,
managing TIFF rather than a hardware or desktop publishing vendor, companies
are more likely to see TIFF as a standard instead of an aid to competitors.
Microsoft is currently working with those companies whose products scan and
archive images. It also wants to work with such page description vendors as
Adobe, DDL, and Interpress to ensure that the functions TIFF supports are
mapped closely to future functions of page description languages. If, for
example, compression schemes are compatible, print time can be significantly
improved. Manny Vellon of the Microsoft Windows marketing group is the new
TIFF administrator; you can write to him for a copy of the spec and also
feel free to send him your suggestions.

Currently, the desktop publishing market is most interested in TIFF, but the
market is expanding to include OCR, FAX, 3-dimensional CAD, and
sophisticated medical imaging. Someday, instead of going to your bookshelf
and reaching for a volume in your published art collection, you may go to
your CD ROM disc and get high-quality typeset images stored in TIFF format.
Soon you may be able to receive an engineering drawing as a FAX on your PC,
use an application to vectorize the drawing, touch it up with your CAD
program or the specs that accompany the drawing in another FAX, run an
application to OCR the specs, edit them with your word processor, and then
send them back in FAX form. These hardware and software applications will be
able to talk and work together when there is a true standard for exchanging
digital image data; TIFF is well on its way to becoming that Standard.


Figure 1:  TIFF's features can support scanners, paint programs, and cameras
           and treats simple paint programs just as it does complex medical
           documents.
                                                         ╔══════════════╗
                ┌░░░▒▒┐                                  ║ ░░░░░░░░░░░  ║█
       Camera   └──░──┘            __________            ║ ░░░≡  ≡≡░░░░ ║█
┌───▀▀────────────▀▀▀─┐       ╓───/        /─╖           ║ ░░░≡≡≡≡≡░░░░ ║█
│╔════╗ ◘▒◘░░░░╔════╗ │█    ╔═╝──┴────────┴──╚═╗         ║ ░░░░░░░░░░↔↨ ║█
│║░░░▒║ ╔════╗ ║░▒▒▒║ │█    ║ ----‼‼‼‼‼‼‼‼‼‼‼  ║█      ╔═╝──────────────╚═╗
│║░░░▒║ ║   ░║ ║░░▒▒║ │█    ╚══════════════════╝█      ║ ----‼‼‼‼‼‼‼‼‼‼‼  ║█
│╚════╝ ╚════╝ ╚════╝ │█      ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀      ╚══════════════════╝█
└─────────────────────┘█          Scanner         ┌────  ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
  ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀             │            │      Desktop Publishing
                     │             tiff           │
  ╔════════════╗    tiff             │          ascii
  ║ ░░░░☻░░░░  ║█    │         ┌──▄▄▄▄▄▄▄▄──\     │        ╔══════════╗
  ║ ░░░ █ ░░░░ ║█    └─────────│  ██████ █  │     │        ║ ▒▒▒▒▒▒▒▒▒║█
  ║ ░░░   ░░░░ ║█ ───tiff──────│┌──────────┐│─────┤        ║ ▒▒▒▒▒▒▒▒▒║█
  ║ ░░░░▌▐░░↔↨ ║█              ││   Disk   ││     │   ╔════╩──────────╩══╗
╔═╝────────────╚═╗             │└──────────┘│     │   ║ ----‼‼‼‼‼‼‼‼‼‼‼  ║█
║ ----‼‼‼‼‼‼‼‼‼  ║█            └────────────┘     │   ╚══════════════════╝█
╚════════════════╝█                               │     ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
  ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀                               │       Word Processsor
    Paint/Draw                                    └────


Figure 2:  Fixed Format Organization

               ╔════╗  ┌────────────────────────────┐
               ║  0 ║  │         Revision #         │
               ╟────╢  ├────────────────────────────┤
               ║  2 ║  │       X Resolution         │
               ╟────╢  ├────────────────────────────┤
               ║  4 ║  │       Y Resolution         │
               ╟────╢  ├────────────────────────────┤
               ║  6 ║  │         H Pixels           │
               ╟────╢  ├────────────────────────────┤
               ║  8 ║  │         V Pixels           │
               ╟────╢  ├────────────────────────────┤
               ║ 10 ║  │        Colorplane          │
               ╟────╢  ├────────────────────────────┤
               ║    ║  │                            │
               ║    ║  │                            │
               ║    ║  │                            │
               ║ 64 ║  ≈           Data             ≈
               ║    ║  │                            │
               ║    ║  │                            │
               ║    ║  │                            │
               ╚════╝  └────────────────────────────┘


Figure 3:  TIFF uses tagged information, a style typically used in database
           design. Tags define the attributes of the image and how the actual
           image is stored. Each image has a header followed by tags that
           describe information conatined elsewhere in the file.

                ┌─────────────────────┐
                ≈    Tiff Header  •───≈──┐
                ├─────────────────────┤  │  ┌──────────┐  ╔═══╗
          ╔═══╗ │       Tag 1 ◄───────┼──┘ •│ Tag Type │█ ║ T ║
          ║ D ║ ├─────────────────────┤  •  ├──────────┤█ ║ A ║
          ║ I ║ │       Tag 2         │•    │   Size   │█ ║ G ║
          ║ R ║ ├─────────────────────┤  •  ├──────────┤█ ║   ║
          ║ E ║ │       Tag 3         │    •│  Length  │█ ║ E ║
          ║ C ║ ├─────────────────────┤     ├──────────┤█ ║ N ║
          ║ T ║ │       Tag 4     •───┼──┐  │ Data or  │█ ║ T ║
          ║ O ║ ├─────────────────────┤  │  │ Pointer  │█ ║ R ║
          ║ R ║ │       Tag 5         │  │  └──────────┘█ ║ Y ║
          ║ Y ║ ├─────────────────────┤  │    ███████████ ╚═══╝
          ╚═══╝ │       Tag 6     •───┼──│─┐
                └──────────:──────────┘  │ │
                ┌──────────:──────────┐  │ │
                │  Long Tag Data ◄────┼──┘ │
                └──────────:──────────┘    │
                ┌──────────:──────────┐    │
                │ Bitmap Image Data ◄─┼────┘
                └─────────────────────┘


Figure 4:  The header and tags for an image are called the Image File
           Directory (IFD). It is structured so that you can store multiple
           versions of the same image in one file.

                                            ┌────────────┐       Image Data
  ┌────────────────────┐                 ┌─►│ Pointer to ├─┐    ┌──────────┐
  │    File Header     │                 │  │ Strip 1    •─────►│ Strip 1  │
  ├────────────────────┤                 │  ├────────────┤ ├─┐  ├──────────┤
┌─•Pointer to first ifd│                 │  ≈            ≈ │ │  │          │
│ └────────────────────┘    ┌──────────┐ │  ├────────────┤ │ │  ≈          ≈
│  Full Resolution ifd   ┌─►│Pointer to│ │  │ Pointer to │ │ │  │          │
│ ┌─────────┬──────────┐ │  │  Plane 1 •─┘  │ Strip n    •────┐ ├──────────┤
└►│  # of   │ Subfile  │ │  ├──────────┤    └┬───────────┘ │ │└►│ Strip n  │
  │ entries │   Type   │ │  ≈          ≈     └┬────────────┘ │  ├──────────┤
  ├─────────┴──────────┤ │  ├──────────┤ ┌───►│  Pointer to  │  │          │
  │        Tags        •─┘  │Pointer to│ │    │  Strip n     │  ≈          ≈
  ├────────────────────┤    │  Plane m •─┘    └──────────────┘  │          │
┌─•Pointer to next ifd │    └──────────┘    ┌────────────┐      ├──────────┤
│ └────────────────────┘                 ┌─►│ Pointer to ├─┐  ┌►│ Strip 1  │
│     Subsample ifd         ┌──────────┐ │  │ Strip 1    •────┘ ├──────────┤
│ ┌─────────┬──────────┐ ┌─►│Pointer to│ │  ├────────────┤ ├─┐  │          │
└►│  # of   │ Subfile  │ │  │  Plane 1 •─┘  ≈            ≈ │ │  ≈          ≈
  │ entries │   Type   │ │  ├──────────┤    ├────────────┤ │ │  │          │
  ├─────────┴──────────┤ │  ≈          ≈    │  Pointer to│ │ │  ├──────────┤
  │        Tags        •─┘  ├──────────┤    │  Strip y   •───┼─►│ Strip y  │
  ├────────────────────┤    │Pointer to│    └┬───────────┘ │ │  ├──────────┤
  │Pointer to next ifd │    │  Plane x •───┐ └┬────────────┘ │  │          │
  └────────────────────┘    └──────────┘   └─►│  Pointer to  │  ≈          ≈
                                              │  Strip y     │  │          │
                                              └──────────────┘  │          │


───────────────────────────────────────────────────────────────────────────
The CCITT/3 Scheme in TIFF
───────────────────────────────────────────────────────────────────────────

The CCITT/3 scheme used in TIFF identifies continuous runs, which are called
run-lengths, of either all white or all black pixels. Each of these run-
lengths is then replaced with a code word that can later be used to
reconstruct the continuous run of data.

The compression scheme uses two tables. One table contains code words for
different continuous runs of white pixels from 0 to 63 pixels in length.
The other table contains code words for the same continuous runs of black
pixels. The tables also contain values for some discrete run-lengths above
63. If the white- or black-pixel run-lengths exceed 63, combinations of
codewords can be used to compress these longer run-lengths.

Each row of the bitmap is compressed independently from any other row. In
addition, each row must start with a white pattern and must end on a byte
boundary. Starting with a white pattern provides synchronization and allows
the decompression algorithm know whether to search in the black or white
run-length table. If the row starts with one or more black pixels, then a
white run-length of 0 pixels must start the row of compressed data code
words.

End-of-line (EOL) markers, common to CCITT/3 used for FAX, are not used. If
EOL markers are used, readers of TIFF files would not be able to interpret
the information. Also, the TIFF PhotoMetric interpretation tag doesn't
apply when using CCITT compression. The values for white and black are
predefined. You may need to invert all binary data before encoding it in
CCITT format.

Any padding bits added to the uncompressed image row must not be compressed.
You should only compress the length of data that was specified in the width
tag field. For example, if a bitmap consisted of 10 pixels across, it would
be stored uncompressed as 2 bytes so that each row would begin on a byte
boundary. In this case, six padding bits would have been added. When
compressing this information, only the first 10 bits should be considered as
the data to be compressed, not the six padding bits.

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

Ask Dr. Bob!

Microsoft Pascal Inquiries

Dear Dr. Bob,

I received my first copy of MSJ last week, and from now on I will look
forward to your column, since it gives me an inkling of what the rest of
the world is doing. I have several questions about Microsoft(R) Pascal
Version 3.31.

In your March 1987 column (MSJ, Vol. 2 No.1), you refer to the C library
function getenv(). Microsoft Pascal 3.31 supplies a part of the C library
with the functions named SYSTEM and SPAWNLP, but no GETENV or any others.
Is there a way to get and use them from Microsoft Pascal?

In Microsoft Pascal, how can I make a single procedure definition file
that can be included in all program modules, even the ones defining the
procedures in the include file? Normally this gives some kind of
"duplicate identifier" error.

I use ALLMQQ extensively. Is there a problem with excessive use of ALLMQQ
and FREMQQ, even if I am careful to free up what I don't use? When I
allocate several large (25Kb) blocks and then free them up, sometimes I
can't allocate them again. Granted, there are additional scores of ALLMQQs
and FREMQQs going on at the same time.

Where can I get information on resident programming techniques? I am
totally ignorant, and the standard language manuals don't even hint at it.
──RHK

If you have the Microsoft C Compiler, Version 3.0 or Version 4.0, you may
use any of the large-memory model C libraries from a Microsoft Pascal 3.31
program. Actually, you can mix large model C code with Microsoft Pascal or
FORTRAN code, if you like. See the appendix on mixed-language programming
in your Pascal User's Guide.

With regard to GETENV, you are particularly lucky. It is present in the
subset of the C library included with Pascal 3.31 in the file CEXEC.LIB.
Figure 1 is a small program that uses GETENV to print out the PATH.

The Microsoft Pascal metacommands for conditional compilation ($if, $then,
$else, $end) provide a way to make a single module definition file. First,
create the definition file with the procedure and function definitions
followed by "extern;", grouping the definitions together by module. Next,
put an "$if moduleX $else ... $end" conditional around each module's group
of definitions, where X is the module number, as shown in Figure 2.

Finally, include this definition file in all your program's modules
following a list of constant definitions. Make all the constants 0, except
for the one for the current module. In the file for module number 1, for
instance, you would place the following:

  CONST
    module1 = 1;
    module2 = 0;
    module3 = 0;
    dummy   = 0;
  {$include:'define.inc'}

The constant "dummy" is required because the compiler keeps one look-ahead
symbol as described in section 18.3 of the Pascal 3.31 Reference Manual.

Dr. Bob knows of no bugs in the ALLMQQ and FREMQQ memory management
functions, but it is easy to make a sequence of calls to ALLMQQ and FREMQQ
that will cause ALLMQQ to fail even when there seems to be lots of free
space. This is because the memory heap can become fragmented, that is,
broken into small pieces of alternating free and allocated blocks, so that
there is no single chunk large enough for the ALLMQQ request. A more
sophisticated memory management scheme would have a "garbage collector" to
shuffle memory blocks around and put all the free space together to give
ALLMQQ a chance to allocate a large block. However, adding a garbage
collector opens its own can of worms and makes programming more
complicated.

There is no general-purpose method for working around this fragmentation
problem. The best you can do is try to allocate large blocks early in your
program, before the heap gets fragmented. Also, use ALLMQQ and FREMQQ less
often by reusing a block that's already been allocated.

Glad you asked about TSRs. The December 1986 issue of MSJ (Vol. 1 No. 2)
has an article on TSRs, "Moving Toward an Industry Standard for Developing
TSRs." Also, we've seen articles on writing TSRs in recent issues of Byte
and Dr. Dobb's Journal.

Environment Size in DOS

Dear Dr. Bob,

It appears that with MS-DOS Version 3.2 the only way to increase the
environment size is with COMMAND /E:xxxx. I have tried putting this
command into my AUTOEXEC.BAT with an additional /P switch to make it
permanent. This results in an infinite regression of COMMAND invoking
AUTOEXEC, which runs COMMAND and invokes AUTOEXEC , which ... you get the
idea. How can I do this without getting thrown for a loop?──EG

When started with the /P switch, COMMAND.COM looks for an AUTOEXEC.BAT in
the root directory of the current disk. You can take advantage of this by
making a small AUTOEXEC.BAT on your boot disk that copies the "real"
AUTOEXEC.BAT to a RAMdisk and runs the secondary version of COMMAND from
there. Figure 3 shows what such an AUTOEXEC might look like, where C: is
the boot disk and D: is a RAMdisk.

In the example, drive C: is the boot disk, drive D: is the RAMdisk, and
AUTOEXEC.2ND contains the rest of the boot sequence normally found in
AUTOEXEC.BAT.

An Alternative Suggestion

Dear Dr. Bob,

Your delightfully convoluted response to Forgetful in the March 1987 issue
of MSJ (Vol. 2 No. 1) prompts me to suggest this alternative. Instead of
twice compiling an IFDEFed C file and piping it into both an .INC and an
.H file for inclusion in both assembly and compilation processes, try the
trick shown in Figure 4, which depends on a basic difference between C and
MASM comments: C comments can cross line boundaries, while MASM comments
go to the end of the line.

This file can be directly included in both the assembly and the C source
code. Perhaps there is an even more compact and bizarre method of achieving
the same goal, but I can't think of it.──BN

You have a cute idea there, but we find it deficient in two respects.
First, there are still two distinct copies of each piece of information,
and it would be all too easy to change one without changing the other.
This defeats the purpose of the system, which is to make a single
collection of definitions that will automatically serve for both assembler
and C programming. Second, none of our C compilers will accept your code,
as they all require the #define directives either to start in the first
column or to be the first nonwhite space on a line.

Comments and Corrections

Dear Dr. Bob,

In Dr. Bob's "Self-Modifying Code" in the March 1987 issue of MSJ (Vol.2
No.1), the answer is incorrect. LINK4 does not accept the MULTIPLE option
for the CODE statement. I have struggled with the problem of forcing Windows
to use different code segments for multiple instances when I was writing
about Windows memory management.

To my mind, Windows should load different code segments for multiple
instances when you specify that a CODE segment is IMPURE or NONSHARED in
the module definition file. This is valid syntax, but Windows seems to
ignore these options.

The only way I found to force Windows to load multiple code segments for
different instances is by making the .EXE name different from the NAME of
the module in the .DEF file. I discovered this bug by accident, and it
certainly doesn't make any sense to me.

I should also note that even if you get Windows to load multiple code
segments, it will still discard code segments if you specify only MOVEABLE
without DISCARDABLE. This is implied by the text of your answer, but not
by the example. You have to make a code segment FIXED to prevent Windows
from discarding it.

For the stock bitmap question, I think the answer should have included the
fact that the stock bitmap has a width and height of only one pixel, so it
is useless for drawing until you select another bitmap onto it.──CP

MULTIPLE (or IMPURE or NONSHARED) code segments are not supported. All
code segments are by definition shared. It is unfortunate that the
documentation is not clearer on this point. The documentation is also not
clear about saying that all CODE can either be FIXED or DISCARDABLE and
that MOVEABLE implies DISCARDABLE for code segments. As for forcing
Windows to load multiple code segments for different instances by making
the .EXE name different from the NAME of the module in the .DEF file, I
suspect that what was happening was not what you think it was. It
definitely does not load multiple instances of the same code segment.

Dear Dr. Bob,

In MSJ, Vol. 2 No. 1, Dr. Bob tells a very good story about why
Windows uses only three of the four planes on the EGA, but unfortunately
it's totally wrong. As the programmer who first wrote the Windows EGA
driver, I know what I'm talking about. The mouse cursor is stored in off-
screen memory in planes 0-2. Windows also uses planes 0-2 for display.
Plane 3 is not used at all by Windows. Plane 3 by itself wouldn't have
been enough for the cursor──I wish it had been, as it would have made my
job a lot easier──since the Windows cursor requires two planes: it supports
white, black, transparent, and inverted pixels.

Why is the fourth plane unused? When I did the driver originally, it was
because I had to get the thing working in a matter of days and I didn't
have time to fool with the fourth plane. Since the fourth plane is most
logically used as some kind of intensity bit, and this is not independent
of the R, G, and B bits (that is, you can't make R intensified without
simultaneously making G and B intensified), this fourth bit doesn't really
fit into the GDI virtual RGB model very well, so logical <-> physical
mapping is hard to do.

Since then, the code has been maintained by someone else, and support for
the fourth plane was never added, probably because of the awkward logical
<-> physical mapping mentioned above.──BC


Figure 1:

program EnvTest(input,output);

     type
          chrptr = ^array[0..1000] of char;

     var
           path : chrptr;
           i    : integer;

     function getenv : chrptr [c,varying]; extern;

     begin
          write('Dr Bob''s PATH = ');
          path := getenv(ads('PATH'*chr(0)));
          i := 0;
          while (path^[i] <> chr(0)) do
               begin
                    write(path^[i]);
                    i := i+1;
               end;
     end.


Figure 2:

{$if module1 $else}
  {definitions for module1:}
  procedure f1; extern;
{$end}

{$if module2 $else}
  {definitions for module2:}
  procedure f2(p2:integer); extern;
{$end}


Figure 3:

COPY AUTOEXEC.2ND D:AUTOEXEC.BAT
D:
C:COMMAND /p


Figure 4:

;/* ScreenHt=200 ; */ #define ScreenHt (200)    /* comment
ScreenWidth=640  ; */ #define ScreenWidth (640) /* comment

Point STRUC      ; */ struct Point {           /* comment
 x  DW           ; */    int x;                /* comment
 y  DW           ; */    int y;                /* comment
Point ENDS       ; */ };


════════════════════════════════════════════════════════════════════════════


Vol. 2 No. 4 Table of Contents


Microsoft(R) Windows/386: Creating a Virtual Machine Environment

Windows/386 exploits the 80386's virtual machine capabilities to offer an
environment that allows the user to simultaneously run and switch between
existing MS-DOS and Windows applications. The product fully implements
preemptive multitasking and data exchange between applications.


Programming in C the Fast and Easy Way with Microsoft(R) QuickC(TM)

Making the process of C programming faster and easier has long been a goal
for developers of Microsoft C programming products. The Microsoft QuickC
compiler offers an integrated environment that provides programmers with
high-speed compilation and an easy debugger.


Character-Oriented Display Services Using OS/2's VIO Subsystem

Microsoft OS/2 provides application programs with a wide variety of display
functions. The OS/2 video capabilities detailed in this article are the
VIO services──the subsystem that lets you get your applications up and
running quickly without the Windows Presentation Manager.


Dynamic Allocation Techniques for Memory Management in C Programs

How well memory is managed can make a real difference in the performance
of a program, particularly those programs that process unpredictable
amounts of data. Dynamically allocating memory to hold array and linked
list data provides your programs with extra flexibility and utility.


CD ROM Technology Opens the Doors on a New Software Market

Thanks in part to the success of the compact audio disc, CD ROM is viable
and growing. Developers with applications requiring large amounts of data
space, such as parts catalogs, can benefit from CD ROM's huge data
capacity and low cost duplication.


MS-DOS(R) CD ROM Extensions: A Standard PC Access Method

Using the Microsoft MS-DOS CD ROM Extensions frees developers to concentrate
solely on their applications, eliminating dependence on any particular drive
technology. This article discusses CD ROM device drivers and the MSCDEX.EXE
program that interfaces with MS-DOS.


Microsoft(R) QuickBASIC: Everyone's First PC Language Gets Better

You can create elegant applications in BASIC by combining the powerful
Microsoft QuickBASIC 3.0 control structures and statements, the INT860
interface to MS-DOS and BIOS services, and custom assembly-language
routines of your own design. Here's how.


Ask Dr. Bob


EDITOR'S NOTE

Microsoft(R) Windows/386 provides users of 80386-based computers with a
new operating environment. Its principal benefit is the flexibility to
simultaneously run nearly any combination of MS-DOS applications, each in
its own virtual machine. While it does not offer developers a new
programming interface, we think you will appreciate a behind-the-scenes look
at the product's design.

Meanwhile, many software developers are working hard to unravel the details
and intricacies of MS(R) OS/2. The first OS/2 applications will take
advantage of the rich set of character-based display functions that are part
of the OS/2 API. In this issue, Ray Duncan explores the use of these
functions.

However, operating environments aren't all that's new. QuickC gives both
the professional and beginning C programmer a new tool──the integrated
working environment first seen in QuickBASIC. CodeView(R)-compatible
debugging aids and superfast compilation speeds helped create TOUCH.C, a
useful file date-stamp utility included here. We also have two articles
about a new kid on the PC block──CD ROM, which finally seems ready for
general acceptance.

All our source code listings can now be found on DIAL, CompuServe, BIX, and
two public access bulletin 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.

A reminder──we read all of our mail, hardcopy and electronic (our MCI
mailbox is MSJ). Write and let us hear from you. If you're interested in
submitting a manuscript drop Tony Rizzo, our technical editor, a note.
He would be more than happy to hear from 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

BETSY KAUFER
Administrative Assistant

Copyright(C) 1987 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 XENIX are
registered trademarks of Microsoft Corporation. QuickC and Bookshelf
are trademarks of Microsoft Corporation. IBM and PC AT are registered
trademarks of International Business Machines Corporation. PS/2 is a
trademark of International Business Machines Corporation. dBASE II is a
registered trademark of Ashton-Tate Corporation. AT&T and UNIX are
registered trademarks of AT&T. Lotus and 1-2-3 are registered trademarks
of Lotus Development Corporation. Intel is a registered trademark of Intel
Corporation. COMPAQ is a registered trademark of COMPAQ Computer
Corporation. DESKPRO 386 is a trademark of COMPAQ Computer Corporation. CP/M
is a registered trademark of Digital Research, Inc. Hercules is a registered
trademark of Hercules Computer Technology. WordStar is a registered
trademark of MicroPro International Corporation. Zenith is a registered
trademark of Zenith Radio Corporation.

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

Microsoft Windows/386: Creating a Virtual Machine Environment

───────────────────────────────────────────────────────────────────────────
Also see:
A Comparison of Current and Future Windows Versions
───────────────────────────────────────────────────────────────────────────

Ray Duncan☼

The past year has seen the emergence of a new class of personal computers,
based on the IBM(R) PC AT(R) architecture but incorporating an Intel 80386
microprocessor with a 32-bit memory path for increased performance. The
pioneer in this category was the COMPAQ(R) DESKPRO 386(TM), which has since
been joined by the IBM PS/2(TM) Model 80, as well as a score of machines from
other clone vendors. Until now, the primary benefit associated with these
machines has been their formidable speed. However, 32-bit operating
systems and programming tools for the 80386 are still in the development
stage, while 32-bit applications software waits in the wings until the
tools stabilize.

Microsoft Corp.'s new product, Windows/386, unmasks the larger potential
of 80386 machines while protecting the user's investment in MS-DOS(R)-
compatible hardware and software. The key features of Windows/386 are:

  ■ a graphical user interface compatible with Windows 2.0 and the OS/2
    Presentation Manager

  ■ true preemptive multitasking of MS-DOS applications, each in a private
    640Kb memory space

  ■ applications that run under Windows/386 receive much more memory than
    they would under Windows 2.0

  ■ the ability to run MS-DOS applications in overlapping windows, even
    so-called "ill-behaved" applications that do not rely on MS-DOS or
    the ROM BIOS for screen output

  ■ exchange of screen data between both standard MS-DOS and Windows
    applications

  ■ emulation of the Lotus/Intel/Microsoft Expanded Memory Specification
    Version 4.0

Windows/386 requires either an 80386-based PC AT-compatible or an AT&T(R)
6300 computer, an EGA, VGA, or CGA monitor, at least 1Mb of RAM (2Mb are
recommended), and a fixed disk. The 80287 or 80387 numeric coprocessors
are also supported.


An Intel Retrospective

Nearly all of the capabilities of Windows/386 depend on a feature of the
80386 called virtual 86 mode. To fully appreciate this particular facet of
the 80386, it is necessary to digress for a moment and review the
characteristics of the 80386's ancestors, the Intel 8086/88 and 80286
microprocessors. Each successive generation of Intel processors has
supported the software that was written for its predecessors by means of
"execution modes," and virtual 86 mode is the logical culmination of this
approach to software compatibility.

The first Intel 16-bit processors, the 8086 and 8088 (announced in 1978
and 1979 respectively), can address a maximum of 1Mb of memory. When
memory is referenced, the contents of one of the four segment registers is
shifted left four bits and combined with a 16-bit offset to form a 20-bit
physical address; the segment registers simply act as base pointers. The
8086 and 8088 have no provision for memory protection, virtual memory, or
privilege levels; any program can access any location in memory or any I/O
device without restriction.

The Intel 80286 (first shipped in 1982) represents a major increase in
speed and capabilities over the 8086/88. It can run in either of two
modes: real or protected. In real mode, the 80286 acts like a fast 8086
with a few additional machine instructions. It can run virtually all
8086/88 software and is limited to 1Mb of memory.

In protected mode, the 80286's mapping of addresses is altered to add a
level of indirection. The value in a segment register is a selector, which
is an index to an entry in a descriptor table that contains the base
address and length of a physical memory segment, segment attributes
(executable, read-only, or read-write), and privilege information. Each
time a program references memory, the hardware accesses the associated
descriptor to generate the physical address and simultaneously checks to
make sure that the memory access is valid.

This method of protected-mode address generation allows the 80286 to
support memory protection and virtual memory management within a physical
address space of 16Mb and a virtual address space of 1Gb. Four levels of
privilege are also provided, allowing the operating system to be protected
from applications programs and those programs from each other.

When the Intel 80286 was designed, the dominant software base consisted of
CP/M(R) applications such as WordStar(R) and dBASE II(R). At the time the 802
was released, the IBM PC was just a few months old. Hence Intel engineers
had no way of foreseeing the incredible success of the IBM PC and its
compatibles, or the eventual need of an enormous user base to make a
smooth transition from real-mode (8086/88) to protected-mode (80286)
environments. Although the 80286 was designed to start up in real mode for
compatibility with 8086 software, and although machine instructions were
included to switch from real mode to protected mode, no mechanism was
built into the chip to allow a return from protected mode to real mode
under operating system control without halting the processor.

This single omission proved to be a hideous technical problem during the
development of the Microsoft OS/2, since one of the overriding design
goals for the new operating system was to allow "old" (real-mode)
applications to run alongside "new" (protected-mode) applications.
Although experimentation and painstaking optimization eventually led to an
acceptable solution for the necessary mode switching, another drawback
remains: the protected-mode operating system cannot be shielded against
bad behavior on the part of a real-mode program. By the very nature of
real mode, such a program has a free hand with the hardware and can easily
crash the machine.


The Four Modes

The Intel 80386 (introduced in 1985) is a true 32-bit processor that
supports a 4Gb physical address space and a 64Tb virtual address space. To
ensure compatibility with previous processors and to solve the problems of
support for real-mode applications that were encountered with the 80286,
the 80386 has no less than four different execution modes. The first is
the familiar real mode, wherein the 80386 functions as a fast 8086/88-
compatible processor with some bonus opcodes. Like the 80286, the 80386
always powers up in real mode and can therefore run any existing 8086
operating systems and software.

In protected mode, the 80386 can take on two different personalities. It
can execute a logical superset of the 80286 protected-mode instructions
and run 16-bit programs, or it can run in its native mode, which offers
32-bit instructions, registers, and stacks, and allows individual memory
segments as large as 4Gb. In either case, the 80386 translates selectors
and offsets to linear addresses using descriptor tables in much the same
manner as the 80286. However, an additional level of address
translation──supported in hardware by page tables──allows much greater
flexibility in mapping the linear addresses onto physical memory.

Unlike the 80286, the 80386 allows the operating system to switch smoothly
and quickly back from protected mode to real mode when necessary. But it
is unlikely that this capability will find much use because of the 80386's
fourth operating mode: virtual 86 mode.


Four Modes of the Intel 80386 Microprocessor

Real Mode            Functions as a very fast 8086/88-compatible processor.

Protected Mode       Functions in protected mode as an enhanced 286
(16-bit)             processor.

Protected Mode       Functions in protected mode using full 32-bit
(32-bit, native      instructions, registers, and stacks.
mode)

Virtual 86 Mode      Runs multiple, protected-mode, virtual 8086 machines,
                     each with its own 1Mb of memory space.


Device Virtualization

A protected-mode 80386 operating system can create special memory
segments──up to 1Mb in size──that have a remarkable characteristic: programs
that run within these segments execute as though they were running on an
8086/88 in real mode. Each such segment is called a virtual 8086 machine,
and each has its own address space, I/O port space, and interrupt vector
table. Multiple virtual machines can be running simultaneously, each under
the illusion that it is in complete control of the computer.

The crucial difference between real mode and virtual 86 mode is that
memory protection, virtual memory, and privilege-checking mechanisms are
still in effect when a virtual machine is running. Thus, a program
executing in a virtual 8086 machine cannot interfere with the operating
system or damage other processes. If the program reads or writes memory
addresses that have not been mapped into its virtual machine, or if it
manipulates I/O ports to which it has not been allowed access, an
exception (hardware interrupt) is generated, and the operating system
regains control.

The operating system's exception handler can choose to carry out the
operation on behalf of the program running in the virtual machine,
possibly substituting other port or memory addresses; it is also able to
arbitrate requests from multiple virtual machines directed at the same I/O
port or memory address. This process of intercepting I/O operations,
where, for example, the operating system creates the illusion of a
separate disk controller or video controller for each virtual machine
while only one physical device is present in the system, is called device
virtualization.

A program that runs in the 80386's native 32-bit protected mode and
oversees a set of virtual machines is called a 386 control program, or
virtual machine monitor. Windows/386 is a such a virtual machine monitor;
it also provides complete device virtualization for the PC's disk
controller, video adapter, keyboard, mouse, timer chip, and 8259
programmable interrupt controllers.


The User Interface

At first glance, the user interface of Windows/386 appears identical to
that of Windows Version 2.0: it has the same MS-DOS Executive window,
pull-down menus, "hot keys," and utilities (Cardfile, Terminal, and the
like). A program is launched by selecting its executable file, its PIF
file, or one of its data files with the arrow keys or by double-clicking
with the mouse, just like under Windows. An open window can be brought to
the foreground with Alt-Tab or Alt-Esc or by clicking on the window with
the mouse; also, a window may be resized or moved by clicking and dragging
its borders (see Figure 1☼).

The first inkling that something different is going on comes when a
standard application (that is, an MS-DOS application that is not written
specifically for Windows) is started. Under Windows 2.0, only those
relatively rare, "well-behaved" MS-DOS programs that perform all of their
input and output through standard MS-DOS calls, strictly observe the
system's memory management conventions, and avoid all direct access to
hardware can run in a window. "Ill-behaved" programs that access the
hardware directly for performance reasons──a category that includes nearly
every popular MS-DOS application──must run full-screen: Windows 2.0 simply
gets out of the way until such programs terminate, and its multitasking
comes to a screeching halt.

A standard application under Windows/386 initially comes up full-screen
just as it does under Windows 2.0. But if the user presses Alt-Enter, the
program is suddenly running in an overlapping graphics-mode window right
alongside the true Windows application (see Figure 2☼). The user can use
the mouse or the cursor keys to select and copy data from a window
containing a standard application to any other window and can resize,
move, or even "iconize" such a window. Furthermore, the user can toggle
between windowed and full-screen mode with Alt-Enter at any time. When an
application is full-screen, Windows/386 can place it into a text-display
mode to allow faster displays, and the application's intrinsic mouse
support works as though it were running under MS-DOS alone.

Windows/386 associates a drop-down control menu, activated with the
keycode Alt-Space, with each standard application that can be used to
affect its window size, position, and behavior (as shown in Figure 2☼). The
Settings dialog box (see Figure 3☼), which is reached through the control
menu, allows the user to suspend or resume a standard application, specify
whether it should run full-screen or in a window, and control its
multitasking behavior. The initial settings for a program can also be
determined by creating a PIF file for it.

There are three multitasking options available for a standard application:
foreground, background, and exclusive. When foreground is chosen, the
application runs only when it is displayed full-screen or when its window
has been selected, but other applications are allowed to run in the
background at the same time. The background option means that the
application will continue to run even when some other application is
selected (of course, this is not useful for word processors and other
programs that have nothing to do unless they are receiving keyboard
input). When the exclusive option is picked, the application runs only
when it is in the foreground, but while it is active it receives all of
the CPU time and no other applications are allowed to run at all. Hence
the exclusive option permits the application to perform as if it were in a
single-tasking MS-DOS environment.

Figures 4 and 5☼ show the process of defining an area of the standard
application for copying, bringing up another application, and transferring
the defined area.

A little experimentation leads to an additional pleasant surprise: there
seems to be much more RAM left for a standard application than is usual
under Windows. In fact, if COMMAND.COM and then CHKDSK are run in a
window, approximately 580Kb is seen to be available──as though Windows
weren't present at all. And when multiple standard applications are
launched, each runs in a separate memory space as large as 640Kb, until
the physical memory of the system is exhausted. As a fringe benefit,
devotees of RAM-resident applications (TSRs) can find relief from the
phenomenon of "RAM cram" by starting multiple copies of COMMAND.COM to
establish several MS-DOS sessions, and loading a different selection of
TSRs into each session. This is the magic of the 80386's virtual 86 mode
in action.


How Windows/386 Works

The Windows/386 system actually consists of three separate, mutually
interdependent elements. The first is a regulation copy of MS-DOS or PC-
DOS, Version 3.0 or later. The second is a copy of Windows 2.0, which is
included in the Windows/386 product. The third is the core of the
Windows/386 product, the Virtual DOS Machine Manager (VDMM).

The MS-DOS component supplies the routine file, time, and date, as well as
memory allocations services to the application programs running in each
virtual machine; the MS-DOS SHARE.EXE module can optionally be loaded for
networklike file locking and sharing support if multiple applications will
be accessing the same files. Windows 2.0 provides the graphical user
interface and pointer device support. The VDMM runs in the 80386's native
32-bit protected mode and oversees the multitasking and protected-memory
aspects of the system's operation. The VDMM also takes advantage of the
80386's paging capability to supply expanded (Lotus/Intel/Microsoft EMS)
memory to applications that require it. The EMS services furnished by
Windows/386 are compatible with the recently announced EMS 4.0
specification and do not require loading of any special driver.

Windows/386 is started on a system that has already been booted up under
MS-DOS or PC-DOS in the usual manner. When Windows/386 gains control, it
loads the Virtual DOS Machine Manager into extended memory (the memory
above the 1Mb boundary), creates an initial virtual machine (referred to
as VM1), maps the existing copy of MS-DOS into it using the 80386's page
tables, and then loads Windows 2.0 on top of MS-DOS in the first virtual
machine. Control is then passed to Windows 2.0, which reads its
configuration file (WIN.INI) and presents the familiar MS-DOS Executive to
the user.

True Windows applications, such as Cardfile and Terminal, run together in
the virtual machine containing Windows itself (VM1). Windows presents such
programs with exactly the same applications program interface as in real
mode, and multiple Windows applications running concurrently are scheduled
for execution by the internal Windows multitasker──that is, nonpreemptively.
Just as in real mode, the Windows kernel can use expanded memory pages to
swap the code and data segments belonging to Windows applications, so that
the number of Windows applications that can be loaded simultaneously is not
limited by the 640Kb address space.

When a standard application is started, the VDMM creates a new virtual
machine and maps the image of MS-DOS, the ROM BIOS data area, and other
vital structures into it using the 80386 page tables, and then loads the
application into the virtual machine (see Figures 6 and 7). If the
application does not have a PIF file, the size of the new virtual machine
is under the control of the windowmemsize= entry in the WIN.INI file and
defaults to 640Kb. If a PIF file exists for the program, the size of the
new virtual machine depends on the Kb required and Kb desired fields in
the PIF, and on the amount of physical memory actually available (there is
no demand paging in Windows/386 as in OS/2).

While initializing a new virtual machine, the VDMM also creates a
corresponding instance of a special application called VMDOSAP in VM1
(multiple instances share the same code segment, so the overhead in the
Windows VM for each additional standard application VM is minimal).
VMDOSAP is analogous to the WINOLDAP module of real-mode Windows; when the
standard application is running in a window instead of full-screen,
VMDOSAP is responsible for any necessary translation and clipping of its
output for the window shape, size, and graphics-display mode. It also
translates input on behalf of the application when necessary (for example,
when data is being pasted from another window). The operation of VMDOSAP
is completely transparent to the application, and VMDOSAP does not occupy
any memory space within the application's virtual machine.

The final step in initializing the new virtual machine is to allocate a
"shadow" video buffer for the application and select the application's
initial display and multitasking status based on the contents of its PIF
file. The shadow video buffer lies outside the VM's address space and
holds a copy of the application's virtual screen; it is used to restore
the display when the application is brought to the foreground and allows
scrolling when the application is running in a window. If no PIF file
exists for the program, it begins execution with a full-screen display and
with its multitasking option set to "foreground"──that is, the application
is suspended when it is in the background.

The complete isolation of VMDOSAP from the application, and of standard
applications from each other, is made possible by Windows/386's
virtualization of the system's input and output devices, as well as by its
control over each virtual machine's interrupt vector table and I/O port
address space. When an application executes a software interrupt (to
invoke an MS-DOS or ROM BIOS service), reads from or writes to an I/O
port, or accesses a memory address that lies outside its virtual machine
space, an exception is generated that is fielded by the VDMM.

The VDMM disposes of the exception by examining the machine instruction
that provoked it and the contents of the CPU registers. For example,
requests for input and output can be funneled to VMDOSAP, while calls for
file system services revert to the virtual machine's image of MS-DOS for
processing. An application that accesses the hardware to select a video
mode not supported by Windows/386 is not terminated, but is forced to run
full-screen.


Windows/386 Multitasking

The Windows/386 multitasking scheduler uses the standard PC AT 18.2-Hz
timer tick. Its allocation of CPU cycles across multiple virtual machines
is determined by the multitasking setting on each application's control
menu or from its PIF file. In the simplest case, where each active program
has the "background" option selected, the currently selected application
gets two-thirds of the CPU cycles, and the remaining one-third are divided
among the other virtual machines. Applications that hook the ROM BIOS
timer tick interrupt vector (Interrupt 1CH) receive a proportionate number
of timer interrupts.

There are also two extreme cases of multitasking worth mentioning. The
first is when a standard application that runs under the exclusive option
is selected. Such an application receives all the CPU cycles, and all
other virtual machines are suspended. The other is encountered when no
standard applications are running and only the Windows VM is active. In
this case the internal Windows scheduler apportions the CPU cycles between
various Windows applications, and the VDMM plays no multitasking role at
all.


Many Machines, One DOS

One of the most interesting aspects of Windows/386 is its relationship to
MS-DOS. The 80386's paging tables allow a single physical instance of MS-
DOS to be mapped into each virtual machine's address space at the same
apparent address. Windows/386 uses internal knowledge of MS-DOS and
Windows to maintain the integrity of the file system and to prevent
different applications from executing concurrently within MS-DOS's
critical regions. It also maintains a copy of the Windows and MS-DOS
control structures (such as the MS-DOS system file table and the ROM BIOS
video driver data area) for each virtual machine, and swaps these in and
out of the MS-DOS image to supply the proper context for the application
that is running. These tables and structures are referred to as the MS-DOS
kernel instance data.

Since simply copying the kernel instance data back and forth on every
timer tick would burn up a significant number of CPU cycles, and since
applications may execute for prolonged periods (several context switches)
without referencing MS-DOS or the ROM BIOS, the VDMM uses an interesting
trick to reduce the multitasking overhead. Before a program is given
control at the beginning of its time slice, the memory pages in its
virtual machine that contain MS-DOS instance data, the interrupt vector
table, and the ROM BIOS data area are marked "not present" in the CPU's
page table. Only a few pages within the MS-DOS that contain instance data
are marked "not present" and cause page faults. Each page is demanded
independently of the others. This way many accesses to DOS cause no
context switching overboard at all, and when an instance switch is
required, its overhead is as small as possible.

If the program attempts to access these pages (for example, by inspecting
the current cursor location in the ROM BIOS data area or executing an
Interrupt 21H), a page fault is generated that suspends the program and
transfers control to the VDMM's interrupt handler. The VDMM handles the
fault for that page by moving in the MS-DOS kernel instance data, marking
the MS-DOS memory page as "present" in the page table to prevent further
faults during the same time slice, and restarting the instruction that
caused the fault. On the other hand, if the program runs to the end of its
time slice without referencing instance memory, no harm has occurred, and
the overhead of moving the data needlessly has been avoided.


Communications

The techniques used in Windows/386 to virtualize the video controller and
asynchronous communications controller are also particularly interesting.
Since direct hardware access to these devices by standard applications for
performance reasons is common, Windows/386 must stay out of the way as
much as possible. This ensures that the application's throughput will not
not be impaired, and that other programs will not be disrupted while still
providing for preemptive multitasking.

As mentioned earlier, each virtual machine is allocated a "shadow" video
buffer that is used to save a copy of its complete screen image when the
application is running in a window or in the background. The buffer is
located in extended memory, outside the virtual machine's 1Mb address
space, and may range in size from 16Kb to 256Kb depending on the display
mode selected by the program.

When an application is running in the background or in a window, the VDMM
uses the 80386's page tables to map the virtual machine's video refresh
buffer addresses (segment A000H, B000H, or B800H) onto the shadow buffer.
The application can modify what it perceives to be the video buffer at
will (see Figure 8), but the physical screen is not directly affected. At
intervals, the VDMM checks the "dirty bits" in the page table──which are
set by the hardware when an address within the page is written to──to
determine if the application has modified its video buffer. If a write is
detected, the buffer is compared against an earlier copy. The changes are
sent to VMDOSAP, which renders them into an appropriate pattern of pixels
and displays them in the visible portion of the application's window.

When an application is running full-screen, the virtual addresses of the
video refresh buffer within its virtual machine are mapped onto the
physical memory belonging to the video controller, and the shadow buffer
is not used as an intermediary. Thus, the program's control of the video
display is direct, just as if it were running in real mode and there is no
speed degradation. When the user switches away from the full screen
application, the VDMM simply copies the video controller's physical buffer
to the shadow buffer (the 80386's double-word string move instruction,
which transfers 32 bits at a time, is used to advantage here) before
restoring the screen image of the Windows/386 desktop or the next full-
screen application to be selected.

Applications that run in certain EGA graphics modes receive special
treatment. The virtual machine's video buffer addresses are mapped onto
the second 128Kb of the EGA controller's physical refresh buffer. This
lets the application use all of the EGA features without the overhead of
emulation, conserves system memory, and allows a rapid switch to full-
screen operation. (Instead of copying the shadow buffer to the physical
buffer, the VDMM selects the second graphics page as the active display.)

In contrast to the video controller, the asynchronous communications
controller, serial port, is fully virtualized at all times. The VDMM
always handles all communications interrupts and input or output
operations, and maintains an internal queue of serial port data and status
information. Even the initialization of the serial port (baud rate, word
length, parity, and number of stop bits) is virtualized, and this
information is maintained separately for each virtual machine.

An application signals its intention to use the communications controller
by attempting to read or write one of the controller's data ports, change
its interrupt mask bit on the 8259 Programmable Interrupt Controller, or
capture its interrupt vector. Any of these operations will generate an
exception that is processed by the VDMM. If the serial port is not in use
by another program, the VDMM assigns "ownership" of the virtualized
controller to the program that caused the exception; otherwise, a dialog
box is displayed and the user is allowed to decide which program will
retain access to the device.

Once a program establishes ownership of the communications controller, it
can perform input and output operations, and service communications
interrupts in a normal manner from its point of view. In actuality, the
interrupts and I/O operations are simulated by the VDMM, which transfers
data between the virtual machine and its internal queue. In this fashion,
the operation of the physical device, which is asynchronous and can
generate an interrupt at any time, is decoupled from the virtual 86
machine──which can only execute during its time slice.


Portents for the Future

In summary, Windows/386 exploits the 80386's unique capabilities to
furnish preemptive multitasking, a windowing user interface, data exchange
between any two applications, and efficient use of large amounts of RAM in
conjunction with existing MS-DOS and Windows applications. In doing so, it
overcomes the two most common objections to real-mode Windows: the
latter's inability to coexist with popular ill-behaved programs such as
Lotus(R) 1-2-3(R) or Microsoft Word, and its hunger for memory. In addition,
although Microsoft bills Windows/386 as an interim product, it has some
interesting implications──both short-term and long-term.

In the near future, until protected-mode applications appear for OS/2 that
fully exploit its virtual memory management, multitasking, and
interprocess communications facilities, Windows/386 offers more to 80386-
based PC owners than does OS/2. Windows/386 allows multiple MS-DOS
applications to run concurrently, while OS/2 supports only one real-mode
application at a time. In addition, the Windows/386 memory overhead in
each virtual machine is typically 80 to100Kb less than the OS/2 overhead
in the DOS 3.x Box. Finally, since Windows/386 uses the "real" MS-DOS as a
substrate──unlike OS/2, which in effect emulates MS-DOS──Windows/386 is
compatible with a broader range of existing applications than is OS/2.

Taking the longer view, the availability of a true 32-bit version of OS/2
and the Presentation Manager for the 80386 will probably threaten
Windows/386's market niche. However, Windows/386 has an important role to
play here too. It serves as a technology testbed for memory management,
multitasking, device virtualization, and MS-DOS virtual-machine techniques
that can eventually be absorbed into its successor──a system that will run
multiple MS-DOS applications, 16-bit 80286 protected mode applications,
and 32-bit 80386 protected-mode applications simultaneously.


───────────────────────────────────────────────────────────────────────────
A Comparison of Current and Future Windows Versions
───────────────────────────────────────────────────────────────────────────

╓┌──────────────────────┌──────────────┌──────────────┌───────────┌──────────►
                       Microsoft      Microsoft                  MS OS.2
                       Windows        Windows        Windows     Presentation
                       1.03 & 1.04    2.0            386         Manager

Presentation Spaces    Tiled          Overlapped     Overlapped  Overlapped

More Consistent
User and Keyboard
Interfaces             ──             Yes            Yes         Yes

                       Microsoft      Microsoft                  MS OS.2
                       Windows        Windows        Windows     Presentation
                       1.03 & 1.04    2.0            386         Manager

Processor              8088           8088           ──          ──
Environments           8086           8086           ──          ──
                       286            286            ──          286
                       386            386            386         386

Large Memory
Support                ──             EMS/EEMS       EMS/EEMS    16Mb

Multitasking           Nonpreemptive  Nonpreemptive  Fully       Fully
                                                     Preemptive  Preemptive

Enhanced Display
Performance            ──             Yes            Yes         Yes

Runs Exisiting
Windows (1.03)
Applications           Yes            Yes            Yes         No
                       Microsoft      Microsoft                  MS OS.2
                       Windows        Windows        Windows     Presentation
                       1.03 & 1.04    2.0            386         Manager
Applications           Yes            Yes            Yes         No

Graphics API           GDI            GDI            GDI         GPI

Multiple Document
Interface              ──             Yes            Yes         Yes

Device Drivers         ──             Enhanced       Enhanced    New Model

Old Application
Support                ──             Improved       Improved    Improved

Integral Part of OS    ──             ──             ──          Yes

Protected Mode
Applications
Execution              ──             ──             Yes         Yes

                       Microsoft      Microsoft                  MS OS.2
                       Windows        Windows        Windows     Presentation
                       1.03 & 1.04    2.0            386         Manager

New Development API    ──             ──             ──          Yes

New User "Shell" and
Keyboard Interface     ──             ──             ──          Yes

Virtual 86 Mode        ──             ──             Yes         ──


Figure 6:  This diagram shows the relationship between VDMM, MS-DOS, Windows,
           the VMDOSAP module, and standard applications.

    ┌───────────────────────────┐          ╔═════════════╗ ╔═════════════╗
    │The Windows/386 Environment│          ║  MS-DOS 3   ║ ║  MS-DOS 3   ║
    └───────────────────────────┘          ║ Application ║ ║ Application ║
                                           ╚═════════════╝ ╚═════════════╝
    ╔═════════════╗ ╔═════════════╗               ▼               ▼
    ║     WIN     ║ ║     WIN     ║        ╔═════════════╗ ╔═════════════╗
    ║ Application ║ ║ Application ║        ║ VMDSAP DOS  ║ ║ VMDOSAP DOS ║
    ║             ║ ║             ║        ║  Interface  ║ ║  Interface  ║
    ╚═════╤═══════╝ ╚═══════╤═════╝        ╚══╤════════╤═╝ ╚╤╤═══════════╝
          │                 │                 │   ┌────│────┘│
          ▼                 ▼                 ▼   ▼    ▼     ▼
 ┌──────────────────────────────────────────────────┬──────────┐
 │                                                  │          │
 │                   Windows                        │          │
 │                                                  │          │
 ├──────────────────────────────────────────────────┤          │
 │                                                  │          │
 │                   DOS 3.x                        │          │
 │                                                  │          │
 ├──────────────────────────────────────────────────┘          │
 │                                                             │
 │               Virtual DOS Machine Monitor (VDMM)            │
 │                                                             │
 └─────────────────────────────────────────────────────────────┘


Figure 7:  Memory occupied by the DOS and other fixed items, such as the ROM
           BIOS, is shared between all virtual machines. Thus the system
           consumes physical memory sufficient for only one copy of each of
           these items and shares this memory between all the virtual
           machines using the memory management capabilities of the 386.

                          Components Within the
                          Address Space of a VDM

                         ╔═══════════════════════╗ FFFFFH
                         ║          ROM          ║
                         ║                       ║
                         ╠═══════════════════════╣
                         ║       EGA, etc.       ║
                         ╠═══════════════════════╣ 9FFFFH
                         ║                       ║
                         ║      Application      ║
                         ║         Area          ║
                         ║                       ║
                         ╠═══════════════════════╣
                         ║                       ║
                         ║          DOS          ║
                         ║                       ║
                         ╠═══════════════════════╣
                         ║       Int Vect        ║
                         ║       BIOS Data       ║ 00000H
                         ╚═══════════════════════╝


Figure 8:  This diagram shows how a display adapter is virtualized.

                          ┌────────────────────┐
                          │Virtual Video Buffer│
╔═════════════════╗       └────────────────────┘
║                 ║
║    ROM BIOS     ║                                      ╔════════════════╗
║                 ║    ╔═══════════════╗                 ║      VDMM      ║
╚═════════════════╩═══►║               ╟──────────────┐  ║    & Virtual   ║
                       ║               ║  Gets Changes╞═►║ Display Driver ║
┌─────────────────────┐║ Virtual Video ╟──────────────┘  ╚═══════╦════════╝
│Writes into Video RAM│║      RAM      ║                         ║
└─────────────────────┘║               ║                         ║
╔═════════════════╦═══►║               ║                 ┌───────╨────────┐
║                 ║    ╚═══════════════╝                 │Sends Changes to│
║                 ║                                      │ Windows/VMDOSAP│
║       DOS       ║                                      └───────╥────────┘
║   Application   ║                                              ║
║                 ║  ╔══════════════════╗                        ║
║                 ║  ║ ░░░░░░░░░░░░░  ≡≡║                ╔═══════▼════════╗
║                 ║  ║ ░░░░░░┌──────────╨────────────────╢                ║
╚═════════════════╝  ║ ░░ ◄══╡Renders Changes into Window║    VMDOSAP     ║
                     ║ ░░░░░░└──────────╥────────────────╢                ║
                     ║ ░░░░░░░░░░░░░    ║                ╚════════════════╝
                     ╚══════════════════╝

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

Programming in C the Fast and Easy Way with Microsoft QuickC

Augie Hansen☼

The widespread use and popularity of the C language, which began in the
early 1970s with researchers at Bell Laboratories, is likely to endure. C
has matured in the marketplace and is becoming standardized, just as
traditional languages like FORTRAN and COBOL did.

The original purpose of C was to provide a malleable high-level assembler
for reprogramming the UNIX(R) operating system for portability. Programmers
have found the language and its support tools to be an inviting and highly
compliant medium for system-level work and applications programming.

The typical environment for C programmers, whether working on large
multiuser systems or alone on PCs, has been to use a set of separate
programs for each step in the process of translating source code into
executable programs. The sequence is edit, compile (and sometimes
assemble), link-edit object files and library modules, and then test.
Depending on the results, the sequence might need to be repeated a number
of times.

Development can be tedious, even with a reasonable set of support tools,
and is usually time-consuming. Making C programming faster and easier than
is possible with traditional methods has become an important objective for
vendors of C programming products.

In the past few years, we have seen the introduction of C interpreters as
front-end training, testing, and development systems. Interpreters
typically give quick feedback to the user by avoiding the edit-compile-
link-test cycle. However, such systems usually lack the flexibility for
handling large programming projects.

When an interpreter is used in program development, the source code
produced is subsequently used as input to more traditional C compiler
systems, which offer speed and size optimizations, the ability to handle
multiple modules, provisions for custom libraries, and other convenient
features.

C compiler environments, the next stage in the evolution of C programming
systems, are now the rage, and for a good reason: they provide the desired
quickness on both ends of the development process. Creating a program is
easier and faster than ever, and the final program runs at warp one.


Introducing QuickC

QuickC is an integrated C programming environment that provides all of the
features you would expect in a mature program-development system while
offering you the benefit of quick feedback and appropriate hand-holding
when needed. The QuickC compiler can be obtained as a separate product or
packaged with the Microsoft C Compiler Version 5.00.

QC.EXE is the QuickC integrated environment program. It is great for
learning C, useful in prototyping, and completely compatible with
Microsoft C 5.00. The integrated environment consists of a tightly coupled
full-screen editor, compiler, linker, and debugger. QuickC also has a
built-in "make" feature that automates most of the process of compiling
and linking programs.

QCL.EXE, which is the QuickC equivalent of the C 5.00 CL control program,
controls standalone operation. You can use the QCL program together with
MAKE and the special version of the linker, LINK, to compile and link
programs in the traditional manner.

The use of separate programs is slower than working with the integrated
environment because the number of disk accesses is significantly greater,
but the approach lends itself well to automation via batch files and make
files. Using a separate MAKE program gives you more latitude in preparing
a program. It also provides greater access to other programs and features
of the operating system than the built-in program-building feature of the
integrated environment.

QuickC is compatible with Microsoft C 5.00 in several ways. The compilers
accept the same source code, use the same object file formats, and link
with compatible libraries.

Four memory models are supported: small, compact, medium, and large.
Unless in-structed otherwise, the medium model is the default model used
by QuickC. QuickC has no huge keyword to objects that exceed 64Kb in size,
nor does it support the huge memory model.

The near and far addressing keywords, however, are sup-ported. They are
not part of the formal definition of C, but they are useful when you are
pro-gramming the segmented archi-tecture of the Intel(R) 8086/80286
microprocessor family.

The few differences between QuickC and C 5.00 will affect those
programmers who have exacting requirements for optimization of execution
speed, for program size, or who need greater addressing flexibility than
QuickC offers. Using QuickC for fast prototyping and then moving to the
Microsoft Optimizing C Compiler to fine-tune program performance creates a
comfortable working environment. The optimizing compiler has features for
improving code size and execution speed.


Integrated Environment

The QuickC integrated environment, contained in QC.EXE, is shown
schematically in Figure 1. The integrated environment is a collection of
essential components that work together to increase the speed of creating
and testing C programs.

The built-in full-screen editor accepts input from both a keyboard and a
mouse. It is a complete WordStar(R) compatible programmer's editor that
accepts WordStar commands (a Ctrl-key combination) and IBM-compatible
keyboard commands such as those found in Microsoft Word and many other
visual text editors. Also, it is integrated with the compiler so that you
can find errors and fix them quickly.

Compiling at 7000 lines per minute, the QuickC in-memory compiler can
capture information about errors (up to 26 of them) in a single
compilation and feed the information back to the editor. For each error
found, the editor places the cursor on the location in the source file
where the error occurred.

The compiler can produce a memory-based program or a disk-based
executable; the choice is yours. Usually you will run entirely in memory
to get the greatest speed advantage during development and then save the
program to disk for later use as a standalone program.

To help you find program bugs, QuickC incorporates a source-level debugger
that is a comprehensive subset of the well-known CodeView(R) debugger. The
debugger lets you single-step program execution, establish watch
expressions and display their values, set multiple breakpoints, and search
for functions by name. In addition, you can switch between displays of the
program and its output by using a simple keypress command.

Multimodule programs necessitate a significant amount of bookkeeping work.
Such work is best left to computer programs, and QuickC obliges. A built-
in program maintainer patterned after MAKE automatically prepares a make
file from a program file list that you provide. As you modify your
program, QuickC will keep each object module up to date with respect to
its source file and controls linking if the executable file is to be
preserved as an EXE file on disk.


Core Library

QuickC has a set of run-time library routines built right into the
compiler. If a program uses only core routines, all external references
can be resolved in memory, resulting in very high-speed program
preparation. Figure 2 lists the core routines.

If the program needs any routines that are not in the core set, they must
be read from a library file on disk. QuickC uses the medium-memory model
by default, so it normally looks for the library file MLIBCE.LIB or
MLIBC7.LIB in the default library directory. The library to be chosen
depends on whether floating-point emulation or 8087/80287 routines are
used. Alternatively, QuickC looks for the component medium-model files if
you choose not to create a combined library during installation. The
component libraries are MLIBC.LIB, MLIBFP.LIB, EM.LIB or 87.LIB, and
LIBH.LIB.

Because searching a large file like MLIBCE.LIB takes a lot of time, you
can speed up the process by creating a small library that contains only
the routines your program needs. QuickC searches for a file named
QUICK.LIB, or one you name on the QC invocation command line, if it cannot
resolve external references by using the core routines.

The QuickC system interacts with you via a menu bar and pull-down menus
that you can control from the keyboard and a mouse, if one is installed.
All frequently used selections can also be made by a shortcut method, such
as pressing a function key, a Ctrl-key or Alt-key combination, or some
other special key.

Some of the available menu windows can be seen in Figures 3, 4, 5, and 6☼.

Figure 3, which shows the editing window, adorned with scroll bars, and
the File menu, indicates how the QuickC drop-down menus appear. Once a
menu is selected, either from the keyboard by typing the first letter of
its name while holding down the Alt key or by clicking a mouse button on
the menu name, you can use either the arrow keys plus Enter or a mouse
click to select an option.

The figure shows the highlight on the "Set Program List" option. When you
select it, this option leads to a dialog box (signified by "...") on which
you select the names of files that comprise the program you want to
compile. The dialog box lists all source files in the current or specified
directory. You must indicate which of them comprise the sources of your
program by using a "point and shoot" selection process. The selected files
make up the program list that QuickC uses to build the program.


In-Memory Compilation

The advantage of in-memory compilation is that all source files, temporary
files, and most library routines are held in high-speed, primary memory.
This dramatically cuts the time it takes to produce an executable program
compared with the traditional approach, which is usually very disk-
intensive.

The integrated environment facilitates debugging with close editor-
debugger interaction. By placing the cursor on an error in your source
code, you are spared the time and effort it takes to reload the editor,
read in the source file, and move the cursor to the location of the
error.

In the integrated environment, errors are described in sufficient detail
so that reference to manuals is usually unnecessary. For example, Figure 4☼
shows an error message in the pop-up window at the bottom of the screen.
The message is detailed enough to be helpful, and the cursor points to the
line containing the error.

Raw compilation speed on a given machine is about three-to-one in favor of
QuickC compared with a standard C compiler. The programmer perceives the
improvement in speed to be even greater because there is no need for
continually loading and exiting a series of programs and suffering the
concomitant disk-access times.


Library Support

The standard run-time library provided with QuickC includes all of the
routines that accompany the Microsoft Optimizing C Compiler. The library,
compatible with XENIX(R), provides extensive DOS support (bdos and intdos,
for example), and includes routines that conform to the draft-proposed
ANSI standard for C.

A new graphics library is included in the QuickC package. It contains more
than forty graphics routines that handle configuration, palette and
attribute settings, outputting text and images, and a variety of other
color and monochrome graphics tasks.

A new set of DOS and BIOS interface routines add considerably to the
support for PC hardware interactions. While not portable to other hardware
environments, these routines allow you to get the most from a PC and its
peripheral devices.


Standalone Operation

For programmers who prefer the traditional approach, QuickC provides
separate programs for compiling and linking. QCL.EXE reads a list of
optional control switches and filenames from the DOS command line and
produces standard Microsoft format object files that you can put into
libraries (use LIB.EXE) or link with other objects and libraries in order
to produce executable programs.

MAKE can control the entire process; it reads a list of instructions from
a make file, and uses the time stamps on files to determine which files
need to be recompiled and linked. MAKE is more flexible than the program
maintenance feature built into the QuickC integrated environment.

The library maintainer, LIB.EXE, permits you to add modules to a library,
delete modules, copy modules to disk files, and combine libraries. It
allows you to create your own custom libraries and modify existing
libraries to meet special needs. For instance, to create a custom
QUICK.LIB file of routines for a given program, you can use LIB to extract
copies of the needed routines and then use LIB again to prepare the
QUICK.LIB file.


Sample Session

Figures 7 and 8 show the two source files from which the TOUCH program, a
useful adjunct to the MAKE program provided with the compiler, is built.
TOUCH updates the date and time stamps associated with a file to the
current date and time. This gives you a convenient way to force all or
selected files for a program to be remade, even if the files were not
modified since the last successful compile.

TOUCH accepts command-line options -c and -v. TOUCH normally creates a
named file if the file does not exist. The -c option tells TOUCH not to
create any files. The -v option tells TOUCH to be verbose, giving the user
a play-by-play description of what it is doing. This option caters to
those programmers who like chatty programs.

Figure 9 lists the make file and linker response files produced by the
automatic program maintainer feature of QuickC. The make file is
compatible with and can be used by the standalone MAKE command. The linker
response file provides information needed by the LINK program when it
combines object modules and libraries to produce an executable program.

The source code is presented in its final form. However, to demonstrate
the debugging capabilities of QuickC, we will introduce several deliberate
errors into TOUCH.

Program errors fall into two general categories: syntactic and semantic.
Syntax errors result from how a program is put together. The rules for
writing programs in C, as is the case for nearly all computer languages,
are precise and rigid. Compilers have to be unforgiving in insisting on
correct syntax. A missing semicolon, which effectively concatenates two
perfectly good statements into one illegal one, is one of the more common
syntax errors.

Semantic errors result from the contextual meaning of a program's
statements. Does the program run to completion and produce correct
results? If not, it probably contains semantic errors, which can be
further categorized into execution errors or logic errors.

Execution errors are requests written in a perfectly acceptable way that
are impossible to fulfill, for example, dividing by zero. A program
containing such an error aborts with a run-time error. Logic errors also
get by the compiler because the syntax is correct, and the program
containing them runs fine, but it produces incorrect results. Such a
program is not likely to become a best-seller.

QuickC contains the tools to detect and correct both syntax errors and
semantic errors. Syntax errors are caught during compilation and reported
one at a time in a pop-up window. Up to 26 compile-time errors are
maintained in a list that can be scanned in either direction.

The "Warning Level" setting in the compile dialog box controls the level
of checking and reporting to be done by QuickC (see Figure 6☼). The higher
the warning level (they range from 0 to 4), the more detailed the checking
and reporting. The resulting list of warnings and error messages will
guide you in wringing out all compile-time errors.

Figure 4☼ shows an error caught by the compiler. It is the common error of
using assignment where the programmer really intended a test for equality.
The statement

  if (fclose(fp) = EOF)
    {  ...  }

should use "==" instead of "=". The error is detected because a function
is not an lvalue, so nothing can be assigned into it. If the left-hand
side of the assignment were an 1value, the compiler would be happy and
compile without comment. Your program would execute, but probably
incorrectly.

Another common error is a missing parameter in an fprintf statement. Try
leaving the stderr parameter out of an error message line. The compiler
will detect the problem because it uses function prototypes to determine
how many parameters a function takes and what data types the parameters
have. In this case, QuickC will report incorrect parameter types. Because
fprintf takes a variable number of arguments, the compiler cannot
determine that the number of parameters is incorrect.

You could still have semantic errors lurking about in your program, which
the compiler cannot detect. Traditionally, C programmers have resorted to
inserting printf statements in their programs to check values at critical
locations during test runs. This is tedious, time-consuming, and
unnecessary because of the built-in debugger provided in the QuickC
integrated environment.

To use the debugger, which contains an extensive subset of CodeView
features, you must first compile your program with the Debug option set so
that symbolic information needed by the debugger is preserved in the
executable program (see Figure 6☼). Select the Run item (Alt-R) from the
menu bar and then the Compile(C) dialog box. Use the Tab key to select
the Debug option and the Spacebar to toggle it on. Press Enter to compile
the program.

After successful compilation, select the Debug item (Alt-D) from the menu
bar and set watch expressions and breakpoints. A watch expression can be
as simple as a variable name or as complex an expression as you need to
check program operation. You can set watch expressions one at a time or
several at once by separating them from each other with a semicolon. Watch
expressions are displayed in a separate watch window at the top of the
screen during program execution (see Figure 5☼).

A breakpoint is a line in the program at which execution should stop to
let you observe the values of watch expressions or just to observe the
displayed output using the screen-switching feature of QuickC. It is
simple, effective, and a lot easier and faster than placing printfs all
over your program.

You can set multiple watch expressions and breakpoints. If the ones you
set initially don't do the job, it is a simple matter to clear them and
set new expressions and breakpoints. There is no need to recompile as
there would be with embedded printf statements.

Introducing deliberate errors into TOUCH.C will help you to learn how to
use the debugger. Try changing the sense of some logical tests (use "!="
instead of "==") and observe the effect on the output.

Deleting the two lines in TOUCH.C just before the first while loop
produces an interesting result. The program will not skip the program name
argument (argv[0]), and all following arguments will be treated as
filenames even if they are preceded by a dash, which is the specified
option flag. To fix the problem, you could set watch expressions on the
command-line argument parameters, argc and *argv, by typing "argc; *argv"
in the watch dialog box and put breakpoints on statements where these
values should have just changed, such as the first statement inside each
loop.

You can provide a command line by selecting the Set Runtime Options (O)
from the Run (Alt-r) menu before you run the program. Type only the
command-line arguments; do not type the program name──it is already known
to QuickC. Use something like

  -c one two three

as a command line. After running the program, you can use the DOS Shell
(D) selection of the File menu (Alt-F) to get to a temporary DOS command
line. Run a DIR command and check the list of files. Don't be surprised to
find that TOUCH created files named "-c", "one", "two", and "three", even
though we instructed it not to create any files.

The thorough syntax-checking and integrated debugging features provided by
QuickC are as important to a programmer as the swiftness of the compiler.
Become familiar with these easy-to-use tools and you will never want to be
without them.


Summary

For easy learning and use of a C compiler, raw compilation speed, and
overall flexibility, it's hard to beat QuickC. Compiling in the integrated
environment makes trying out an idea exhilarating. Modifying the program
until it does exactly what you want is as simple as playing "what if" with
a spreadsheet. It's about time programmers received the same kind of help
that financial analysts have had for years.

When we took the big step from batch processing to interactive processing
on mainframes back in the 1960s, programmer productivity received a great
boost. The move to swift and capable integrated programming environments
on personal computers provides the opportunity to make similar, perhaps
even greater, productivity gains in the 1980s.


Figure 1:  You can use separate libraries insteaad of the combined library.
           The default memory model is medium. You can also select small,
           compact, or large.

             ┌──────────────────────────────────────────────────┐
             │ The QuickC Integrated Environment and Libraries. │
             └──────────────────────────────────────────────────┘
      ╔═══════════════════════════╗        ╔════════════════════════════╗
      ║         QC.EXE            ║█       ║        MLIBCE.LIB*         ║█
      ╠═══════════════════════════╣█       ╠════════════════════════════╣█
      ║    Full-screen Editor     ║█       ║Combined Library File Merges║█
      ╟───────────────────────────╢█       ║ the following libraries:   ║█
      ║        Compiler           ║█       ║                            ║█
      ╟───────────────────────────╢█       ║    • MLIBC.LIB             ║█
      ║   Source-level Debugger   ║█       ║    • MLIBFP.LIB            ║█
      ╟───────────────────────────╢█       ║    • LIBH.LIB              ║█
      ║  Program List Maintainer  ║█       ║    • EM.LIB (or 87.LIB)    ║█
      ╟───────────────────────────╢█       ╚════════════════════════════╝█
      ║                           ║█         █████████████████████████████
      ║                           ║█       ╔════════════════════════════╗
      ║ "Core Subroutine Library" ║█       ║          QUICK.LIB         ║█
      ║                           ║█       ╠════════════════════════════╣█
      ║                           ║█       ║        Quick Library       ║█
      ╚═══════════════════════════╝█       ╚════════════════════════════╝█
        ████████████████████████████         █████████████████████████████


Figure 2:  The core library routines are defined in QC.EXE and speed up
           linking time by avoiding unnecessary disk accesses.

                          Core Library Routines

    abort        _fmalloc       isatty         puts         strlen
    access       _fmsize        itoa           read         strlwr
    atof         fopen          kbhit          realloc      strncat
    atoi         fprintf        longjump       remove       strncmp
    atol         fputc          lseek          rewind       strncpy
    bdos         fputs          ltoa           rmdir        strnset
    brk          fread          malloc         rmtmp        strpbrk
    calloc       free           _memavl        sbrk         strrchr
    chdir        _freect        memccpy        scanf        strrev
    chmod        fscanf         memchr         segread      strset
    clearerr     fseek          memcmp         setbuf       strspn
    close        fstat          memcpy         setjmp       strtok
    creat        ftell          _memmax        setmode      strupr
    dosexterr    fwrite         _memset        setvbuf      system
    eof          agetch         mkdir          signal       tell
    _exit        getche         movedata       spawnl       time
    exit         getcwd         msize          spawnv       tmpfile
    _expand      _getdate       _nfree         sprintf      tmpnam
    fclose       _gettime       _nheapchk      sscanf       tolower
    fflush       getenv         _nheapset      strcat       toupper
    _ffree       gets           _nheapwalk     strchr       tzset
    _fheapchk    halloc         _nmalloc       strcmp       ultoa
    _fheapset    hfree          _nmsize        strcmpi      unlink
    _fheapwalk   int86          onexit         strcpy       vfprintf
    fgetc        int86x         open           strcspn      vprintf
    fgets        intdos         printf         strdup       vsprintf
    filelength   intdosx        putch          stricmp      write
    flushall


Figure 7:  TOUCH.C

/*
 * PROGRAM: TOUCH
 *
 * DESCRIPTION:  Update the last modification time of a file or a
 *     group of files to the current time.
 *
 * ENTRY:  A list of files to "touch".  The filenames can be preceded
 *     by one or both of the following options:
 *
 *          -c     don't create any files
 *          -v     operate in verbose mode (report activity)
 *
 * SYNTAX:
 *      touch [-cv] filename ...
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <io.h>
#include <errno.h>
#include <sys\types.h>
#include <sys\stat.h>
#include <sys\utime.h>

#define ERR     0x7FFF
#define MAXNAME 8

typedef enum { FALSE, TRUE } BOOLEAN;
extern void usage(char *);

int
main(argc, argv)
int argc;
char *argv[];
{
     int ch;                  /* character buffer */
     char *cp;                /* character pointer */
     int badcount;            /* number of files that cannot */
                              /* be successfully updated */
     struct stat statbuf;     /* buffer for stat results */
     BOOLEAN errflag;         /* error flag */
     BOOLEAN cflag;           /* creation control flag */
     BOOLEAN vflag;           /* verbose control flag */
     FILE *fp;                /* file pointer */

     static char pgm[MAXNAME + 1] = { "touch" };

     /*
      * Initialize flags and variables
      */
     errflag = cflag = vflag = FALSE;
     badcount = 0;

     /*
      * Move past the command name argument and look for
      * optional arguments (signaled by a leading dash)
      */
     ++argv;
     --argc;
     while (argc > 1) {
          cp = *argv;
          if (*cp != '-')
               /* not an option flag */
               break;

          /*
           * Having seen an option flag ('-'), look at
           * any option letters that follow it in the
           * current argument string
           */
          for (++cp; *cp != '\0'; ++cp)
               switch (*cp) {
               case 'c':
                    /* don't create files */
                    cflag = TRUE;
                    puts("CREATION flag set");
                    break;
               case 'v':
                    /* verbose -- report activity */
                    vflag = TRUE;
                    break;
               default:
                    fprintf(stderr, "%s: unknown option %c\n",pgm, *cp);
                    usage(pgm);
                    exit(ERR);
               }
          ++argv;
          --argc;
     }

     /*
      * Update modification times of files
      */
     for (; argc-- > 0; ++argv) {
          if (stat(*argv, &statbuf) == -1) {
               /* file doesn't exist */
               if (cflag == TRUE) {
                    /* don't create it */
                    ++badcount;
                    continue;
               }
               else if ((fp = fopen(*argv, "w")) == NULL) {
                    fprintf(stderr, "%s: Cannot create %s\n",
                         pgm, *argv);
                    ++badcount;
                    continue;
               }
               else {
                    if (fclose(fp) == EOF) {
                         perror("Error closing file");
                         exit(ERR);
                    }
                    if (stat(*argv, &statbuf) == -1) {
                         fprintf(stderr, "%s: Can't stat %s\n", pgm, *argv);
                         ++badcount;
                         continue;
                    }
               }
          }
          if (utime(*argv, NULL) == -1) {
               ++badcount;
               perror("Error updating date/time stamp");
               continue;
          }
          if (vflag == TRUE)
               fprintf(stderr, "Touched file %s\n", *argv);
     }

     return (badcount);
}


Figure 8:  USAGE.C

/*
 * usage()
 *
 * DESCRIPTION: Display an informative usage message using the
 *  actual program name, which may have been changed by the user.
 *
 * ENTRY: A pointer to the program name.
 */

#include <stdio.h>

void
usage(pname)
char *pname;
{
    fprintf(stderr, "Usage: %s [-c] [-v] file ...\n", pname);
    fprintf(stderr, "\t-c  Do not create any files\n");
    fprintf(stderr, "\t-v  Verbose mode -- report activities\n");
}


Figure 9:  Make File

#
# Program: Touch
#

.c.obj:
     qcl -c  -Iusage.c -W0 -Ze -Zid -AM $*.c

usage.obj : usage.c

touch.obj : touch.c

Touch.exe : usage.obj touch.obj
     del Touch.lnk
     echo usage.obj+ >>Touch.lnk
     echo touch.obj  >>Touch.lnk
     echo Touch.exe >>Touch.lnk
     echo Touch.map >>Touch.lnk
     link @Touch.lnk;


Link Information - Touch.lnk
============================
touch.obj+
usage.obj
Touch.exe
Touch.map

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

Character-Oriented Display Services Using OS/2's VIO Subsystem

───────────────────────────────────────────────────────────────────────────
Also see the related article:
A Survey of OS/2 Display Services
───────────────────────────────────────────────────────────────────────────

Ray Duncan☼

OS/2 provides the programmer with a fast and powerful set of video I/O
functions. This particular group of API calls is important because the
average user bases his assessment of the quality of application software
largely on the ease and speed of his interaction with the program. He has
neither the opportunity nor the experience necessary to peek into the
source code and appreciate the elegance of its structure or the
appropriateness of its algorithms, but the rapidity with which the
application builds and updates its screen displays speaks for itself.
Naturally, control of the video display has become one of the major areas
of concern for the PC programmer.

Unfortunately, the built-in video drivers available in MS-DOS(R) have done
little to aid the programmer in his quest for competitive performance. The
speed with which MS-DOS transfers text to the screen might be adequate for
EDLIN, but it certainly doesn't suffice for sophisticated word processors,
while the degree of control MS-DOS provides over character colors and
attributes, display mode, and cursor shape and position is rudimentary at
best. Consequently, the authors of most popular MS-DOS applications have
felt justified in abandoning portability, bypassing the operating system
altogether, and directly manipulating the video controller to achieve the
desired throughput.

In the real mode, single-tasking world of MS-DOS, such hardware-dependent
display techniques rarely cause any significant problems, but the same
cannot be said for the multitasking, protected-mode OS/2 environment. A
program that tries to commandeer the video controller will either be
terminated with a protection fault, or, if it manages to circumvent the
memory-protection mechanisms, wreak havoc among the other processes that
are active in the system. The designers of MS(R) OS/2 correctly perceived
that they could head off such anarchy only by removing the motivation for
programmers to go around the operating system and placed a high priority
on the creation of a battery of video services rich enough and efficient
enough to satisfy the needs of any reasonable application program.

And that is just what they did. MS OS/2 provides application programs with
a huge assortment of display functions that cover the full range of
hardware independence, from the sophisticated graphical and rich text
capabilities of the Microsoft(R) OS/2 Presentation Manager to the ability of
an application to temporarily lock onto the logical or physical display
buffer and modify its contents. (See "A Survey of OS/2 Display Services.")
This article will focus specifically on the VIO subsystem, which occupies
the middle ground (both in complexity and power) of OS/2's video cap-
abilities. The other techniques for driving the display under OS/2 will be
discussed in future articles.


The VIO Subsystem

OS/2's VIO subsystem provides character-oriented display services suitable
for program editors, simple pop-up utilities, compilers and interpreters,
and the like. The VIO function calls can be regarded as a superset of
those available from MS-DOS and the IBM(R) PC ROM BIOS in real mode, and
their actions can be grouped into the following general categories:

  ■ transferring strings of text to the screen at any position
  ■ controlling such text attributes as blink, underline, and color
  ■ controlling the cursor shape and position
  ■ scrolling and clearing the screen
  ■ setting the display mode
  ■ support for partial or full-screen pop-up windows

The VIO subsystem is implemented as a dynamic link library (VIOCALLS.DLL)
that resides on the system disk. Programs are bound to the VIO routines
when they are loaded for execution, rather than at link time. Thus, the
VIO library can be replaced at any time without recompiling or even
relinking the client applications.

Although the VIO subsystem does not support graphics operations aside from
providing a means to get in and out of graphics display modes, programs
that confine themselves to the use of VIO calls and do not access the
logical or physical screen buffer will run properly in a window under the
Presentation Manager without any changes. How is this possible? Under the
Presentation Manager, the normal VIO dynalink library is replaced by a new
"Windows-aware" library that maps each character and its attributes into
an appropriate pattern of pixels and clips the output appropriately for
the application's current window size.

Figure 1 shows a summary of the VIO function calls. We'll take a close
look at some of the most commonly used VIO services, together with sample
C and MASM source code. Note that the C code shown assumes use of the
large model, and the parameter called a "VIO handle" in the function
descriptions is always zero in the Software Development Kit version of MS
OS/2.


Displaying Text

There are a number of different VIO functions that can be used to place
text on the screen. The simplest is VIOWRTTTY, which is called with the
address and length of a string and a VIO handle and returns a status code
of zero if the write succeeded or an error code if it failed. The control
characters line feed, carriage return, backspace, and bell code are
properly handled, automatic line wrap and screen scrolling are provided
(unless they are intentionally disabled), and the cursor position is
updated to lie at the end of the newly displayed text. Figure 2 is an
example of the use of VIOWRTTTY.

The VIOWRTTTY call is functionally similar to a call to DOSWRITE with the
stdout handle (the preassigned handle for the standard output device), but
is much faster and immune to redirection. As with DOSWRITE, and unlike the
other VIO calls, ANSI escape sequences can be included in the text stream
to control cursor position and character attributes.

Ordinarily, the only error code returned by VIOWRTTTY is an invalid VIO
handle. Providing the function with an invalid string address or length
does not result in an error code but may well cause a General Protection
Fault (Trap D), terminating the process.

Three additional VIO calls are available for displaying text:
VIOWRTCHARSTR (write character string), VIOWRTCHARSTRATT (write character
string with attribute), and VIOWRTCELLSTR (write string of
character/attribute pairs).

These services are faster than VIOWRTTTY and offer direct control over
screen placement. They are not sensitive to the presence of such control
characters as carriage return or line feed; any control characters
embedded in the string are simply displayed as their graphics character
equivalents. These functions do not affect the current cursor position and
only support limited line wrapping: if a string is too long for the
current line, it will wrap onto the next line. However, if the end of the
screen is reached, any remaining characters are discarded, the screen is
not scrolled, and an error code is not returned.

VIOWRTCHARSTR is the simplest of the three functions listed above. It
accepts the address and length of a string, the screen position at which
to begin writing the string, and a VIO handle. The new text assumes the
attributes of the characters that were previously displayed at the same
screen positions.

VIOWRTCHARSTRATT is similar to the VIOWRTCHARSTR function, except for one
additional parameter: the address of an attribute byte that is applied to
every character in the string (see Figure 3). On monochrome adapters, the
attribute byte specifies normal or reverse video, blink, underline, and
intensity (see Figure 4). On color adapters in text modes, the attribute
byte contains the background color in the upper four bits and the
foreground color in the lower four bits (see Figure 5).

VIOWRTCELLSTR displays a string that consists of alternating character and
attribute bytes.VIOWRTCELLSTR is designed to be used in combination with
VIOREADCELLSTR to restore an area of the display that was previously saved
into a buffer. A programmer would not ordinarily select this function for
normal text output, because generation of initialized strings with
embedded attribute bytes is awkward in most languages, such strings are
relatively bulky, and it is rare that an application needs to display a
string where the attribute byte of each successive character is
different.

The VIO functions VIOWRTNCHAR (write string of identical characters),
VIOWRTNATTR (write string of identical attribute bytes), and VIOWRTNCELL
(write string of identical characters and attributes) offer some special
capabilities that supplement the previously discussed text display
functions and require similar parameters. VIOWRTNATTR replicates an
attribute byte across a selected area without changing the text at that
position and can rapidly alter the appearance of a display field.
VIOWRTNCHAR and VIOWRTNCELL replicate a character or a character/attribute
pair respectively and allow extremely efficient drawing of borders and
similar screen objects.


Scrolling and Clearing

The four kernel services VIOSCROLLUP, VIOSCROLLDN, VIOSCROLLLF, and
VIOSCROLLRT can clear a window of arbitrary size and position or scroll it
up, down, left, or right any number of columns or rows. Any desired
character and attribute byte can be specified to fill the newly blanked or
scrolled lines. All four functions have the following parameters: the
screen coordinates of the upper-left corner and lower-right corner of a
window, the number of lines to be scrolled or blanked, the address of a
character and attribute pair to be used to fill the blanked lines, and a
VIO handle.

Clearing the entire display in any mode, without first determining the
screen dimensions, can be accomplished as a special case call of any of
the four scroll functions, using an upper-left coordinate of (0,0), a
lower-right coordinate of (-1,-1), and the value -1 as the number of lines
to scroll. Figure 6 demonstrates clearing the screen to ASCII blanks with
a normal video attribute. Scrolling a selected portion of the screen is
also easy, as shown in Figure 7.


Cursor Control

The function VIOSETCURPOS positions the cursor, while the parallel
function VIOGETCURPOS allows a program to obtain the current cursor
location. Both VIOSETCURPOS and VIOGETCURPOS use text coordinates and
assume a home position of (0,0) at the upper-left corner of the screen;
neither function can be called in graphics modes. Figures 8 and 9 show
examples of typical VIOSETCURPOS and VIOGETCURPOS calls.

The functions VIOGETCURTYPE and VIOSETCURTYPE are used to get or alter the
cursor height, width, and hidden/visible attributes. Both functions use a
four-word data structure with the same format to communicate the attribute
values──quite convenient when the cursor must be altered or hidden
temporarily (see Figure 10).


Display Mode Control

The functions VIOGETMODE and VIOSETMODE allow programs to query or select
the video display mode. Both calls use a data structure that contains
flags for the adapter type, color burst enable, and text versus graphics
mode; the number of displayable colors; the number of alphanumeric (text)
columns and rows; and the number of pixel columns and rows. Another
function, VIOGETCONFIG, returns the type of monitor and video adapter
installed in the system and the amount of memory present on the adapter.

This approach is open-ended: it allows the application to deal with the
adapter on the basis of its capabilities and doesn't require the
programmer to remember an ever-expanding list of "mode numbers" that are
less and less related to the display modes they represent. For example,
the code in Figure 11 selects 80 X 25, 16-color text mode with color
burst enabled.


Pop-up Support

The functions VIOPOPUP and VIOENDPOPUP allow background processes to
temporarily assert control of the screen and interact with the user.
These processes, launched with the DETACH command, are placed in a
special "black box" screen group; ordinarily the programs in this group
cannot perform KBD or MOV input calls, and thus their output to the screen
is discarded.

In order to gain access to the screen, the background process first calls
VIOPOPUP. This function has a wait/no-wait option: if wait is specified,
the calling process is suspended until the screen is available; if no-wait
is specified, the function returns immediately with an error code if the
screen cannot be preempted, for example, if another background process has
already called VIOPOPUP. If the VIOPOPUP call is successful, the current
contents of the screen, which belong to the foreground process, are saved
away, the screen is blanked and placed into an 80 X 25 text mode, and all
further VIO calls by the background process that issued VIOPOPUP are
directed to the active display.

At this point, the background process can interact freely with the user.
Other processes continue to run normally with their output going to the
usual virtual screen buffer, until they require input or call VIOPOPUP, at
which point they can be suspended. When the background process is finished
with its pop-up activities, it must call VIOENDPOPUP to release control of
the screen. The display belonging to the foreground process is then
automatically restored. Figure 12 shows an example of this procedure.

Since screen group switching is disabled during a VIOPOPUP...VIOENDPOPUP
sequence and the abrupt transition from a normal display to the largely
blank display of a pop-up (with a possible concomitant change in display
mode) can be startling and disruptive to the user, use of VIOPOPUP should
be kept to a minimum and reserved for true background processes. An
ordinary application that wishes to use pop-up windows as part of its
interaction can achieve more pleasant results by using the VIOREADCELLSTR
and VIOWRTCELLSTR functions to save and restore small portions of the
display.


Summary

MS OS/2's VIO subsystem provides a set of character-oriented display
functions for use by so-called "kernel apps," programs that use only OS/2
kernel services and do not rely on the presence of the Presentation
Manager. Most of the programs initially ported to OS/2 from MS-DOS will
likely be based on VIO services exclusively. This is because the VIO calls
do not require any drastic changes to the converted program's internal
logic; they are sufficiently powerful to make direct hardware access
unnecessary and can be invoked directly from high-level languages. Also,
the Presentation Manager itself will not be available during the
introductory period.


───────────────────────────────────────────────────────────────────────────
A Survey of OS/2 Display Services
───────────────────────────────────────────────────────────────────────────

The MS OS/2 Presentation Manager is based on Microsoft(R) Windows and
provides applications with a uniform graphical user interface. It supports
text fonts, windowing and clipping, pull-down menus, pop-up dialog boxes,
pointing devices, and a broad range of high-performance graphic drawing
and painting operations. Applications written to take full advantage of
the Presentation Manager's services must have a special structure and must
abide by an intricate set of conventions. However, the programmer's payoff
for making this effort is complete portability between machines and output
devices that support the Presentation Manager, transparent adjustment of
the program's output to compensate for the display's resolution and aspect
ratio, the ability to exploit available fonts, and a shortened learning
curve for the user.

Character-oriented applications can avoid the complexity of the
Presentation Manager graphical interface, while retaining device
independence and the ability to run in a window, by using the function
DOSWRITE together with the preassigned handles for the standard output (1)
and standard error (2) devices to send their output to the screen. ANSI
escape sequences embedded in the output allow for control of the video
mode, foreground and background colors, and cursor position. This method
is analogous to the use of Interrupt 21H Function 40H (Write to File or
Device) under MS-DOS Versions 2.x and 3.x and is most appropriate for
filters and other utility programs where redirectability of the output is
an important consideration.

For more flexibility and higher performance, character-oriented
applications can employ the OS/2 kernel's VIO family of services. The VIO
functions allow scrolling in all four directions, control over the cursor
shape, more versatile assignment of character attributes and colors, and
reading strings back from the screen buffer, among other things. The VIO
calls are roughly analogous to the ROM BIOS video driver (Interrupt 10H)
calls available under MS-DOS 2.x and 3.x in that they are immune to
redirection of the standard output and mostly ignore control codes
embedded in the text. Applications that use VIO calls and avoid other
hardware dependence will still run in a window under the Presentation
Manager.

Finally, we come to two hardware-dependent display methods that are
allowed by MS OS/2. Applications that need to present a graphics display
without the aid of the Presentation Manager, drive the controller in a
mode or resolution not supported by OS/2's built-in screen driver, or have
other special requirements might use these methods.

The first hardware-dependent display technique is to obtain a selector
from OS/2 that gives the application direct access to the screen group's
Logical Video Buffer. After making modifications to the contents of the
buffer, the program issues an additional command to refresh the actual
video display──this call has no effect if the application's screen group is
not currently in the foreground. Such applications will obviously not run
in a window under the Presentation Manager and may also have unexpected
effects on other programs running in the same screen group, but they will
not conflict with the operation of programs in other screen groups.

The second, and potentially more destructive, hardware-dependent display
technique is to obtain the actual physical address of the video refresh
buffer from OS/2. After "locking" the screen with a function call, the
application can write to the refresh buffer and modify the state of the
video controller directly, "unlocking" the screen with another function
call when it is finished. While the lock is in effect, the user is
prevented from switching to another screen group, and background tasks are
unable to obtain control of the screen so as to attract the user's
attention.


Figure 1:  VIO Subsystem Services at a Glance

Text Display Functions

VioWrtTty          Write text string to display, then reposition cursor
                   at end of string. Line feeds, carriage returns, tabs,
                   and backspaces are interpreted properly. Line-wrap
                   and screen scrolling are provided.

VioWrtCellStr      Write string of alternating characters and attribute
                   bytes to screen at specified position. Cursor
                   position is not affected.

VioWrtCharStr      Write character string to screen at specified
                   position. Each character takes on the attribute of the
                   previous characters at the same position. Cursor
                   position is not affected.

VioWrtCharStrAtt   Write character string to screen at specified
                   position, applying same attribute byte to each
                   character. Cursor position is not affected.

VioReadCellStr     Read string of characters and attributes from
                   specified position in display buffer to local buffer.

VioReadCharStr     Read string of characters from specified position in
                   display buffer to local buffer.

Replication Functions

VioWrtNAttr        Replicate attribute byte on screen n times, starting
                   at specified position. Cursor position is not affected.

VioWrtNCell        Replicate character and attribute byte on screen n
                   times, starting at specified position. Cursor position
                   is not affected.

VioWrtNChar        Replicate character on screen n times, starting at
                   specified position. Cursor position is not affected.

Cursor Size and Position

VioGetCurPos       Get cursor position.

VioSetCurPos       Set cursor position.

VioGetCurType      Get cursor shape and size.

VioSetCurType      Set cursor shape and size.

Scroll or Clear Screen

VioScrollDn        Scroll entire screen or portion of screen down by 1
                   to n lines, filling new lines with specified character
                   and attribute, or erase part or all of screen.

VioScrollLf        Scroll left or clear screen as above.

VioScrollRt        Scroll right or clear screen as above.

VioScrollUp        Scroll up or clear screen as above.

Display Mode Control

VioSetANSI         Turn interpretation of ANSI escape sequences on
                   or off.

VioSetCP           Select code page used to display text data.

VioSetFont☼        Downloads a display font into the video adapter and
                   defines the dimensions of a character cell.

VioSetMode         Select current display mode.

VioSetState☼       Set palette registers, border color, or
                   blink/intensity toggle.

Mode Information

VioGetANSI         Get state of ANSI driver (on or off).

VioGetBuf          Get selector for logical video buffer of current
                   screen group and buffer length.

VioGetConfig       Get information about adapter type, display type,
                   and amount of memory on adapter board.

VioGetCP           Get identifier for current code page in use for text
                   display.

VioGetFont☼        Get character cell dimensions and address of bit
                   table for current or specified font.

VioGetMode         Get current display mode. Information returned
                   includes adapter type, number of colors, vertical and
                   horizontal resolution in both characters and pixels.

VioGetPhysBuf☼     Get selector for physical video buffer.

VioGetState☼       Get current settings of palette registers, border
                   color, and blink/intensity toggle.

Miscellaneous Functions

VioPrtSc           Print screen.

VioPrtScToggle     Turn echo of display to print device on or off.

VioScrLock☼        Disable screen switching (used by a process that is
                   updating the physical video display buffer directly).

VioScrUnLock☼      Enable screen switching (used when a process is
                   finished with direct access to physical display
                   buffer).

VioShowBuf         Force update of physical display buffer from logical
                   buffer.

Pop-up Support

VioPopUp           Allocate full-screen text-mode pop-up display (used
                   by background process, for example, a TSR
                   program, or to notify user of errors while process's
                   screen group is not selected).

VioEndPopUp        Deallocate pop-up display screen.

VioSavReDrawWait☼  Allows a process to be notified when its screen
                   should be saved or redrawn. Used by graphics mode
                   program to recover from screen group switch or a
                   pop-up by another program.

VioSavReDrawUndo☼  Cancels a VioSavReDrawWait call by another thread
                   within the same process.

VioModeWait☼       Allows a process to be notified when it should
                   restore the video display mode. Used by graphics
                   mode program to recover from screen group switch
                   or a pop-up by another program.

VioModeUndo☼       Cancels a VioModeWait call by another thread
                   within the same process.

VIO Function Replacement

VioRegister☼       Register video subsystem. Allows replacement of
                   system's default routine for any or all VIO functions
                   with new driver routines.

VioDeRegister☼     Deregister video subsystem.


Figure 2:  Using VIOWRTTTY to write the string "Hello World" to the screen
           at the current cursor position, then move to a new line (scrolling
           if necessary). The cursor position is updated appropriately after
           the write.

Microsoft Macro Assembler:
                                    ∙
                                    ∙
                                    ∙
          push ds             ; address of string to display
          push offset DGROUP:msg
          push msg_length     ; length of string
          push 0              ; VIO handle (reserved)
          call VIOWRTTTY      ; transfer to OS/2
          or   ax,ax          ; did write succeed?
          jnz  error          ; jump if write failed
                                    ∙
                                    ∙
                                    ∙
msg       db   'Hello World',0dh,0ah
msg_length equ $-msg

Microsoft C:

extern unsigned far pascal VIOWRTTTY(char far *, unsigned, unsigned);
                                    ∙
                                    ∙
                                    ∙
     int status;
     static char msg[]="Hello World\r\n";
                                    ∙
                                    ∙
                                    ∙
     status=VIOWRTTTY(msg, sizeof(msg)-1, 0);
                                    ∙
                                    ∙
                                    ∙


Figure 3:  Using the VIOWRTCHARSTRATT function to write to the screen. This
           code displays the string "Hello World" in reverse video at cursor
           location (10,5), column 10, row 5. The VIOWRTxxx calls support
           high-speed output of strings of any length (up to the size of the
           screen), with simultaneous control over character position and
           attributes.

Microsoft Macro Assembler:
                                    ∙
                                    ∙
                                    ∙
          push ds             ; address of string
          push offset DGROUP:msg
          push msg_length     ; length of string
          push 5              ; Y position
          push 10             ; X position
          push ds             ; address of reverse video attrib
          push offset DGROUP:rev_attr
          push 0              ; VIO handle (reserved)
          call VIOWRTCHARSTRATT    ; transfer to OS/2
          or   ax,ax          ; did write succeed?
          jnz  error          ; jump if write failed
                                    ∙
                                    ∙
                                    ∙
msg       db   'Hello World'
msg_length equ $-msg

rev_attr  db   70h            ; reverse video attribute

Microsoft C:

extern unsigned far pascal VIOWRTCHARSTRATT(char far *, unsigned,
     unsigned, unsigned, char far *, unsigned);
                                    ∙
                                    ∙
                                    ∙
     int status;
     static char msg[]="Hello World";
     char rev_attr=0x70;
                                    ∙
                                    ∙
                                    ∙
     status=VIOWRTCHARSTRATT(msg,sizeof(msg)-1,5,10,&rev_attr,0);
                                    ∙
                                    ∙
                                    ∙


Figure 4:  Attribute byte for monochrome display adapter (MDA), EGA in
           monochrome text mode, or Hercules(R) Graphics Card in text mode.

                      7 │ 6   5   4 │ 3 │ 2   1   0
                     BL │background │I  │forground

                      BL = blink bit
                      I  = intensity (highlight or bold)

                      Background  Foreground  Display
                      000         000         no display
                      000         001         underline
                      000         111         normal video
                      111         000         reverse video


Figure 5:  Attribute byte for graphics adapter in text mode is divided into
           two four-bit fields that control the foreground and background
           colors. The color assignments show are immutable for the original
           Color/Graphics Adapter and are the defaults for the Enhanced
           Graphics Adapter (other color assignments can be obtained by
           programming the EGA palette with VIOSETSTATE).

                          7   6   5   4 │ 3   2   1   0
                         background     │foreground

          Color Assignments
                  0     black
                  1     blue
                  2     green
                  3     cyan
                  4     red
                  5     magenta
                  6     brown
                  7     white
                  8     dark gray
                  9     light gray
                 10     light green
                 11     light cyan
                 12     light red
                 13     light magenta
                 14     yellow
                 15     intensified white


Figure 6:  Clearing the screen to ASCII blanks with a normal video attribute,
           using one of the four VIO scroll functions. This special case call
           with an upper-left window coordinate of (0,0), lower-right
           coordinate of (-1,-1), and -1 as the number of lines to scroll
           does not require that the programmer first determine the
           dimensions of the active display.

Microsoft Macro Assembler:
                                    ∙
                                    ∙
                                    ∙
          push 0              ; Y upper-left
          push 0              ; X upper-left
          push -1             ; Y lower-right
          push -1             ; X lower-right
          push -1             ; number of blank lines
          push ds             ; address of character/attrib
          push offset DGROUP:init_cell
          push 0              ; VIO handle (reserved)
          call VIOSCROLLUP    ; transfer to OS/2
          or   ax,ax          ; did scroll call succeed?
          jnz  error          ; jump if call failed
                                    ∙
                                    ∙
                                    ∙
init_cell db   20h,07h        ; ASCII blank, normal video

Microsoft C:

extern unsigned far pascal VIOSCROLLUP(unsigned, unsigned, unsigned,
     unsigned, unsigned, char far *, unsigned);
                                    ∙
                                    ∙
                                    ∙
     int status;
     static char init_cell[2];     /* initialization parameter */
     init_cell[0]=0x20;            /* character = ASCII blank */
     init_cell[1]=0x07;            /* attribute = normal video */
                                    ∙
                                    ∙
                                    ∙
     status=VIOSCROLLUP(0, 0, -1, -1, -1, init_cell, 0);
                                    ∙
                                    ∙
                                    ∙


Figure 7:  Scrolling selected portions of the screen in any direction can be
           accomplished very easily with the VIOSCROLLxx functions──a useful
           building block for windowing packages. For example, this code
           scrolls the bottom half of the screen up one line, filling the
           newly blanked line with ASCII blanks having a normal video
           attribute, and leaving the top half of the screen untouched.

Microsoft Macro Assembler:
                                    ∙
                                    ∙
                                    ∙
          push 12             ; Y upper-left
          push 0              ; X upper-left
          push 24             ; Y lower-right
          push 79             ; X lower-right
          push 1              ; lines to scroll
          push ds             ; address of character/attrib
          push offset DGROUP:init_cell  ; for new line
          push 0              ; VIO handle
          call VIOSCROLLUP    ; transfer to OS/2
          or   ax,ax          ; did scroll succeed?
          jnz  error          ; jump if error
                                    ∙
                                    ∙
                                    ∙
init_cell db   20h,07h        ; ASCII blank, normal video

Microsoft C:

extern unsigned far pascal VIOSCROLLUP(unsigned, unsigned, unsigned,
     unsigned, unsigned, char far *, unsigned);
                                    ∙
                                    ∙
                                    ∙
     int status;
     static char init_cell[2];     /* initialization parameter */
     init_cell[0]=0x20;            /* character = ASCII blank */
     init_cell[1]=0x07;            /* attribute = normal video */
                                    ∙
                                    ∙
                                    ∙
     status=VIOSCROLLUP(12, 0, 24, 79, 1, init_cell, 0);
                                    ∙
                                    ∙
                                    ∙


Figure 8:  Cursor positioning with the VIO calls. This code locates the
           cursor on the first column of the last line of the screen.

Microsoft Macro Assembler:
                                    ∙
                                    ∙
                                    ∙
          push 24             ; Y coordinate, row
          push 0              ; X coordinate, column
          push 0              ; VIO handle (reserved)
          call VIOSETCURPOS   ; transfer to OS/2
          or   ax,ax          ; did function succeed?
          jnz  error          ; jump if function failed
                                    ∙
                                    ∙
                                    ∙

Microsoft C:

extern unsigned far pascal VIOSETCURPOS(unsigned, unsigned, unsigned);
                                    ∙
                                    ∙
                                    ∙
     int status;
                                    ∙
                                    ∙
                                    ∙
     status=VIOSETCURPOS(24,0,0);
                                    ∙
                                    ∙
                                    ∙


Figure 9:  Reading the cursor position with VIOGETCURPOS. The cursor
           location is returned in text coordinates that assume a home
           position of (0,0).

Microsoft Macro Assembler:
                                    ∙
                                    ∙
                                    ∙
          push ds             ; address to receive row (Y)
          push offset DGROUP:CurY
          push ds             ; address to receive column (X)
          push offset DGROUP:CurX
          push 0              ; VIO handle
          call VIOGETCURPOS   ; transfer to OS/2
          or   ax,ax          ; was cursor position obtained?
          jnz  error          ; jump if function failed
                                    ∙
                                    ∙
                                    ∙
CurY      dw   ?
CurX      dw   ?

Microsoft C:

extern unsigned far pascal VIOGETCURPOS(unsigned far *,
    unsigned far *, unsigned);
                                    ∙
                                    ∙
                                    ∙
     int curx,cury,status;
                                    ∙
                                    ∙
                                    ∙
     status=VIOGETCURPOS(&cury,&curx,0);
                                    ∙
                                    ∙
                                    ∙


Figure 10:  Example of altering the cursor to a block and then back to its
            original shape.

Microsoft Macro Assembler:
                                    ∙
                                    ∙
                                    ∙
          push ds             ; address of structure to
          push offset DGROUP:CurData    ; receive cursor information
          push 0              ; VIO handle
          call VIOGETCURTYPE  ; transfer to OS/2
          or   ax,ax          ; did we get cursor info?
          jnz  error          ; jump if call failed

          mov  ax,CurStart    ; save cursor starting line
          mov  CurPrev,ax

          mov  CurStart,0     ; force starting line to
                              ; zero to get block cursor

          push ds             ; address of cursor data block
          push offset DGROUP:CurData
          push 0              ; VIO handle
          call VIOSETCURTYPE  ; transfer to OS/2
          or   ax,ax          ; did we change cursor?
          jnz  error          ; jump if call failed
                                    ∙
                                    ∙
                                    ∙
          mov  ax,CurPrev     ; restore original cursor shape
          mov  CurStart,ax

          push ds             ; address of cursor data block
          push offset DGROUP:CurData
          push 0              ; VIO handle
          call VIOSETCURTYPE  ; transfer to OS/2
          or   ax,ax          ; did we change cursor?
          jnz  error          ; jump if call failed
                                    ∙
                                    ∙
                                    ∙
CurData   label byte          ; cursor data structure
CurStart  dw   ?              ; starting scan line
CurEnd    dw   ?              ; ending scan line
CurWidth  dw   ?              ; width (0=default)
CurAttr   dw   ?              ; attribute (0=visible, -1=hidden)

CurPrev   dw   ?              ; previous starting line for cursor,
                              ; used to restore shape later

Microsoft C:

struct CursorData {
     unsigned cur_start;
     unsigned cur_end;
     unsigned cur_width;
     unsigned cur_attribute;
     };

extern unsigned far pascal VIOGETCURTYPE(struct CursorData far *, unsigned);
extern unsigned far pascal VIOSETCURTYPE(struct CursorData far *, unsigned);
                                    ∙
                                    ∙
                                    ∙
     struct CursorData CurData;
     int status,CurPrev;
                                    ∙
                                    ∙
                                    ∙
     status=VIOGETCURTYPE(&CurData,0);  /* get current cursor data */

     CurPrev=CurData.cur_start;         /* save cursor start line */
     CurData.cur_start=0;               /* set start line to 0 */

     status=VIOSETCURTYPE(&CurData,0);  /* force block cursor */
                                    ∙
                                    ∙
                                    ∙
     CurData.cur_start=CurPrev;         /* previous cursor start line */

     status=VIOSETCURTYPE(&CurData,0);  /* restore cursor size */
                                    ∙
                                    ∙
                                    ∙


Figure 11:  Using VIOWRTTTY to write the string "Hello World" to the screen
            at the current cursor position, then move to a new line
            (scrolling if necessary). The cursor position is updated
            appropriately after the write.

Microsoft Macro Assembler:
                                    ∙
                                    ∙
                                    ∙
          push ds             ; address of mode data structure
          push offset DGROUP:TextMode
          push 0              ; VIO handle (reserved)
          call VIOSETMODE     ; transfer to OS/2
          or   ax,ax          ; did mode set succeed?
          jnz  error          ; jump if mode set failed
                                    ∙
                                    ∙
                                    ∙
TextMode  dw   8              ; length of data passed
          db   1              ; non-MDA, text mode, color enabled
          db   4              ; 16 colors
          dw   80             ; 80 text columns
          dw   25             ; 25 text rows

Microsoft C:

struct ModeData {
     unsigned length;
     unsigned char type;
     unsigned char color;
     unsigned col;
     unsigned row;
     unsigned hres;
     unsigned vres;
     };

extern unsigned far pascal VIOSETMODE(struct ModeData far *, unsigned);
                                    ∙
                                    ∙
                                    ∙
     int status;
     struct ModeData TextMode;     /* init mode params */
                                    ∙
                                    ∙
                                    ∙
     TextMode.length=8;            /* amount of data passed */
     TextMode.type=1;              /* non-MDA, text mode, color */
     TextMode.color=4;             /* 16-color mode */
     TextMode.col=80;              /* 80 columns */
     TextMode.row=25;              /* 25 rows */

     status=VIOSETMODE(&TextMode,0);    /* force mode change */
                                    ∙
                                    ∙
                                    ∙


Figure 12:  Example of the use of VIOPOPUP and VIOENDPOPUP by a background
            process to interact with the user. Note that if a successful
            VIOPOPUP call is made, the program must be careful not to take
            any branch away from the main line of execution toward
            VIOENDPOPUP.

Microsoft Macro Assembler:
                                    ∙
                                    ∙
                                    ∙
                              ; First put up pop-up Window...
          push ds             ; address of Wait flags
          push offset DGROUP:WaitFlag
          push 0              ; VIO handle
          call VIOPOPUP       ; transfer to OS/2
          or   ax,ax          ; did we capture display?
          jnz  error          ; no, jump to error handler

                              ; now display pop-up message...
          push ds             ; address of message
          push offset DGROUP:msg
          push msg_len        ; length of message
          push 12             ; Y
          push (80-msg_len)/2 ; X (center it)
          push 0              ; VIO handle
          call VIOWRTCHARSTR  ; transfer to OS/2

          push 0              ; pause for 5 seconds
          push 5000
          call DOSSLEEP       ; transfer to OS/2

                              ; Take down pop-up window...
          push 0              ; VIO handle
          call VIOENDPOPUP    ; (should never fail)
                                    ∙
                                    ∙
                                    ∙
msg       db   'Hello World'
msg_len   equ  $-msg

WaitFlag  dw   1              ; bit 0=1 if Wait until pop-up
                              ; is available, =0 if no wait

Microsoft C:

extern unsigned far pascal DOSSLEEP(unsigned long);
extern unsigned far pascal VIOWRTCHARSTR(char far *, unsigned,
     unsigned, unsigned, unsigned);
extern unsigned far pascal VIOPOPUP(unsigned far *, unsigned);
extern unsigned far pascal VIOENDPOPUP(unsigned);
                                    ∙
                                    ∙
                                    ∙
     static char msg[]="Hello World";
                                    ∙
     status=VIOPOPUP(&WaitFlag, 0);     /* pop-up screen */

                                        /* display msg at center screen */
     status=VIOWRTCHARSTR(msg,sizeof(msg)-1,12,(80-sizeof(msg))/2,0);

     status=DOSSLEEP(5000L);            /* pause for 5 seconds */

     status=VIOENDPOPUP(0);             /* release pop-up screen */
                                    ∙
                                    ∙
                                    ∙

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

Dynamic Allocation Techniques for Memory Management in C Programs

Steve Schustack☼

Most end-user PCs have between 64Kb and 640Kb of memory, and it is safe to
assume that many of these machines will, at some point, have more RAM
added on. Though we know what the lower and upper bounds of installed
memory are, we can never be certain about how much memory any given
machine might have.

Programs can be written to run in specific, fixed-size environments, or
they can be designed to dynamically adjust to any given amount of RAM.
Programs that support such flexible memory management capability certainly
offer an extra measure of utility. I began to consider this when my
favorite word processor did not recognize the 128Kb of additional memory
I had just installed in my own machine; instead it completely ignored the
additional RAM and merrily continued to access the PC's disk.

Had the programmers who wrote my word processing program utilized dynamic
memory allocation techniques, the program would have been able to use the
additional memory I had installed to hold more of my text, and perhaps to
hold more of the application as well.

Dynamic allocation allows programs to adapt to their environments, taking
advantage of any memory available on an as-needed basis. Such programs
dynamically allocate memory to hold array and linked list data rather than
using fixed array dimensions, which are set by the programmer in array
declarations. The only limitation on the array sizes becomes the amount of
memory available on the user's system.

This article will focus on using C's standard library functions to write C
programs that employ data structures that grow and shrink dynamically
during program execution. We will implement these functions through
variable-sized arrays and linked lists, which are applications of dynamic
memory allocation.

Standard library functions such as malloc and free are among the
programmer's tools explored here. The function malloc, for example,
accepts as its argument the number of bytes of memory needing to be
dynamically allocated, and returns a pointer to a block of free memory at
least that large, provided enough memory is available. A call to the
function free returns dynamically allocated memory to the pool of memory
available to a program.


The Problem

Like most programming languages, C requires that when you declare an array
you must specify its size. This often becomes a limitation of the program,
however, because it sets an upper bound on the amount of data the program
can process. Why not simply make the arrays so large that they are sure
to satisfy even the largest uses?

Regrettably, memory is not limitless. The more memory a program needs, the
larger the system required to run it. As terminate-and-stay-resident (TSR)
programs become more popular and operating system memory requirements
grow, it makes a great deal of sense for programs to conserve memory by
using only what is needed. A PC's memory has, in fact, become a shared
resource. What are a programmer's options for reserving memory to hold a
program's data?


Memory Organization

A program's memory is organized into areas known as heap, stack, data, and
code. The heap is at the top of a program's memory space and is the source
of dynamically allocated memory. The size of the heap depends on the sizes
of the other areas, because the heap consists of all available memory not
assigned to one of the other areas.

The stack is just below the heap in memory; its size is set by LINK,
either to 2048 bytes by default, or to the size given to LINK with the
/STACK option. The data area contains static and external variable data.
Executable machine instructions fill the code area.


Allocation of Memory

When a program obtains memory for its exclusive use, the memory is said to
be allocated to that program. C programs have two options for memory
allocation. The most common way memory is allocated is by simple
declaration of variables. The alternative is to dynamically allocate
memory by calling certain standard library functions, such as malloc,
calloc, and realloc.

Memory for static and external variables is allocated and initialized only
once, prior to execution of the program, at the time the program is loaded
from disk into RAM. It remains allocated until the program terminates its
execution.

All other variables, namely those of auto or register storage class, are
allocated each time the function (or block) in which they are declared is
entered. The memory for auto and register variables is released
(deallocated) when the flow of control leaves the declaring function (or
block).

Because memory for statics and externals is allocated throughout
execution, limiting the use of static and external variables tends to
conserve RAM. Auto and register variables, on the other hand, need only be
allocated when a given function is active (on the calling stack). A
function is active when it is called by main or any other function.
Lifetime, scope (visibility), and initialization are other factors to
consider when selecting storage classes for your variables.

In order to dynamically allocate memory, the amount of memory needed must
first be known. The C keyword operator sizeof is used to find the number
of bytes allocated to any variable that has been declared. The sizeof
operator may also be used on data types that are enclosed in parentheses.
This means that a program can expect the value of the expression "sizeof
(int)" to be two or four, depending on whether it was compiled and whether
it is executing on a 16-bit or on a 32-bit CPU (see Figure 1).


Variable-length Arrays

Dynamic allocation of a block of memory takes place during execution. The
size of the dynamic block is specified using arguments to standard library
functions such as malloc, calloc, and realloc. This is in contrast to the
dimension of an array, which is fixed by a constant or by a constant
expression in a declaration.

The elementary program SUM_NUM, listed in Figure 2, introduces the
standard library function calloc. Within SUM_NUM, calloc returns the
address of a block of memory whose size is specified by passing the number
of cells (elements) to be allocated, along with the size in bytes of each
cell. Simply declare a pointer to the type of data to be stored, and
assign the return value from calloc to that pointer variable, as follows:

  long *nums;
          ∙
          ∙
          ∙
  nums = (long *) calloc(how_many,sizeof(long));

These two statements create a dynamically allocated array of "how_many"
long integers, and cause the pointer "nums" to point to that array.

The "(long *)" before calloc, which is a cast operator indicating "pointer
to type long," is optional and specifies a conversion that will avoid
compiler warnings such as "different levels of indirection." If a function
is not declared prior to being called, or if it is not explicitly declared
as being of a particular type (like "long"), the compiler assumes that
"int" is the type of value returned by that function. Assigning "nums" a
value from calloc without the "(long *)" cast would therefore result in
trying to assign an int value to a variable declared as long, resulting in
a mismatch of levels of indirection and causing a compiler warning
message.

If the amount of memory requested is not available, then calloc returns
the value of NULL, which happens to be defined in stdio.h as zero. If you
don't test for this special null pointer value and store data at address
0, you will get a "null pointer assignment" error when your program
terminates. Clobbering data in memory not dynamically allocated and not
allocated to any variable, such as data at address 0, can easily go
unnoticed and create hard-to-find errors, hard-to-find because the program
may only misbehave while handling some rare and unusual case not tested
during program development.


Pointers and Arrays

An understanding of C's use of pointers and arrays is essential,
especially in relation to memory management schemes. The pointer and array
data types are very closely related──perhaps more than you realized.
Whenever you refer to an array in C by using that array in an expression,
such as passing it to a function or getting the value of one of its
elements, the value of the name of the array is taken to be the starting
address of that array. So if you have a pointer variable that has been
made to point to an array of the same data type, you can use that pointer
in expressions as if it were the array (see Figure 3).

The fact that pointers behave very much like arrays means that you can
operate on elements of the array of data pointed to by the variable nums
in SUM_NUM.C (in Figure 2), as if nums were an ordinary array of long
integers. The address of an element pointed to by nums in the expression
"&nums[inum]" is passed to the function scanf. The value of the element is
added to sum in the statement "sum += nums[inum];".

Even if you are a newcomer to C's pointer operations, you have been
applying this concept every time you've written a function that operated
on elements of an array that was passed to it. The parameters of a
function that receive data from an array are declared using empty
brackets, as in the expression get_param[ ]. What C actually passes to the
function is the array's starting address. The function treats the
parameter as an array, even though the parameter is in fact a pointer. One
way to tell the array itself apart from a pointer to it is by using the
sizeof operator. In Figure 3, the sizeof operator returns different values
for p_longs and long_array, as indicated.

A consequence of this type of argument-passing is that a called function
cannot directly tell the dimension of an array that was passed to it as an
argument. This is because the array itself is not available to it, so its
size cannot be tested. Instead, the value passed to the function is a
pointer to the array's first element, and as a pointer variable, it is
either 2 or 4 bytes in size (an int or a long in a 16-bit machine).


Free Memory

When a program terminates, all of the memory allocated to it is released.
This includes memory allocated dynamically by calling C library functions,
like calloc. When allocated memory is released, it is deallocated and
becomes available for future allocations. If your program requires
repeated dynamic allocations, then you can call the function free to avoid
running out of memory. Call free to release blocks of memory when they are
no longer needed. The free function expects as its argument the same
address that was returned by a previous call to calloc, malloc, or
realloc, for example:

  nums = (long *) calloc(how_many,sizeof(long));
  free(nums); /* deallocates memory nums points to */

Never pass an address to free that was not first given to you by calloc,
malloc, or realloc; otherwise things will get nasty. Each block of
dynamically allocated memory is preceded by internal system maintenance
information, which includes the size of that block, as well as a pointer
to the next block. If that maintenance information has been clobbered by
storing data off the end of another block or the beginning of this block,
or the information is simply not there, then the pool of available memory
will become "polluted" by that call to free. Library dynamic allocation
functions will erroneously determine that free memory is already
allocated, or even worse, that already allocated memory is free to be
reallocated.

To illustrate the concept of dynamically allocating data we will look at a
particular application, namely sorting a directory. Directory information
is an unknown and unpredictable quantity. A directory sorting program
would therefore have to do one of two things: either some specific size
would have to be predetermined, or the sorting routine could be designed
to dynamically allocate whatever memory it might need. Though the program
may seem simple, it provides an excellent example of dynamic memory
allocation and lends itself well to the memory management technique called
linked list processing.


Problem Definition

The number of files in a directory can vary and change in unpredictable
ways. Directories can vary in size from as little as two files ("." and
".."──an empty directory) to whatever upper bound the most current release
of MS-DOS(R) allows. Because this upper bound may change with the next
release of the operating system, a utility to sort an MS-DOS directory
should be able to deal with this change in a manner that will not
necessitate rewriting the program.

The DIR command outputs information about files in a directory.
Unfortunately, the order in which files are listed is not very useful. A
solution to both problems will be discussed.

The MS-DOS filter program SORT can be used to reorder the listing of
files, to be sorted by file name and extension, for instance. Just tell
SORT what column to begin sorting on, and it does the job. Using a pipe
with the SORT and DIR command provides a somewhat useful but rather
limited directory listing and sorting capability.

Furthermore, a number of things are poorly handled by SORT. For example, a
file modified in the early afternoon will be listed ahead of a file
modified late in the morning of the same day, because MS-DOS uses "a" and
"p" to represent A.M. and P.M., rather than a 24-hour clock. Combining DIR
and SORT can be useful, but something better is needed.


Linked Lists

Linked lists are a very powerful and flexible way of using dynamically
allocated memory. They are suited to applications in which data of
different types (integer, character, and floating point) can be combined
in chainlike structures of unpredictable lengths. Each element of a linked
list is like a record in a memory-resident file of records. A record holds
keys for other records, which it points to. These records link together to
form structures ranging from a simple single chain to exotic networks of
chains.

Linked lists come in a variety of sizes and shapes, with some common
characteristics and terminology. They can become quite complex in
structure. We will be looking at the simplest type, the singly linked
list, which consists of a series of elements, called nodes, that are
joined to form a chain. It is only possible to move along this type of
list in one direction, from the starting node of the list, its head, to
the end node, its tail.

The ease of inserting a new link between two links in a list makes linked
lists great for storing data in a variety of different orderings. If each
new link added to a list is inserted at the head of the list, then the
list behaves like a push-down stack (also known as a last-in-first-out
(LIFO) queue). If new links are stored only at the tail of the list, a
first-in-first-out (FIFO) queue is the result. Any ordering is possible if
new links can be inserted anywhere in the list. Sorting text data
alphabetically is easily accomplished.

The data in Figure 4 was output by the DIR command and will be used to
construct two kinds of linked lists: first a push-down stack, then a
sorted list. The evolution of a push-down stack from an empty list is
demonstrated in Figures 5, 6, 7, 8, 9, and 10. The list itself is
displayed in Figure 11. A memory diagram showing the state of the
list after each code segment is executed follows each segment in
Figures 5, 6, 7, 8, 9, and 10.

The link structure tag, s_dir_node, is declared in Figure 5, along with
p_head and p_new, which are both declared to be pointers to that type of
structure. The member "next", the first member of an s_dir_node structure,
is also a pointer to an s_dir_node structure. This type of structure is
called self-referencing because it contains a pointer to its own data
type. The member "next" is the means for joining structure links to form a
linked list. The pointer to the head of the list, p_head, is assigned the
null pointer NULL (from stdio.h) to create an empty list.

Enough memory to hold a single s_dir_node structure is dynamically
allocated by calling the standard library function malloc in Figure 6. The
argument passed to malloc is the number of bytes required to be allocated.
The pointer returned by malloc is the address of the newly allocated
block, but it is type pointer to char, so it must be converted to type
pointer to an s_dir_node structure using the "(struct s_dir_node *)" cast
operator. The type-converted address is assigned to p_new as the final
action of this first statement. The memory diagram indicates that the new
link is at address 3800.

The remaining two statements in Figure 6 assign values to the members of
the allocated link by using the arrow "->" operator. The expression
"p_new->dir_line" refers to the member dir_line in the s_dir_node
structure pointed to by p_new. NULL is assigned to "p_new->next" to make
it the tail of the list. This link is also the head of the list.

The variable p_head is made to point at the same link that p_new points to
via the assignment in Figure 7. In Figure 8, another new link is allocated
at address 3900. The combined actions in Figures 9 and 10 place this
second new link at the head of the list, in front of the previous head, in
push-down stack fashion.

The "for" loop in Figure 11 lists each link in the list, starting at the
head and moving, one link per iteration, to the tail. One interesting
aspect of the loop is that, rather than counting up or down, it moves
through a chain of pointers. The expression "p_new = p_new->next"
accomplishes that movement by assigning to p_new the value of the member
"next" in the link p_new points to.

Notice that in the format string passed to printf, inside the loop, the %u
(unsigned integer) format specifier is used to display addresses, since
addresses are never negative.

Now that you've had a taste of building linked lists and operating on
them, take a moment to examine the comparison between linked lists and
arrays in Figure 12. Linked lists facilitate handling unpredictable
amounts of data. It is easy to change the order of the data, by inserting
a new link or chain of links between two existing links on the list; it is
just as simple to remove a single link, or chain of links.

Accessing the different elements of an ordinary array is quite fast and
provides random direct access. The elements in a linked list must be
accessed in order, from the head, moving link-by-link, to the tail.
Different paths through (or orderings of) a single list can be achieved by
adding additional next-link pointers to the link structure, however, which
speeds up the process somewhat. The term "doubly linked list" refers to a
linked list that contains a pointer to the previous link as well as a
pointer to the next one. The overhead of storing link pointers in each
link is another price paid for linked lists, but not for arrays. Keeping
those facts in mind, let's proceed to a more real-world application of
linked lists.


BY_TIME Program

The BY_TIME program may be used to sort DIR command output for all files
in a directory, or only those selected by a wildcard file specification.
The file information is placed in order with the most recently created or
modified first. The sort is accomplished by inserting a link for each
file's information, read from standard input, into its correct position in
the list so that the list is always in order. A DOS command to invoke
BY_TIME to list the *.EXE files in date and time sequence appears in
Figure 13, along with some sample output.

The organization and structure of the program BY_TIME demonstrates a
coding style that is appropriate for developing larger applications. The
main function hides the details of the data manipulations that take place
in the lower-level functions, but it does give a clear overall picture of
the processing to be performed. Each function is short and concise, doing
a single job. No external data is used; instead the paths through which
the data flows are made more clear by being passed as arguments.

The BY_TIME program is built by separately compiling each of the C source
files and then linking the resulting object files (see Figures 14, 15,
16, 17, 18, 19, and 20).

Each node in BY_TIME's linked list is a familiar structure with two
members, a pointer to another node and a string of information, output by
DIR, about each file. The structure of the nodes is defined in the header
file, BY_TIME.H in Figure 14, so that it can be #include'd in each source
file that needs it.

The first action taken by the main function in Figure 15 is a call to the
function bgn_list, passing the address of a dummy head node that was
declared as an s_dir_node structure. The function bgn_list in Figure 16
assigns dummy data (not for a file from DIR) into that node, with a date
value that is high enough to ensure that that node will always be the
head, given the date and time ordering requirements for this list. This
special head helps keep the code clean, because it prevents the head node
from being treated as a special case when inserting new links. The head of
the list is not dynamically allocated──it is an ordinary auto structure
type variable.

The initialized list is now ready to grow with the user's file
information. The "while" loop in main alternates between calls to get_dir
and sav_dir, until get_dir returns zero. Each call to get_dir (listed in
Figure 17) places one line of file information in the buffer that was
passed to it. It returns 1 if successful; otherwise it returns 0,
indicating that the end of input has been reached.

Once DIR information for the first file has been input by the function
get_dir, the first block of dynamically allocated memory is requested and
put to use in sav_dir, as shown in Figure 18. The function sav_dir is
responsible for inserting new links in such a way that the list of files
is always in most-recently-created-or-modified-first sequence.

The insertion process begins with a loop that searches the list for the
correct insertion point. The function time_b4 in Figure 19 is called in
the test expression of the sav_dir "for" loop. The time_b4 function
compares the date and time in the input buffer with the date and time in a
link in the list and returns 1 if the buffer time is earlier; otherwise it
returns 0.

Memory for the new node is dynamically allocated by a call to malloc in
sav_dir. Notice that a test for NULL is made to verify that enough memory
is available for the new node. The last two statements in sav_dir break
the connection between the two appropriate links and splice in the new
link, thereby creating a list that is one node longer, with all nodes in
the correct order.

Each DIR output line is displayed by the function sho_list in Figure 20,
as it moves from the head to the tail of the list. This takes place after
the end of input has been reached and the list is built. The code in
sho_list loops through the list, displaying the data portion of each link,
just as the loop in Figure 11 did.


Beware of Dangling Pointers

As with the free function previously mentioned, care must be taken when
dealing with memory management C code, especially when referencing memory
through pointers. The program DANGLE.C in Figure 21, for example, is
guilty of memory mismanagement, because it refers to values in unallocated
memory. The results don't make sense at first glance, until you realize
that a nasty bug is at work. The output consists of three different lines
of garbage. Try it and see for yourself.

The memory pointed to by p_text was not allocated in the three cases where
its address was passed to puts and printf for output. It was allocated
while str_dang was executing, but after str_dang returned the address of
its variable auto_string, the memory for auto_string became deallocated.
Automatic variables within a function become deallocated when that
function returns to its caller. Next, the auto_string memory became
reallocated for a new use, so the string pointed to by p_text changed, as
if by magic. The new use was for printf, or one of its subfunctions.

Memory for all of a program's static and external variables remains
allocated for the entire time the program is executing. If the declaration
of auto_string were preceded by the word "static," then instead of three
different lines of junk, you would see three identical lines of 123456789.


Summary and Beyond

Programs that process unpredictable amounts of data residing in memory can
benefit from dynamically allocated arrays. These arrays may be made up of
simple single-element data, or they may be arrays of complex structures.
Once allocated, these dynamic arrays may be operated on by using the
square brackets "[ ]" operator to access individual elements such as
ordinary arrays.

If changes in the ordering of the data are part of the data's processing,
then linked lists should be considered to hold the data. The trade-offs in
this decision involve how the data will be accessed. Will random accesses
be frequent, or will the list only be processed in sequence? Will the
overhead of node pointers for each link be acceptable? Are the people who
will maintain this code fluent C programmers who understand pointers,
dynamic allocation, and linked list theory?

Only the singly linked list was examined here in detail. The doubly linked
list, which can be traversed forward or backward, received a mention. A
tree is another, more complex, type of linked list. The head of a tree may
have two or more links it points to, which are sometimes called children,
as in a family tree. Each of the child nodes may also have one or more
children. Nodes without children are called leaves. No loops within a tree
are permitted, just as a person cannot be his own parent or grandparent.
Applications of trees include sorting, parsing expressions in compilers
and translators, and processing directories of files and their
subdirectories.

For more information about managing memory, dynamic allocation, and linked
lists, I encourage you to read the chapter "Efficient Use of Memory" in my
book Variations in C (Microsoft Press: 1985).

How well memory is managed can make a real difference in the value of a
program to those who use it. May your pointers not dangle and your lists
stay linked.


Figure 1:  Using sizeof to Determine Memory Needs

main()
        {
        short counter;          /* 2 bytes of RAM allocated.   */
        static char name_list[1000][25] = '\0'; /* 25000 bytes */
        int status_vector[10];  /* 20 bytes on 16-bit CPU's,   */
                                /* 40 bytes on 32-bit CPU's.   */

        printf("sizeof counter %d, name_list %d, status_vector %d\n",
                sizeof counter, sizeof name_list,
                sizeof status_vector);

        printf("int's are %d bytes, I'm a %d-bit CPU\n",
                sizeof (int), 8 * sizeof (int));
        }

This output will result from the program above:

     sizeof counter 2, name_list 25000, status_vector 20
     int's are 2 bytes, I'm a 16-bit CPU


Figure 2:  SUM_NUM.C

/****************************************************************
 * SUM_NUM.C: Input and total a variable number of integers.
 *   Show list of numbers and cumulative subtotals in output.
 */
main()
        {
        long *nums;      /* Pointer to array of numbers to sum */
        short how_many;  /* Number of numbers to input and sum */
        short inum;      /* Counter to index into nums array   */
        long sum;        /* Sum of numbers in nums array       */

        printf("How many numbers to sum? ");
        if (1 != scanf("%hd", &how_many) || how_many < 2)
                exit(1);         /* Terminate sum_num program  */

        /* Dynamically allocate memory for how_many long's     */
        nums = (long *) calloc(how_many, sizeof (long));

        for (inum = 0; inum < how_many; ++inum) /* Input nums  */
                {
                printf("Enter #%d: ", inum + 1);
                if (1 != scanf("%ld", &nums[inum]))
                        how_many = inum;        /* Quit early  */
                }
        for (sum = inum = 0; inum < how_many; ++inum)
                {
                sum += nums[inum];      /* Add number to sum   */
                printf("%3d:  %10ld %10ld\n", /* Show results  */
                        inum + 1, nums[inum], sum);
                }
        printf("\nThe sum of the %d numbers entered is %ld\n",
                how_many, sum);
        }


Figure 3:  Use of Pointers with Arrays

main()
        {
        long long_array[100];   /* Ordinary array of long's    */
        long *p_longs;          /* Pointer to type long        */

        p_longs  = &long_array[0]; /* point at long_array      */
        p_longs[8] = 32;
        printf("%d equals %d\n", p_longs[8], long_array[8]);
        printf("sizeof p_longs %d, sizeof long_array %d\n",
                sizeof p_longs, sizeof long_array);
        }

Which outputs:

     32 equals 32
     sizeof p_longs 2, sizeof long_array 400


Figure 4:  Sample Data from Original DIR Command

     MEMO     TXT       46  11-18-86   6:26p
     DIROUT   TXT     1206   1-17-87   7:25p


Figure 5:  Declaring the Link Structure Tag

#include <stdio.h>                    /* For #define of NULL pointer */
#define DIR_LINE_LEN 39               /* DIR output line length      */
struct s_dir_node                     /* Linked list node structure  */
        {
        struct s_dir_node *next;      /* Next node pointer */
                                      /*   or NULL (0)     */
        char dir_line[DIR_LINE_LEN + 1];  /* DIR output line   */
        }

struct s_dir_node *p_head;            /* Pointer to head of list */
struct s_dir_node *p_new;             /* Pointer to new link     */
p_head = NULL;                        /* List is empty now       */

            ┌──────┐
  p_head    │ 0000 │
            └──────┘
            ┌──────┐
  p_new     │ ???? │
            └──────┘


Figure 6:  Allocating for a New Link

/* Dynamically allocate a new link and assign data to it  */

p_new = (struct s_dir_node *) malloc (sizeof (struct s_dir_node));
strcpy (p_new->dir)line,
          "DIROUT    TXT    1206    1-17-87    7:25p");
p_new->next = NULL;    /* No next node - this is the tail */

            ┌──────┐
  p_head    │ 0000 │
            └──────┘
            ┌──────┐
  p_new     │ 3800 │
            └───┬──┘  ┌──────┐
                └─────┤ 0000 │  DIROUT    TXT    1206    1-17-87    7:25p
                      └──────┘


Figure 7:  First Link

p_head = p_new;     /* Empty list grows to a single link  */

            ┌──────┐
  p_head    │ 3800 ├─────┐
            └──────┘     │
            ┌──────┐     │
  p_new     │ 3800 │     │
            └───┬──┘  ┌──┴───┐
                └─────┤ 0000 │  DIROUT    TXT    1206    1-17-87    7:25p
                      └──────┘


Figure 8:  Allocating for Additional Links

/* Allocate a second link and assign data to it      */

p_new = (struct s_dir_node *) malloc(sizeof (struct s_dir_node));
strcpy(p_new->dir_line,
        "MEMO      TXT      46   11-18-86    6:26p");
p_new->next = NULL;     /* No next node yet          */

            ┌──────┐
  p_head    │ 0000 │
            └───┬──┘  ┌──────┐
                └─────┤ 0000 │  DIROUT    TXT    1206    1-17-87    7:25p
                      └──────┘
            ┌──────┐
  p_new     │ 3900 │
            └───┬──┘  ┌──────┐
                └─────┤ 0000 │  MEMO      TXT      46   11-18-86    6:26p
                      └──────┘


Figure 9:  Second Link

p_new->next = p_head;   /* New link points to list's head link   */

            ┌──────┐
  p_head    │ 0000 │
            └───┬──┘  ┌──────┐
                └─────┤ 0000 │  DIROUT    TXT    1206    1-17-87    7:25p
                      └───┬──┘
            ┌──────┐      │
  p_new     │ 3900 │      │
            └───┬──┘  ┌───┴──┐
                └─────┤ 3800 │  MEMO      TXT      46   11-18-86    6:26p
                      └──────┘


Figure 10:  Completed Links

p_head = p_new;    /* List grows again, now 2 links long   */

            ┌──────┐
  p_head    │ 0000 ├─────┐
            └──────┘  ┌──┴───┐
                      │ 3800 │  MEMO      TXT      46   11-18-86    6:26p
                      └──┬───┘
            ┌──────┐     │
  p_new     │ 3900 │     │
            └───┬──┘     │
                │     ┌──┴───┐
                └─────┤ 0000 │  DIROUT    TXT    1206    1-17-87    7:25p
                      └──────┘


Figure 11:  Output Result from the Linked List

/* Now that the linked list is built, display it in a for loop */
for (p_new = p_head; p_new != NULL; p_new = p_new->next)
        printf("At addr: %4u, next: %4u, dir_line: %-.12s\n",
                p_new, p_new->next, p_new->dir_line);

*  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *
Output:

At addr: 3900, next: 3800, dir_line: MEMO     TXT
At addr: 3800, next:    0, dir_line: DIROUT   TXT


Figure 12:  Linked Lists vs Arrays

Tradeoffs exist between storing data in linked lists and in arrays.

Linked lists                  Arrays

Variable length               Fixed dimension with
                              maximum size

Will use memory added         Ignores memory added
by user                       by user

Fast to insert or remove      Slow to insert or
a link                        remove an element

No random access, sequential  Random direct
access to lists only          accesses are fast

Memory overhead for node      No pointer memory
pointers                      overhead

Code to manipulate lists      Code to manipulate
is harder to read             arrays is easier to read


Figure 13:  Sample Output of BY_TIME Program

DIR *.EXE | BY_TIME

BY_TIME  EXE      5654    1-26-87    8:44p
VWC      EXE     16886    1-17-87    8:22p
GREP     EXE      9336    4-06-87    6:43p
PR       EXE     11604    3-16-86   11:07p
CFIND    EXE      8146    6-20-85    5:30p
CHARCNT  EXE      8146    4-07-87    9:53p
LISTING  EXE      6266    4-07-85    9:27p
VISIBLE  EXE      6402    4-07-85    9:24p


Figure 14:  BY_TIME.H

/****************************************************************
 * BY_TIME.H:  Header file for BY_TIME program, define linked
 *   list structure as next node pointer and line of DIR text
 */
#define DIR_LINE_LEN 39         /* DIR output line length      */
struct s_dir_node               /* Linked list node structure  */
        {
        struct s_dir_node *next;         /* Next node pointer  */
                                         /*   or NULL (0)      */
        char dir_line[DIR_LINE_LEN + 1]; /* DIR output line    */
        };


Figure 15:  BY_TIME.C

/****************************************************************
 * BY_TIME.C: Filter sorts DIR output in linked list, most recent
 * first. This is the main function of the program.
 */

#include <stdio.h>                      /* For BUFSIZ symbol   */
#include "by_time.h"                    /* Linked list struct  */
main()
        {
        struct s_dir_node head;         /* First node in list  */
        char in_buf[BUFSIZ];            /* Input buffer for    */
                                        /*   DIR output        */
        bgn_list(&head);                /* Initialize list     */
        while (get_dir(in_buf))         /* Input DIR info for  */
                                        /*   next file         */
                sav_dir(in_buf, &head); /* Save file info in   */
                                        /*  sorted linked list */
        sho_list(&head);                /* Show sorted list    */
        }


  Figure 16 BGN_LIST.C

/****************************************************************
 * BGN_LIST.C: Initialize head of list to dummy highest value
 */
#include <stdio.h>              /* For NULL (zero) pointer     */
#include "by_time.h"            /* For linked list struct      */

bgn_list(p_head)
struct s_dir_node *p_head;      /* Pointer to head of list     */
        {
        /* Date in head is greater than DIR date for any file  */
        strcpy(p_head->dir_line,
                "ZZZZZZZZ ZZZ    99999  99-99-99  99:99p");
        p_head->next = NULL;    /* No next node - empty list   */
        }


Figure 17:  GET_DIR.C

/****************************************************************
 * GET_DIR.C:  Input DIR info for next file from stdin
 */
#include <stdio.h>              /* For NULL (zero) pointer     */

int get_dir(buf)
char *buf;              /* Buffer for read and pass line back  */
        {
        char *rtn;                      /* save gets() return  */

        /* Loop:  Input lines until no more input or got line  */
        /*   starting with an uppercase letter (has file data) */
        while ((rtn = gets(buf)) &&     /* Input a DIR line    */
                (buf[0] < 'A' || buf[0] > 'Z'))   /* For file? */
                ;
        return (rtn != NULL);  /* Return 1 if got data, else 0 */
        }


Figure 18:  SAV_DIR.C

/****************************************************************
 * SAV_DIR.C: Allocate new node in list, save DIR info in it
 */
#include <stdio.h>              /* For NULL (zero) pointer     */
#include "by_time.h"            /* For linked list struct      */

sav_dir(buf, p_head)
char *buf;                      /* Line of DIR output to save  */
struct s_dir_node *p_head;      /* Pointer to head of list     */
        {
        struct s_dir_node *p_next;      /* Pointer to next     */
                                        /*   node in list      */
        struct s_dir_node *old_p_next;  /* Pointer to previous */
                /*   next node, parent of current next node    */
        struct s_dir_node *p_new;       /* Pointer to new node */

        /* Loop: for each node in list until end of list or    */
        /*   insert point that will keep list sorted is found  */
        for (p_next = p_head->next, old_p_next = p_head;
                p_next && time_b4(buf, p_next);
                old_p_next = p_next, p_next = p_next->next)
                ;
        /* Dynamically allocate memory for new node - DIR output
         *   line. Note use of the cast (struct s_dir_node *)
         *   operator in the assignment to avoid this message:
         *         warning 47: '=' : different levels of
         *                 indirection
         */
        p_new = (struct s_dir_node *)
                malloc(sizeof (struct s_dir_node));
        if (p_new == NULL)      /* malloc() failed, out of RAM */
                {
                puts("Out of memory!!!");
                return;
                }
        strcpy(p_new->dir_line, buf);   /* Save DIR line in    */
                                        /*  newly alloc'd node */
        p_new->next = old_p_next->next; /* New node points to  */
                                        /*   rest of list      */
        old_p_next->next = p_new;       /* Insert new node in  */
                                        /*   list              */
        }


Figure 19:  TIME_B4.C

/****************************************************************
 * TIME_B4.C: Return 1 if date and time in buf is before date
 *   and time in node p_next points to, otherwise return 0.
 */
#include "by_time.h"            /* For linked list struct      */
int time_b4(buf, p_next)
char *buf;                      /* Line of DIR output to find  */
                                /*   insert point in list for  */
struct s_dir_node *p_next;      /* Pointer to node in list to  */
        {                       /*   compare time in buf with  */
        int rtn;                /* Return value from strncmp() */

        /* compare year, month, day, am/pm, hour, and minute   */

        if (rtn = strncmp(&buf[29], &(p_next->dir_line)[29], 2))
                 return (rtn < 0);      /* Years differ        */
        if (rtn = strncmp(&buf[23], &(p_next->dir_line)[23], 2))
                 return (rtn < 0);      /* Months differ       */
        if (rtn = strncmp(&buf[26], &(p_next->dir_line)[26], 2))
                 return (rtn < 0);      /* Days differ         */
        if (buf[38] != (p_next->dir_line)[38])  /* am/pm's     */
                return (buf[38] == 'a');        /* differ      */
        if (rtn = strncmp(&buf[33], &(p_next->dir_line)[33], 2))
                 return (rtn < 0);      /* Hours differ        */
        if (rtn = strncmp(&buf[36], &(p_next->dir_line)[36], 2))
                 return (rtn < 0);      /*  Minutes differ     */
        return (0);             /* Dates and times are equal   */
        }


Figure 20:  SHO_LIST.C

/****************************************************************
 * SHO_LIST.C: Show sorted linked list - output it to stdout
 */
#include <stdio.h>              /* For NULL (zero) pointer     */
#include "by_time.h"            /* Linked list struct          */

sho_list(p_head)
struct s_dir_node *p_head;      /* Pointer to head of list     */
        {
        struct s_dir_node *p_next;      /* Pointer to next     */
                                        /*   node in list      */

        for (p_next = p_head->next;     /* Start at first node */
                p_next != NULL;         /* Still more list?    */
                p_next = p_next->next)  /* Move down a node    */

                puts(p_next->dir_line); /* Output a DIR line   */
        }


Figure 21:  DANGLE.C──Beware of Dangling Pointers

/****************************************************************
 * DANGLE.C: Danger of referencing a dangling pointer. That is a
 *      pointer to deallocated memory.
 */
main()
        {
        char *p_text;

        p_text = str_dang();
        puts(p_text);
        printf("%s\n", p_text);
        printf("%s\n", p_text);
        }

str_dang()
        {
        char auto_string[10];

        strcpy(auto_string, "123456789");
        return (&auto_string[0]);
        }

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

CD ROM Technology Opens the Doors on a New Software Market

Tony Rizzo

Along with the introduction of major new CD ROM applications from
Microsoft(R) and other software publishers, the confident glow of success
that usually follows a "good year" can be seen on the faces of all the
participants in the CD ROM industry these days. After a somewhat shaky
entry into the personal computing environment, a new stability has settled
on the CD ROM world that is finally fostering the growth of a new PC
software market──and expectations for dramatic growth are running high.

Two factors in particular contribute to this new stability. First, the
commercial success of compact audio discs give CD ROM a strong, low-cost
manufacturing capability, prompting considerable interest from potential
users of the medium. Second, the creation of a standardized logical disc
format and new systems software from Microsoft allows CD ROM drives to
easily interface with MS-DOS(R)-based personal computers so as to provide a
high level of software and hardware compatibility.

CD ROM applications extend the use of PCs from information processing to
include information accessing. It has proved to be a perfect medium for PC
applications requiring:

  ■ a substantial amount of data space

  ■ wide and inexpensive distribution of medium-volatile data, such as a
    parts catalog──it is considerably less expensive to press and ship new
    discs in quantity than it is to print new copies of reference works or
    update looseleaf references

  ■ random searching and accessing of large databases

  ■ distribution of large standardized reference works, such as dictionaries
    and specification catalogs, to be integrated into a PC user's
    environment

Additionally, state-of-the-art hardware now gives developers the ability
to integrate text with video and audio information, which will ultimately
give rise to multimedia PC applications.


The Audio Connection

CD ROM evolved from Sony and Philips's development of the audio compact
disc. Their new optical format offered the ability to store very large
quantities of digital information on a relatively small and damage-
resistant disc. More importantly, their combined work led to a definition
for the physical format of CD ROM that has become a de facto industry
standard.

As with compact audio discs, a major advantage of CD ROM is the fact that
once a master disc is made, any number of discs can be quickly and
inexpensively pressed from it. The success of compact audio discs led to
the creation of a growing number of pressing facilities, which, in turn,
has considerably reduced the cost of mass-producing both compact audio and
CD ROM discs.

The production process involves the following:

  ■ all data relevant to a given application is assembled

  ■ the data is indexed and structured for retrieval

  ■ a CD ROM image in High Sierra format is created from the data files──
    this step can be done by a service bureau or by using special systems
    such as CD Publisher

  ■ a High Sierra data image is then mixed with control information such as
    error correction code and recorded on a master tape (premastering)

  ■ the master tape is shipped to a pressing plant, where the master disc is
    then created (mastering)

  ■ the desired number of CD ROM discs is pressed from the master disc

After production of the master disc, which can cost from $3,000 to
$10,000, distribution discs can be pressed for as little as $3 per disc in
quantities of 100 or more. If one considers that a single CD ROM disc can
hold 660 Mb of data──more than 1500 floppy disks──it is a very cost
effective and efficient way to distribute information.

The CD ROM drive market also benefits from the success of compact audio
discs. Technological gains made in audio hardware lead directly to
improvements in CD ROM drive technology. More than ten manufacturers
currently make CD ROM drives. Prices, which now average less than $1,000,
will continue to drop as volume picks up.


The CD ROM Disc

The specification Sony and Philips created for CD ROM discs was included
in a document called "CD ROM," informally known as the Yellow Book. Though
not an officially recognized standard, its universal acceptance by hardware
developers has created a stable and well-defined physical environment.

The specification states that a CD ROM disc contains a stream of micron-
size pits that are read by a laser. The pits, laid out in a spiral, define
bit patterns that are grouped into bytes. Bytes are then grouped to form
sectors that are each 2352 bytes long: 304 bytes are dedicated to providing
system and hardware information (for example, error correction code), and
the remaining 2048 bytes contain user data (see Figure 1).


CD ROM Drives

Most magnetic disks store information on concentric tracks of information.
Each track is divided into an equal number of sectors, each of which holds
the same amount of information; that is, the largest outer sectors hold the
same amount of information as the smallest inner sectors. As sectors get
smaller, information is more densely packed.

Because the drives spin at a constant rate, because the number of sectors
is the same on each track, and because the outer tracks move at a faster
rate than the inner tracks, information on the largest sectors must be
spaced out far enough so that it takes the same amount of time to read
them as it does to read the smallest sectors (see Figure 2☼). This "spacing
out" results in the loss of a significant amount of possible data space,
but because the drive spins at a constant rate, and track and sector
location is specific and constant, the seek times of the read/write heads
can be very fast.

CD ROM works differently. There is only one continuous spiral track. Each
sector is the same physical size, allowing for substantially more data to
be stored──there is no wasted sector space (see Figure 3☼). To read data
stored in this format, however, the drive must spin at a variable rate.

As the head moves towards the inner layers of the spiral the rotational
speed increases, and as the head move towards the outer layer the
rotational speed decreases. This change in rotational speed is required to
maintain a constant linear velocity (CLV), which ensures that the speed
with which the outer and inner layers of the spiral travel, relative to
the read head, is the same. The drive first seeks to the approximate
location of a sector within the spiral, and then must read through the
sectors in order to find the exact sector it is looking for.

This mechanical characteristic of CD ROM results in relatively slow seek
times, generally ranging from .2 to 1 second (200 to 1000 ms). The benefit
of the design is a much denser and substantially greater data space (see
Figure 4). Good CD ROM layout and design means placing related data close
together──this ensures that a good deal of the time an application will
seek information that is nearby. Seeks that are close to each other can
have a minimum seek time of as little as 10 ms.


The Pioneers

Most of the pioneer CD ROM projects involved the distribution of text-
based information. Aside from building the applications themselves, early
CD ROM developers had to deal with a number of issues over which they did
not have much control.

The major problem was compatibility. Though a physical disc format
existed, there was no equivalent standard for logical disc formats. Worse,
there was no standard hardware or software PC interface for CD ROM drives,
and each project required the writing of system software to access the CD
ROM drive from a PC. This resulted in a number of proprietary logical disc
formats and interfacing techniques. Users found themselves in the position
of having to invest in expensive hardware that was incompatible with any
software not specifically designed for it. Developers had to face the
probability that proprietary hardware and software would not be widely
accepted.


The Road to Success

It became clear that, if CD ROM was to succeed, the industry would require
hardware and software compatibility, as well as the ability to easily
interface CD ROM drives to MS-DOS-based PCs.

An industry committee of dedicated CD ROM participants that became known
as the High Sierra Group (HSG) was formed to iron out the particulars of
creating a standard logical disc format. At the same time, Microsoft
undertook development of the MS-DOS CD ROM Extensions, software so as to
interface CD ROM drives to MS-DOS.


The HSG Proposal

The High Sierra Group work is based on the Sony/Philips physical format
specification. The HSG logical format provides two levels of CD ROM disc
definitions. The first level deals with the volume as a whole, and the
second deals with the actual files and directories that make up a volume's
information base. The full proposal also provides for several levels of
operating system support. The lowest level (level 1) supports MS-DOS and
similar operating systems, while the higher levels support operating
systems like XENIX(R) and VMS.

The HSG logical format limits itself to using only the 2048-byte user data
segment of the physical sector, which it calls a logical sector (see
Figure 5). The proposal anticipates larger sector sizes on future media.
The underlying hardware, device driver and system software will accommodate
such changes, maintaining compatibility with older formats while taking
advantage of newer formats.

The HSG format defines specifications for file identifiers, file
directories, and subdirectories. File layout is defined in terms of
logical blocks, extents, and optional XARs (eXtended Attribute Records).
Files containing actual information can be grouped within hierarchical
directories, just as they are in XENIX or MS-DOS. Directories, which are
themselves files, contain entries that provide the information necessary
for locating the entire contents of every file. Directories can contain
subdirectories nested to a maximum of eight levels.

The HSG specification takes into account the seek-performance limits of CD
ROM drives. For example, the proposal does not define an absolute location
for directories. This means directories can be placed close to the most
commonly used files, which would help keep seek time to a minimum, thereby
optimizing read/seek performance.

Another optimization in the HSG file structure is the Path Table. Because
of the design of CD ROM drives, finding and tracking subdirectories can be
a painfully slow process, especially when information is located deep
within a hierarchy──for example, within the sixth, seventh or eighth level
of subdirectory──or at distant parts of the spiral track itself.

The Path Table is an index into the hierarchical directories (see
Figure 6). It provides a way of quickly determining the starting sector of
any directory at any level in the hierarchy without having to traverse the
entire directory path, thus keeping the number of seeks necessary to look
up a directory or file to a minimum. The sectors that contain the Path
Table will be cached in memory by the MS-DOS CD ROM Extensions,
significantly improving seek performance.

From there, the system can search the directory to find actual files that
it needs. Because searching for directories should be very fast, while
searching for files can be very slow, the designer is cautioned against
using directories with lots of files in them. Otherwise, an application
might need to scan many sectors of a given directory to find a particular
file, resulting in seek time penalties. A single CD ROM directory sector
can hold about 40 directory entries.

The specification also defines Volume Descriptors, which provide
information for the entire contents of a CD ROM volume. Volume Descriptors
are the only "fixed-location" entities defined by the HSG specification,
from which any other information can be traced. Each CD ROM volume must
have at least one Volume Descriptor that provides directory and Path Table
information. Types of information that can be kept in the Volume
Descriptors include the standard ASCII character set (Standard File
Structure Volume Descriptor), author names, and creation dates.

A specification is also provided for handling multidisc applications,
where data exceeds the 660Mb capacity of one volume.

The HSG completed its work on May 28, 1986. Since that time, most CD ROM
vendors have embraced the group's set of specifications.


The High Sierra Groups Initial Members

  Apple Computer
  Digital Equipment Corp.
  Hitachi
  LaserData
  Microsoft Corp.
  Phillips
  Reference Technologies Inc.
  Sony Corp.
  3M
  TMS Inc.
  VideoTools
  XEBEC


The MS-DOS CD ROM Extensions

The MS-DOS CD ROM Extensions are designed to allow a user to purchase any
CD ROM drive from any manufacturer, to install the drive on any MS-DOS-
based personal computer, and to read any publisher's discs that use the
HSG format in that drive or in any other CD ROM drive.

Microsoft has implemented the Extensions through a terminate-and-stay-
resident (TSR) program known as MSCDEX.EXE, which is installed on top of
MS-DOS and handles the interfacing of CD ROM Device Drivers to MS-DOS
itself. The set of existing device driver commands was enhanced so that
they would support the unique characteristics of the CD ROM drives.

Microsoft has opted to distribute the MS-DOS CD ROM Extensions through CD
ROM drive manufacturers. Each drive manufacturer provides its own device
driver(s) and a copy of Microsoft's MSCDEX.EXE program with the CD ROM
drive itself, both supplied on a setup diskette. CD ROM application
developers need not supply any systems software with their applications.
This approach is analagous to the floppy and hard disk market. Software
developers can simply make the same assumptions about CD ROM drives that
they make about magnetic drives and disks.

Once the hardware is in place, the setup program is run by the user and
makes the necessary modifications to CONFIG.SYS (to install the device
driver) and AUTOEXEC.BAT (to include the call to MSCDEX.EXE). Once the
system is rebooted the PC will recognize the newly installed CD ROM drive,
and users and application programs can access the information on any CD
ROM disc adhering to the HSG specifications.

For a technical discussion of the MS-DOS CD ROM Extensions, see the
accompanying article MS-DOS CD ROM Extensions: A Standard PC Access Method.


Development Issues

A number of features unique to the CD ROM environment must be taken into
consideration during application design. The first and foremost is that CD
ROM is read-only. Most software assumes, for example, that the current
disk is always writable; this will create obvious problems if the current
drive is a CD ROM drive. And of course, index and search strategies, file
layouts and judicious use of directories must be considered.

One must also keep in mind that MS-DOS is, in High Sierra format terms, a
level 1 operating system──for example, filenames are limited to eight
characters plus a three-character extension. The MS-DOS CD ROM Extensions
will eventually support most features of the High Sierra format, such as
multivolume sets.

The HSG specification and the MS-DOS CD ROM Extensions have made the
development process substantially easier. Many of the obstacles faced by
the pioneer CD ROM developers and marketers have been cleared: The MS-DOS
CD ROM Extensions have eliminated most of the systems-level programming
that was previously required, and the HSG specification ensures logical
disc compatibility. The developer need only concentrate on the application
itself.


Applications

Typical CD ROM applications include parts catalogs, information databases
such as those offered by on-line database services, specialized applications
like medical diagnoses, and applications requiring the distribution of very
large texts, such as an encyclopedia. According to Carl Stork, Microsoft's
director of marketing for CD ROM, 150 vertical applications using CD ROM are
currently shipping, and the use of the High Sierra format is booming.

Microsoft itself has developed what can be called the first general-
purpose CD ROM product available to the PC user. Microsoft(R) Bookshelf(TM)
offers ten reference works on a single CD ROM disc. Bookshelf is designed
to work as either a standalone program or interactively with programs such
as word processors, allowing immediate on-line access to the ten reference
works while creating or editing a document.

Applications will soon include video and audio components. Digital Video
Interactive (DVI) is a new technology that is ideally suited for CD ROM; it
allows for full-motion video and graphics at the same data rates as those
of a CD ROM.

Optical media technology has finally hit its stride, and CD ROM is leading
the way through its vastly improved drive technology, the High Sierra format
specification, and the MS-DOS CD ROM Extensions.

Developers with existing applications that lend themselves to CD ROM
technology or with applications that would benefit from the CD ROM format
now have the entire PC market available to them. And those exciting new
applications that CD ROM is ideally suited for can now be brought to this
market, offering personal computer users new and added functionality, as
well as increased productivity.

The combination of desktop personal computers and CD ROM is proving to be a
winning hand. Disc compatibility has paved the way for mass market
acceptance of CD ROM, which is bound to encourage the creation of a new
software market and provide this new technology with a large degree of
success.


Figure 1:  The physical sector is 2352 bytes long; 2048 bytes can contain
           user data.

Type of Information           Allocated Space

Synchronization Data            12 bytes
Header Data                      4 bytes
User Data                     2048 bytes
Error Detection Code (EDC)       4 bytes
Unused Space                     8 bytes
Error Correction Code (ECC)    276 bytes


Figure 4:  Performance and storage characteristics of several types of
           media.

╓┌─────────────┌────────────┌─────────┌────────┌──────────┌────────────┌─────╖
             │ Small      │ Large   │        │          │ Large      │     ▌
             │ Winchester │ Optical │ Floppy │ Magnetic │ Winchester │ CD  ▌
       Media │ Disk       │ ROM     │ Disk   │ Tape     │ Disk       │ ROM ▌
─────────────┼▀▀▀▀▀▀▀▀▀▀▀▀┼▀▀▀▀▀▀▀▀▀┼▀▀▀▀▀▀▀▀┼▀▀▀▀▀▀▀▀▀▀┼▀▀▀▀▀▀▀▀▀▀▀▀┼▀▀▀▀▀┤
       Media ▌            │         │        │          │            │     │
        Cost ▌            │         │        │          │            │ 10- │
 (in U.S. $) ▌ N/A        │ 15-30   │ 1-5    │ 10-20    │ N/A        │ 20  │
─────────────┼────────────┼─────────┼────────┼──────────┼────────────┼─────┤
       Drive ▌            │         │        │          │            │     │
        Cost ▌            │ 7,000-  │ 200-   │ 3,000-   │ 10,000-    │ 500-│
 (in U.S. $) ▌ 500-3,000  │ 100,000 │ 1,500  │ 15,000   │ 150,000    │ 2500│
─────────────┼────────────┼─────────┼────────┼──────────┼────────────┼─────┤
    Capacity ▌            │ 1,000-  │ 0.36-  │          │            │ 550 │
             │ Small      │ Large   │        │          │ Large      │     ▌
             │ Winchester │ Optical │ Floppy │ Magnetic │ Winchester │ CD  ▌
       Media │ Disk       │ ROM     │ Disk   │ Tape     │ Disk       │ ROM ▌
─────────────┼▀▀▀▀▀▀▀▀▀▀▀▀┼▀▀▀▀▀▀▀▀▀┼▀▀▀▀▀▀▀▀┼▀▀▀▀▀▀▀▀▀▀┼▀▀▀▀▀▀▀▀▀▀▀▀┼▀▀▀▀▀┤
    Capacity ▌            │ 1,000-  │ 0.36-  │          │            │ 550 │
     (in Mb) ▌ 5-50       │ 4,000   │ 1.20   │ 30-300   │ 50-4000    │ 680 │
─────────────┼────────────┼─────────┼────────┼──────────┼────────────┼─────┤
       Media ▌            │         │        │          │            │     │
   Size (in) ▌ 5.25       │ 12.0    │ 5.25   │ 10.50    │ 14.0       │ 4.72│
─────────────┼────────────┼─────────┼────────┼──────────┼────────────┼─────┤
 Access Time ▌            │ 0.03-   │ 0.03-  │          │            │ 0.40│
   (in sec.) ▌ 0.03-0.30  │ 0.40    │ 0.05   │ 1-40     │ 0.01-0.08  │ -1  │
─────────────┼────────────┼─────────┼────────┼──────────┼────────────┼─────┤
     Density ▌            │         │        │          │            │ 35, │
  (bits/in.) ▌ 15,000     │ 35,000  │ 10,000 │ 6,250    │ 15,000     │  000│
─────────────┼────────────┼─────────┼────────┼──────────┼────────────┼─────┤
   Data Rate ▌            │         │        │          │            │     │
   (Kb/sec.) ▌ 625        │ 300     │ 31     │ 500      │ 2,500      │ 150 │
▀▀▀▀▀▀▀▀▀▀▀▀▀┴────────────┴─────────┴────────┴──────────┴────────────┴─────┘



Figure 5:  The HSG proposal focuses only on the 20448-byte user data space of
           the 2352-byte physical sector defined by Sony and Philips. The
           logical sector can then be further divided into logical blocks as
           per the HSG proposed definition.

                             ╔═══════════════════╗
                             ║  Logical Sector   ║
                             ╚═════════╤═════════╝
                                       │
                       ╔═══════════════╧═════════════════╗
                       │                                 │
   ┌────────┬──────────┬─────────────────────────────────┬───────╥───────┐
   │  Sync  │  Header  │       2048 bytes user data      │  EDC  ║  ECC  │
   └────────┴──────────┴─────────────────────────────────┴───────╨───────┘
   │                                                                     │
   ╚═══════════════════════════════════╤═════════════════════════════════╝
                                       │
                             ╔═════════╧═════════╗
                             ║  Physical Sector  ║
                             ╚═══════════════════╝


Figure 6:  The Path Table provides the means to move directly to the location
           of the first logical sector of any directory and subdirectory.

               VOLUME DESCRIPTOR
                       ├──────────────────────────────────────────┐
                       ▼                                          ▼
                     ROOT                                       ╔═══╗
        ┌─────┬─────┬──┴──┬─────┬─────┐                         ║   ║
        │ ┌─────┬─────┬─────┬─────┬─────┬───────────────────────║ P ║
        │ │   │ │   │ │   │ │   │ │   │ │                       ║ A ║
        ▼ ▼   ▼ ▼   ▼ ▼   ▼ ▼   ▼ ▼   ▼ ▼                       ║ T ║
       ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐                      ║ H ║
       │ A │ │ B │ │ C │ │ D │ │ E │ │ F │                      ║   ║
       └┬─┬┤ └───┘ └───┘ └───┘ └┬─┬┤ └───┘                      ║ T ║
        │ │└──────┐             │ │└──────┐                     ║ A ║
        ▼ ▼       ▼             ▼ ▼       ▼                     ║ B ║
    ┌───┐ ┌───┐ ┌───┐       ┌───┐ ┌───┐ ┌───┐                   ║ L ║
    │ G │ │ H │ │ I │       │ J │ │ K │ │ L │                   ║ E ║
    └───┘ └───┘ └───┘       └───┘ └───┘ └───┘                   ║   ║
                                                          ║   ║
      │     │     │           └─────┴─────┴─────────────────────║   ║
      └─────┴─────┴─────────────────────────────────────────────╚═══╝

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

MS-DOS CD ROM Extensions: A Standard PC Access Method

Tony Rizzo

Developers of CD ROM applications need not concern themselves with the
details of interfacing CD ROM drives to the MS-DOS environment.
Microsoft(R), working closely with CD ROM drive manufacturers, has developed
the software necessary to accomplish this. The MS-DOS CD ROM Extensions,
in conjunction with the High Sierra logical disc format, free developers
to concentrate solely on their applications and eliminates dependence on
any particular manufacturer's drive technology.

The MS-DOS CD ROM Extensions consist of a device driver for a CD ROM and a
RAM-resident program called MSCDEX.EXE that interfaces with MS-DOS. Together
they provide an interesting solution to some rather thorny MS-DOS/CD ROM
interfacing problems.


The Dilemma

CD ROM device drivers are of concern primarily to manufacturers of CD ROM
drives, who write and package them with their products in accordance with
Microsoft's "CD ROM Device Driver Specification." However, an understanding
of how these drivers work will benefit CD ROM application developers.

Unlike conventional hard and floppy disks, CD ROM discs are read-only,
with file structures that can occupy up to 660Mb of data space, part of
which might be audio or video information. There is no File Allocation
Table (FAT) as there is on a normal MS-DOS file system, and there is no
need to dynamically track disc space for allocating data. Furthermore, CD
ROM drives cannot be accessed via the standard MS-DOS Interrupt
25h/Interrupt 26h (read/write disk sector) mechanism, and they cannot be
looked at by the operating system at a physical level because they are not
in MS-DOS file format. Files and directories are not organized as MS-DOS
expects them, which is why CHKDSK, FORMAT, and other MS-DOS utilities will
not work on CD ROMs.

MS-DOS interfaces to standard magnetic media through block device drivers
that deal with disk drives at the hardware level and transfer blocks of
data in multiples of a given sector size. Also, block drivers are able to
support multiple devices (for example, two or more hard disks at once), as
well as removable media.

MS-DOS makes some assumptions about block device drivers. It assumes there
is a FAT, and it will attempt to read one when the device driver is
initialized. MS-DOS also assumes that it will need to assign drive letters
to each unit supported by a particular device driver. Neither of these
assumptions is relevant to CD ROM drives, nor can MS-DOS deal with CD
ROM's 660Mb data space, since MS-DOS can support a maximum disk space of
only 32Mb.

MS-DOS also accepts a character device driver, which deals with
information only one byte at a time, rather than in blocks. Character
device drivers are usually associated with serial I/O devices such as
modems, printers, the keyboard, and the video monitor. Character device
drivers are assigned a unique name instead of drive letters and do not
support multiple units or removable media. Since MS-DOS assumes nothing
about them, character device drivers are useful models for CD ROM
drivers.

What CD ROM required was a hybrid device driver closely modeled on
character device drivers but supporting, in block driver fashion, multiple
units and removable media. In fact, CD ROM device drivers are character
device drivers that have additional fields to support drive letters and
multiple units.

The standard MS-DOS character device driver command set consists of
commands to control input and output and the device itself, as well as to
query the status of the device. The CD ROM device driver command set
consists of a subset of these commands, plus a group of new commands that
pertain only to CD ROM drivers (see Figure 1).


CD ROM Device Drivers

Standard device drivers are simply EXE files that do not contain Program
Segment Prefixes. Because they have no PSPs, they originate at location 0h
rather than 100h. A device driver contains a header (see Figure 2), which
identifies the file as a device driver, defines the device's particular
attributes, and provides information that MS-DOS will need when the driver
is initialized. There is also a name/unit field that will hold a character
device name or, if the device is a block device, the number of units to be
supported.

Commands are passed to standard device drivers from MS-DOS via request
headers (see Figure 3), which contain system information such as the
length of the request header, the command code itself (from Figure 1) and
any data necessary for the device's operation, as well as a field in which
the driver can return a status code. If the device is a block device,
there will also be a subunit code to tell the device driver which of its
supported units is being requested.

When a CD ROM device driver is first initialized by MS-DOS, some
interesting things happen to maintain its character device driver disguise
while it is operating as a block device driver. This makes it necessary
for CD ROM device headers to have three additional fields that are not
found in standard character or block device headers (see Figure 4).

When MS-DOS installs device drivers, it gets its information from the
DEVICE=xxx entries in the CONFIG.SYS field. A typical line in a CD ROM
device entry in CONFIG.SYS might be:

  DEVICE=C:\DEV.SYS\HITACHI.SYS /D:CDROM1 /N:3

When MS-DOS reads this line, it issues an INIT command, via a request
header, to HITACHI.SYS (in subdirectory C:\DEV.SYS). The CD ROM device
driver responds in familiar character driver fashion by supplying its
strategy and interrupt routine offsets and its attributes (see Figure 5),
obtaining its unique device name, in this case CDROM1, by returning a 0 to
the INIT's request for number of units (which MS-DOS expects since it is
supposed to be a character device driver). The CD ROM device driver then
returns a status code (see Figure 6) that prevents MS-DOS from wanting to
read the FAT or trying to supply drive letters.

Having accomplished this, the CD ROM driver next determines from the
CONFIG.SYS entry the number of units it will support (3 in this case) and
fills the Number of Units field in its device header, which MS-DOS knows
nothing about. Next, the driver returns control to MS-DOS, which goes on
to read whatever else might be in CONFIG.SYS and then executes
AUTOEXEC.BAT, if it exists.

At this point, the CD ROM device driver has successfully fooled MS-DOS
into thinking it's a character device driver, and it has almost set itself
up as a block driver. Once initialized, MS-DOS itself will never again
deal directly with the CD ROM driver.


MSCDEX.EXE

Along with the CD ROM drive itself, drive manufacturers are planning to
include the CD ROM device driver and MSCDEX.EXE, a special terminate-and-
stay-resident (TSR) program that is really the key to the CD ROM
interfacing. MSCDEX.EXE is typically loaded from AUTOEXEC.BAT with a
command line such as:

  C:\MSCDEX.EXE /D:CDROM1 /D:CDROM2 /M:20 /V

The command line must contain the name of any CD ROM device driver that is
listed in CONFIG.SYS. MSCDEX.EXE goes through the following procedure for
each of the identified device drivers:

  ■ issues a DEVICE OPEN call to MS-DOS using the device driver's name and
    receives from MS-DOS a handle for the device driver

  ■ executes a DOS IOCTL call to obtain the device header address of the CD
    ROM device driver

  ■ determines the number of subunits that are to be supported by the driver
    and assigns legal drive letters for each subunit

  ■ after completing this process for every listed driver, terminates but
    remains resident in background mode

The MSCDEX.EXE command line shown above also contains a number of other
switches. /M:<value> tells MSCDEX.EXE how many sector buffers it should
allocate for its own use (the default value is four sectors per drive).
MSCDEX.EXE uses the sector buffers to cache sectors from the Path Table,
directory sectors, and file data sectors. Allowing it to cache more
directory information reduces its need to reread directories from the disc
and speeds things up. /V (for Verbose) provides additional start-up
information. Finally, there is an /L switch that allows manual
specification of a drive letter assignment.


The CD ROM Interface

At this point, the CD ROM drivers and drives are set up, but we still have
another problem. MS-DOS no longer knows anything about them, so how is it
able to access them? Again, MSCDEX.EXE fools the operating system, this
time using several components of MS-DOS in a unique way. MS-DOS Version
3.1 and later has a number of built-in networking features: when MS-DOS
sees a request for network drives, it sends the request to the network
redirector, which passes it on to the networking software.

MSCDEX.EXE designates the drive letters it assigns to its device drivers
as network drives. Whenever MS-DOS receives a CD ROM drive request, it
thinks it's a request for a network drive and forwards the request to the
redirector. MSCDEX.EXE remains active in the background intercepting these
requests, processing those that are really requests for a CD ROM drive,
and passing on the legitimate network requests to the redirector.

MS-DOS issues requests to the network at a virtual file level. MSCDEX.EXE
converts these virtual file level requests into physical sector requests
that can be understood by the CD ROM device driver. For example, a virtual
file level CD ROM request going to the network from MS-DOS might read:

  OPEN "\DIRECT1\SUBDIR2\FILEA"
  READ x bytes at FILEA offset y

MSCDEX.EXE intercepts the request, analyzes it, determines that it's a CD
ROM request, translates it to

  READ sector z

and sends the appropriate command codes to the CD ROM driver. Operating at
a file level, rather than a physical level, allows certain limits of MS-
DOS──for example the 32Mb file limit──to be circumvented. Figure 7
illustrates the entire process.

The end result: MS-DOS thinks that it has available to it a very large
network drive, which it can easily deal with. The MS-DOS CD ROM Extensions
(MSCDEX.EXE plus the CD ROM device drivers) thus enable a developer to
create a CD ROM-based application that will run on any CD ROM drive
attached to any PC.


The Commands

Now let's take a look at several of the commands supported by CD ROM
device drivers and see how they compare to various standard device driver
commands.

Figure 8 shows the standard READ/WRITE command control block. Note that
this particular control block is used for command codes 3, 4, 8, 9, 12, and
16 (refer to Figure 1). For CD ROM, only command codes 3 and 12 (IOCTL
INPUT and IOCTL OUTPUT) are valid.

IOCTL calls will permit MSCDEX.EXE and application programs to send
control strings to the device driver that consist of CD ROM-specific
commands or requests for information about the status of the device
drivers and the CD ROM drive. Figure 9 lists the CD ROM IOCTL codes for
command codes 3 and 12. The transfer address for an IOCTL call points to a
control block that is used for communication with the driver. The first
byte of the control block serves as the command code for the IOCTL call.
IOCTL INPUT calls request and receive information from the device driver
about the device. IOCTL OUTPUT calls instruct the device driver to open or
close the device door, eject a disc, and so on; in this case, the device
responds to the instruction but returns no information.

Figure 10 shows the CD ROM READ LONG command (command code 128). This
differs in a number of ways from the standard READ command control block
(see Figure 8). First, the media descriptor byte, which is set to 0 for
IOCTL calls and ignored, becomes the addressing mode byte. This byte will
almost always be set to 0, or High Sierra Group (HSG) addressing mode,
that is, long address values are read as logical block numbers, as per the
HSG format. All CD ROM drives will support HSG addressing mode, which can
be considered the standard or default mode of operation. Interleaving is
not yet supported, though it will play a role in file design in later
releases of the software.

The standard READ command pointer to a requested volume ID is replaced
with a data read mode byte in the READ LONG command. This will usually be
set to 0, for cooked mode, in which 2048 bytes are read using error
detection and correction. With the other option──raw mode, or Red Book
addressing mode (mode byte set to 1)──the entire 2352 bytes are read,
including the error detection and correction code. Any drives and drivers
that support raw mode can invoke it by setting this byte to 1.

Perhaps the major difference between READ and READ LONG is that the
byte/sector count field becomes the number of sectors to read, and the
starting sector number is expanded to double-word length. This means it
can now reference 4 gigasectors (or more than 8 terabytes). This is how
the CD ROM's larger capacity is handled.

At the very least, CD ROM device drivers will be able to return proper
values for IOCTL calls and will be able to read cooked mode 1 data sectors
using HSG addressing mode.

READ LONG PREFETCH attempts to anticipate where the next sector reads will
take place. It is a hint from MSCDEX.EXE to the device driver that the
sectors requested by this command will most likely be needed; it will
attempt to cache the requested sectors or at least position the read head
within the area they occupy. This minimizes the time that is required to
seek the next location when reading data.

SEEK positions the read head at specific locations on a disc. All CD ROM
device drivers are required to support DEVICE OPEN and DEVICE CLOSE and
are included for DOS compatibility. For CD ROM drives with audio capability,
PLAY would be used to play the audio section of a disc starting at a given
location, and STOP PLAY would end audio playing.


Conclusion

In the final analysis, CD ROM device drivers and their more familiar
siblings have more similarities than differences between them. They are
initialized in the same manner, they are accessed in basically the same
manner (at least to all appearances), and most of the systems issues are
handled the same way for each.

For example, the ES:BX registers are used by both MS-DOS and MSCDEX.EXE to
pass the far address of the request header when the driver's strategy
routine is called. Once the request header has been passed to the strategy
routine, it is dispatched to the appropriate subroutine. See Figure 11 for
sample CD ROM driver dispatch code.

The combination of the front-end MSCDEX.EXE program and the low-level CD
ROM device drivers has the potential to make CD ROM technology an integral
part of the familiar MS-DOS personal computing world. All that remains is
for developers to begin building applications to take advantage of this
new environment.


Figure 1:  Device Driver Command Codes

╓┌─────────┌──────────────────────────────┌──────────────────────────────────╖
                                          Supported
Command                                   CD ROM
Code      Name                            Command

0         INIT                            Yes
1         MEDIA CHECK (block devices)     No
2         BUILD BPB (block devices)       No
3         IOCTL INPUT                     Yes
4         INPUT (read)                    No
5         NONDESTRUCTIVE INPUT NO WAIT    No
6         INPUT STATUS                    No
7         INPUT FLUSH                     Yes
8         OUTPUT (write)                  No
                                          Supported
Command                                   CD ROM
Code      Name                            Command
8         OUTPUT (write)                  No
9         OUTPUT WITH VERIFY              No
10        OUTPUT STATUS                   No
11        OUTPUT FLUSH                    No
12        IOCTL OUTPUT                    Yes
13        DEVICE OPEN                     Yes
14        DEVICE CLOSE                    Yes
15        REMOVABLE MEDIA                 No
16        OUTPUT UNTIL BUSY               No
128       READ LONG                       Yes
129       Reserved
130       READ LONG PREFETCH              Yes
131       SEEK                            Yes
132       PLAY                            Yes
133       STOP PLAY                       Yes


Commands 0, 3, 7, 12, 13, 14, 128, 130, and 131 are the standard codes
necessary to write CD ROM device drivers.

Commands 132 and 133 are used to write extended CD ROM device dreivers that
support audio.

Commands 1, 2, 4, 5, 6, 8, 9, 10, 11, 15, 16, and 129 are not supported CD
ROM commands and will return an error code for unknown command.


Figure 2:  Standard Device Header Format

DevHdr       DD     -1         ; Ptr to next driver in file
                               ; or -1 if last driver
             DW     ?          ; Device attributes
             DW     ?          ; Device strategy entry point
             DW     ?          ; Device interrupt entry point
             DB     dup 8 (?)  ; Character device name field
                               ; or drive letter


Figure 3:  Request Header Format

ReqHdr       DB     ?          ; Length in bytes of request header
             DB     ?          ; Subunit code for minor devices
             DB     ?          ; Command code field
             DW     ?          ; Status
             DB     dup 8 (?)  ; Reserved


Figure 4:  CD ROM Device Header Format

DevHdr       DD     -1          ; Ptr to next driver in file
                                ; or -1 if last driver
             DW     ?           ; Device attributes
             DW     ?           ; Device strategy entry point
             DW     ?           ; Device interrupt entry point
             DB     dup 8 (?)   ; 8-byte character device name
                                ; field or drive letter
             DW     0           ; Reserved
             DB     0           ; Drive Letter
             DB     ?           ; Number of Units


Figure 5:  Sample Device Driver Including Attributes Field Definitions

DevHdr       DD     -1          ; Ptr to next driver in file
                                ; or -1 if last driver
             DW     0c800h      ; Device attributes
             DW     STRATEGY    ; Device strategy entry point
             DW     DEVINT      ; Device interrupt entry pint
             DB     `HSG-CD1'   ; 8-byte character device
                                ; name field
             DW     0           ; Reserved (must be zero)
             DB     0           ; Drive Letter (must be zero)
             DB     1           ; Number of Units (one or more)

Device Attributes

For CD ROM device drivers, the device attributes field is 0c800h.

Bit  15         1       Character Device
Bit  14         1       IOCTL supported
Bit  13         0       Output until busy
Bit  12         0       Reserved
Bit  11         1       OPEN/CLOSE/RM supported
Bits 10-4       0       Reserved
Bit  3          0       Dev is CLOCK
Bit  2          0       Dev is NUL
Bit  1          0       Dev is STO (standard output)
Bit  0          0       Dev is STI (standard input)


Figure 6:  Status Word

╔══════════════════════════════════════════════════════════════════════════╗
║┌──────┬────┬────────────────────────┬───┬───┬───────────────────────────┐║
║│ Bits │ 15 │ 14   13   12   11   10 │ 9 │ 8 │ 7  6  5  4  3   2   1   0 │║
║└──────┼────┼────────────────────────┼───┼───┼───────────────────────────┘║
║       │    │                        │ B │ D │                            ║
║       │ E  │        Reserved        │ U │ O │ Error Code (if bit 15 on)  ║
║       │ R  │                        │ S │ N │                            ║
║       │ R  │                        │ Y │ E │                            ║
╚══════════════════════════════════════════════════════════════════════════╝

The status word is zero on entry.

Bit 15      Error bit    Set by the device driver if an error is detected or
                         if an invalid request is made to the driver. The
                         low 8 bits indicate the error code.

Bits 14-10  Reserved

Bit 9       Busy bit     Set by the device driver when the device is in play
                         mode. All requsets of the physical device when this
                         bit is set will fail unless play mode is
                         interrputed (using the STOP PLAY function) and a
                         request is then made. Monitoring this bit will tell
                         when play mode is complete.

Bit 8       Done bit     Set by the device when an operation is complete.

Bits 7-0    Error code   0 - Write-protect violation
                         1 - Unknown unit
                         2 - Device not ready
                         3 - Unknown command
                         4 - CRC error
                         5 - Bad drive request stricture length
                         6 - Seek error
                         7 - Unknown media
                         8 - Sector not found
                         9 - Printer out of paper
                         A - Write fault
                         B - Read fault
                         C - General failure
                         D - Reserved
                         E - Reserved
                         F - Invalid disk change


Figure 7:  Summary of CD ROM/MSCDEX.EXE Initialized Events


           MS-DOS                                        MSCDEX

               │
               │              ┌────────────┐
               └────Init─────►│   Device   │█
                              │   Driver   │█
                              └────────────┘█
                               ▀▀▀▀▀▀▀▀▀▀▀▀▀▀
───────────────────────────────────────────────────────────────────────────

           MS-DOS    ◄────────────Open───────────────    MSCDEX


                              ┌────────────┐
                              │   Device   │█
                              │   Driver   │█
                              └────────────┘█
                               ▀▀▀▀▀▀▀▀▀▀▀▀▀▀
───────────────────────────────────────────────────────────────────────────
                     ◄───────────────────────────────
           MS-DOS                                        MSCDEX
                     ───────────────────────────────►
               │
             Ioctl            ┌────────────┐
              │ └────────────►│   Device   │█
              └───────────────┤   Driver   │█
                              └────────────┘█
                               ▀▀▀▀▀▀▀▀▀▀▀▀▀▀

───────────────────────────────────────────────────────────────────────────
                     Network                             Non-CD ROM
    MS-DOS  ──────► FILE LEVEL ──────►  MSCDEX  ──────► Calls Passed
                     Request                                 On
                                          │  
                                     All CD ROM Calls
                        ┌──────────────┐  │  │
                        │    CD ROM    │█ │  │
                        │    Device    │█◄┘  │
                        │    Driver    │█ ───┘
                        └──────────────┘█
                         ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀


Figure 8:  Standard Read/Write Command Format

Read or Write
Command code = 3, 4, 8, 9, 12, 16

CmdName      DB     (dup 13 0)  ; Request Header
             DB     ?           ; Media descriptor byte from BPB
             DD     ?           ; Transfer address
             DW     ?           ; Byte/Sector Count
             DW     ?           ; Starting sector number
                                ; (ignored on character devices)
             DD     ?           ; ptr to requested volume ID if
                                ; error 0Fh


Figure 9:  CD ROM IOCTL IMPUT and IOCTL OUTPUT, Command Code = 3 or 12

Note: CmdNAme below could be IOCTLI or IOCTLO, for example.

CmdName      DB     (dup 13 0)  ; Request Header
             DB     0           ; Media descriptor byte from
                                ; BPB is set to zero
             DD     ?           ; Transfer address
             DW     ?           ; Byte/Sector Count
             DW     0           ; Starting sector number
                                ; is set to zero
             DD     0           ; ptr to requested volume ID
                                ; if error 0Fh-set to zero

The transfer address points to a control block that is used to communicate
with the device driver. The first byte of the control determines the request
that is being made.

╓┌─────────────────────┌─────────┌───────────────────────────────────────────╖
                      Code      Function

For IOCTL INPUT       0         Return address of Device Header
(Command code = 3)    1         Location of Head
                      2         Reserved
                      3         Error Statistics
                      4         Audio Channel Info
                      5         Read Drive Bytes
                      6         Device Status
                      7         Return Sector Size
                      8         Return Volume Size
                      Code      Function
                      8         Return Volume Size
                      9         Media Changed
                      10        Audio Disc Info
                      11        Audio Track Info
                      12        Audio Q-Channel Info
                      13        Audio Sub-Channel Info
                      14-255    Reserved

For IOCTL OUTPUT      0         Eject Disc
(Command code = 12)   1         Lock/Unlock Door
                      2         Reset Drive
                      3         Audio Channel Control
                      4         Write Device Control String
                      5-255     Reserved


Figure 10:  READ LONG, Command Code = 128

ReadL        DB     (dup 13 0)  ; Request Header
             DB     ?           ; Addressing mode
             DD     ?           ; Transfer address
             DW     ?           ; Number of sectors to read
             DD     ?           ; Starting sector number
             DB     ?           ; Data read mode
             DB     ?           ; Interleave size
             DB     ?           ; Interleave skip factor


Figure 11:  Sample Dispatch Code for a CD ROM Device Driver

; ***Offsets for cmd buffer ***

             public drvtbl
drvtbl       label  word             ; Standard DOS device driver
                                     ; functions
             dw     error            ; *INIT (init. handled through
                                     ; far jump
             dw     error            ; MEDIA CHECK
             dw     error            ; GET BPB
             dw     ioctl$i          ; *IOCTL INPUT
             dw     error            ; INPUT
             dw     error            ; NON-DESTRUCTIVE INPUT
             dw     error            ; INPUT STATUS
             dw     error            ; *INPUT FLUSH (Nothing for this
                                     ; controller to do)
             dw     error            ; OUTPUT
             dw     error            ; OUTPUT WITH VERIFY
             dw     error            ; OUPTUT STATUS
             dw     error            ; OUTPUT FLUSH
             dw     ioctl$o          ; *IOCTL OUTPUT
             dw     devopen          ; *DEVICE OPEN
             dw     devclose         ; *DEVICE CLOSE
             dw     error            ; REMOVABLE MEDIA
             dw     error            ; OUTPUT UNITL BUSY
             public drvtbl2
drvtbl2      label  word             ; Extended CD ROM device driver
                                     ; functions
             dw     read$            ; *READ LONG
             dw     error            ; reserved
             dw     pfetch$          ; *READ LONG PREFETCH
             dw     seek$            ; *SEEK
             dw     play$            ; *PLAY (for extended drivers)
             dw     stop$            ; *STOP (for extended drivers)
             dw     error            ; WRITE LONG
             dw     error            ; WRITE LONG NON-BLOCKING
             dw     error            ; WRITE LONG VERIFY

             public ioi_tbl
ioi_tbl      label  word             ; IOCTL INPUT subfunctions
             dw     ret_addr         ; IOI_ret_addr
             dw     loc_head         ; IOI_loc_head
             dw     error            ; IOI_ioquery
             dw     error            ; IOI_err_status
             dw     error            ; IOI_audio_info
             dw     rd_drv           ; IOI_rd_drv_bytes
             dw     dev_stat         ; IOI_dev_status
             dw     sect_size        ; IOI_ret_sectsize
             dw     vol_size         ; IOI_ret_volsize
             dw     media_changed    ; IOI_media_changed
             dw     audio_diskinfo   ; IOI_audio_diskinfo
             dw     audio_trackinfo  ; IOI_audio_trackinfo
             dw     audio_qchaninfo  ; IOI_audio_qchaninfo
             dw     error            ; IOI_audio_subinfo
             dw     upc_code         ; IOI_upc_code

             public ioo_tbl
ioo_tbl      label  word             ; IOCTL OUTPUT sub-functions
             dw     eject            ; IOO_eject_disc
             dw     lock             ; IOO_lock_door
             dw     reset_drv        ; IOO_reset_drv
             dw     error            ; IOO_set_audio_param
             dw     wr_drv           ; IOO_wr_drv_bytes

             PAGE

; *** Device strategy routine ***

             public strat
strat        proc   far

             mov    word ptr cs:[reqhdr],bx
             mov    word ptr cs:[reqhdr+2],es
             ret
strat        endp
; *** Device interrupt handler ***

             public devint
devint       proc far

; save registers

             push   ax               ; (1)
             push   bx               ; (2)
             push   cx               ; (3)
             push   dx               ; (4)
             push   si               ; (5)
             push   di               ; (6)
             push   ds               ; (7)
             push   es               ; (8)

             lds    bx,cs:[reqhdr]        ; dx:bx -> req header

             mov    al,ds:rqh_unit[bx]    ; (al)= drive number
             mov    cs:drive_num,al       ; Save drive number

             mov    si,offset drvtbl      ; Assume it will be
                                          ; normal
             mov    al,ds:rqh_cmd[bx]     ; Get command in al
             cmp    al,DVRQ_NCMD_MAX      ; Check if normal
                                          ; command
             jbe    normal
             cmp    al,DVRQ_ECMD_MIN      ; Extended command?
             jae    ext                   ; Yes
             jmp    error                 ; Maybe

ext:
             cmp    al,DVRQ__EMCD_MAX     ; Extended command?
             jbe    ext2                  ; Yes
             jmp    error                 ; No

ext2:
             sub    al,DVRQ_ECMD_MIN      ; Convert to offset
                                          ; into table
             mov    si,offset drvtbl2     ; get address of
                                          ; table

normal:
             cbw
             shl    ax,1                  ; Change to index
                                          ; into command table
             add    si,ax

             cmp    ax,0                  ; Init. command?
             jz     init_this_dvd         ; Yes
                                          ; No

init_already_done:
             jmp    word ptr cs:[si]      ; dispatch to command

init_this_dvd:
             cmp    cs:init_dvd,0         ; Device driver init.
                                          ; yet?
             mov    cs:init_dvt,1         ; (Set the flag)
             jnz    init_already_done     ; Yes
                                          ; No
             jmp    far ptr doint         : Perform the
                                          ; initialization

; *** IOCTL OUTPUT command handler ***

             public ioctl$o
ioctl$o:
             mov    si,offset ioo_tbl
             mov    dl,IOO_cmd_max
             jmp    short ioctl

; *** IOCTL INPUT command handler ***

             public ioctl$i
ioctl$i:
             mov    si,offset ioi_tbl
             mov    dl,IOI_cmd_max
             jmp    short ioctl

ioctl:
             les    di,ds:ioctl_xfer[bx]
             mov    al,es:[di]
             inc    di                           ; Skip past command
                                                 ; code
             cmp    al,dl
             jbe    iolegal
             jmp    error

iolegal:
             cbw
             shl    ax,1
             add    si,ax
             jmp    word ptr cs:[si]             ; Dispatch to command
                                    ∙
                                    ∙
                                    ∙

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

Microsoft QuickBASIC: Everyone's First PC Language Gets Better

Dan Mick☼

Microsoft's implementation of nearly everyone's first language──BASIC──has
matured into quite a productive, flexible tool for professional program
development with Version 3.00 of Microsoft QuickBASIC (hereafter referred
to as QuickBASIC). Modular coding structures such as SUB...END SUB, DO
WHILE...LOOP and DO...WHILE, SELECT CASE, and others have moved venerable
old BASIC closer to traditionally more structured languages such as C and
Pascal/Modula-2.

Still, it seems that no matter how powerful the programming language, when
programming on microcomputers, the programmer has a real need for low-
level routines to access such things as MS-DOS(R) interrupts (service calls)
or to access the keyboard or CRT directly. This article will explore two
methods of dealing with custom, low-level routines that use QuickBASIC's
INT86 function as well as a custom assembly language function called
DSEARCH.


Why Low-Level Routines?

Why do we need to bypass QuickBASIC's normal set of functions? The DSEARCH
routine addresses a common problem I've encountered when using various
versions of Microsoft(R) BASIC──that of getting a disk directory and
providing it to the user for file choice. There is no good way in
Microsoft BASIC to accomplish this task. A common trick is to use the
rather inflexible FILES statement to provide a directory of a given disk.
This will at least let the user see which files are present and will let
the user retype the filename as input to the program. But there has never
been a good way to get the filenames into BASIC variables, so that a more
user-friendly interface could be created by presenting a menu or a window.

As with any limitation in a language implementation, clever programmers
have found ways around this one. Some have read the filenames character-
by-character directly from the screen after a FILES statement. Another
clever trick is to spawn a DOS DIR command with the output redirected to
a temporary file. Both these solutions are rather inelegant and time-
consuming. The former displays the FILES output momentarily on screen
before the program's custom file-choice interface is presented. Clever
programmers have however avoided this by setting foreground color to
background color. Using DIR output redirection involves such problems as
obtaining a truly unique filename and space on the disk for the redirected
output, which complicates the writing of really "bulletproof" code that
will run on many different hardware configurations. Clearly, a better
solution is preferable.

There is a better solution in the form of DSEARCH, which is an assembly
language subroutine I've written in Microsoft Macro Assembler. This
allows a QuickBASIC program to specify a search path and an MS-DOS file
attribute to search for and then either return a count of the files
matching the specifications or fill a preallocated string array with the
actual filenames (see Figure 1). I've tried to make the routine as
flexible as possible while striking a balance between doing too little in
assembler (making the support code too complex) and doing too much (making
the assembler code lengthy and labyrinthine). Figures 2 and 3 provide two
QuickBASIC programs that use DSEARCH: DSEARCHT.BAS is a test driver that
lets you play with DSEARCH's features, while BROWSE.BAS is a hard-disk
file management application that allows the user to traverse the directory
to browse and delete files.


Interface

Interfacing assembly language to QuickBASIC is much like interfacing to
other Microsoft languages. QuickBASIC uses the Intel-recommended method of
procedure calling: Parameters are passed on the stack, and the called
procedure expects a fixed number of parameters, which it cleans from the
stack before returning to the calling procedure. This is somewhat
different from the normal C calling procedure in which parameters are
removed by the calling program, allowing a variable number of parameters
to be easily passed. Rather, it is much like the pascal sequence available
in Microsoft Pascal or like using the pascal keyword in Microsoft C.

Preserving registers is another issue. The program must preserve the
values in DS, SS, and BP, although all other registers may be cleared,
including ES (the QuickBASIC code always does a PUSH DS/POP ES after the
CALL). Usually, you won't need to set up your own stack, but if you do,
keep in mind that you must obtain the parameters from the stack passed by
QuickBASIC, and you probably should do so before setting up a local
stack.

Local data may be held in the code segment in typical COM-file fashion.
However, it's nice to be able to put it in BASIC's data segment,
especially if you will need to access things in BASIC's data segment
anyway (as we will in the DSEARCH routine). To do this, the recommended
procedure is to allocate a segment named DATA with combine type DATA, use
a GROUP directive to put it in a group named DGROUP (QuickBASIC's reserved
group name for all its data segments), and then use an ASSUME DS:DGROUP
directive so that data references are assembled with the correct offset.
This is accomplished as shown in DSEARCH.ASM (see Figure 1).

A caveat here: If you want to obtain the address of a parameter in your
data area, it's best to use "LEA reg, item" rather than "MOV reg, OFFSET
item". Usually these two are equivalent, but when using groups, the offset
operator returns the offset from the beginning of the current segment,
rather than the offset from the beginning of the group, which is your
ticket to disaster here. The quick fix is to use LEA. You may also be able
to use "MOV reg, OFFSET DGROUP:item" instead. It's easy to forget the
group prefix, however, and you may find it easier to simply not use MOV
with an OFFSET.

The parameters are set up by BASIC on the stack, with the parameter
furthest to the left pushed first and the one furthest to the right
pushed last. A FAR CALL is then issued to the user subroutine. Typically,
the user subroutine saves the BP (base pointer) register on the stack,
loads BP with the current SP (stack pointer) value, and uses BP to access
the parameters on the stack. When the routine finishes, it executes a far
RET <nn> instruction to return to the caller. The <nn> specifies the
number of bytes to clean off the stack after obtaining the return address
from the stack. This enables the subroutine to leave the stack as it was
just before the push/call sequence.

Parameters are most often passed by reference from QuickBASIC, though
later versions of the language allow several new calling methods,
including "pass by value" and "pass by long pointer reference." Not much
is said about these in the QuickBASIC documentation though, and we won't
discuss them here. The normal CALL (parameter1, parameter2, ..., parametern)
statement is the one used for DSEARCH.


Data Types

The CALL statement pushes the address of each parameter, starting with the
one furthest to the left, as a 16-bit offset in QuickBASIC's data segment.
Since the SS, ES, and DS registers are set up to this segment by the call,
16 bits is enough of an address for most items. Large numeric array support
was added to QuickBASIC 2.0 so that numeric arrays dimensioned dynamically
would reside outside the default data segment. To use large numeric arrays,
some method of passing a double word address would be required. We're going
to use only integers, a string, and a string array.

The passing of the integers is the most straightforward. The word address
is simply the address containing the parameter, so that you may access the
variable by simply getting its address from the stack.

Strings are a bit more complex. The address passed is the address of a
string descriptor, which consists of two words. The first word is the
length of the string, while the second is the address (in QuickBASIC's
data segment) of the actual string data. Because of the dynamic string
space management BASIC uses, a subroutine should not change the length of
a string. This may seem quite restrictive at first if you need to pass
back a variable-length item. You can, however, get around this by setting
the string to a fixed, maximum length and then replacing the characters as
needed. This leaves fixed-length strings upon return, but if you've padded
them with blanks to start, you can just trim off the trailing blanks in
QuickBASIC, which knows how to deal with changing string lengths better
than a programmer can in assembler.

This leaves arrays. The normal syntax for passing arrays to QuickBASIC
subroutines is to put the array name in the CALL statement. This is more
flexible than most calling sequences, since the array may be of any
dimensionality and size. However, the array descriptor that is pushed onto
the stack is quite large and complex and is not documented in the
QuickBASIC manual, so it's better to pass the base address of the array.
Also, since QuickBASIC stores the arrays contiguously in a known order and
since the called routine must know the size and type of the array anyway,
it's much easier on the assembler code and uses less of the stack to pass
arrays by base address. This is how C always passes arrays, so the
procedure should be quite familiar to C programmers.


The STRUC Directive

MASM is much more structured than many microcomputer assemblers. It
performs type checking on operands in sometimes surprising ways, and its
data structure definition statements go far beyond the typical define
storage directives in many assemblers. One that is especially useful for
our purposes here is the STRUC directive, which allows you easy access to
the stack structure.

STRUC is similar to the RECORD statement in Pascal or the struct
definition in C. The statements put inside a STRUC/END block are the same
as normal data definition statements (those that simply reserve space,
such as DB, DW, and DD──not those that initialize that space) with labels
for each structure field. Those labels can then be used later in the
assembly to access areas of memory in record style──that is, you can load a
register with the base address of an area of memory that is laid out like
the structure definition and access it with the symbolic names in the
structure. As you would expect, this makes the code much more readable,
and in assembly language, every little bit of readability is crucial.

You can use a STRUC directive to describe the stack on entry to the
routine, enabling you to refer to the parameters by name instead of by
offsets from the BP register. This makes the actual assembler code much
clearer. Also, this will help with the return statement, which must clean
up the stack, requiring you to code a number based on how many and what
type of parameters were passed.

Here's a simple STRUC as an example:

  small    struc
  firstword  dw  ?
  secondword  dw  ?
  small    ends

When this structure is assembled, you will see offsets listed by the
labels in the structure, offset from 0. The member firstword will have an
offset of 0, and secondword will have an offset of 2.

Now, if you want to refer to the members of an area of memory consisting
of two words, as in this example, you can use an instruction like the
following (assuming that BX contains the address of the base of that
area):

  mov si,secondword[bx]
  mov si,secondword.bx
  mov si,[bx+secondword]
  mov si,bx[secondword]

Note that the expression evaluator is fairly flexible. I prefer the first
syntax, since it keeps the components in their most natural order: "Move
into si from secondword (an area addressed by bx)."

In DSEARCH, a structure just like this one describes the string descriptor
structure, which is of the same form as in the example above. The
structure is called strdes and has two fields, len and strptr (these refer
to the string length and the address of the string in QuickBASIC's data
segment). In the move_filename routine, these are used with the statement

  mov di,es:strptr[di]

which loads di with the word contained at es:di+2, but does it in a way
that makes it much more obvious why this is done.

Similarly, a structure called stk describes the stack on entry. This is
even more convenient, because it lets you access the parameters without
having to worry about the stack structure. This time, I used the BP
register, which tells the processor I want to get at the stack segment
automatically (which is why this is the logical choice for the structure
base register). Note the places reserved for the return address that
QuickBASIC pushes in the CALL process, as well as the place reserved for
the BP register, which is saved just before being loaded from SP. After
the MOV BP,SP instruction, I can do whatever else locally with the stack I
want, because I've established the base of that area of the stack
described in the structure stk in BP. Now I can PUSH registers at my
leisure and still get at the path$ parameter simply by referencing
path$[bp].

Note also the equate following the structure:

  popval=(offset path$) + (size path$) - (offset arrofs)

This calculates the parameter that will be required for the RET <nn>
statement at the end of the routine, so I don't have to worry about it.

I'm of the school that believes the tools you use should work as hard for
you as possible, correcting as many of your silly mistakes as they can.
For instance, I'm awful at calculating offsets, but if stack manipulation
isn't just so, the machine will crash horribly, possibly losing some work.
(Of course, it's good to take a very disaster-minded approach when doing
custom assembly language routines──saving everything before you test and
making sure your RAMdisk is backed up──but that's another story.) Using
structures and address calculations can prevent some of the really
disastrous careless errors in your coding. I use a version of this same
stack structure every time I write an assembler routine. It means that all
I have to do is change the number and names of the parameters, and the
structure and popval will take care of the rest.


Parameters to DSEARCH

Let's examine the particular syntax of the DSEARCH call and take a closer
look at parameter passing. DSEARCH will take the following parameters: a
search path string, a DOS file attribute (see MS-DOS documentation or the
comments in DSEARCH.ASM for a full description of the file attribute
settings), a parameter to hold the number of files found, one to indicate
the method of file searching based on the attribute, and one to carry the
address of the string array in which the filenames will be stored.

Why all these parameters? I began by imagining a simple routine just to
get the filenames and pass them back in an array. However, string arrays
use up a fair amount of space: there's the 4-byte overhead for each
element, plus the length of the string, plus another word of overhead in
the string space called the back pointer, and finally 1 byte per string
element. Since you can't change the length of a string, I needed to
allocate 12 spaces (8 filename characters, a period, and 3 extension
characters) to each string. This made a total of at least 18 bytes (12 +
4 + 2) for each filename. Also, since I imagined using DSEARCH only once
in a program, that space might well be wasted once the filename procedure
was finished. Clearly, a dynamic allocation would be better.

QuickBASIC came to the rescue here. Starting with QuickBASIC 2.0, dynamic
arrays were added to the language. With a dynamic string array, you can
redimension the array whenever you need it (starting from zero and using
as many names as required) and then discard the names once the user has
used the directory information. But how do you determine the number of
array positions you need?

I'd thought about using a find first/find next loop in DSEARCH from the
start. It occurred to me that I would have to have a similar routine to
count the filenames (so that the QuickBASIC code could allocate the array)
and then call a routine that actually filled the array. I decided that
because the code for the first routine would be similar to the code for
the second, I could make the first serve double duty by setting a flag
based on a parameter to indicate whether or not I was actually filling the
array. Since I needed an output parameter for the count of filenames, I
saved myself a bit of coding and used that same parameter for the
fill/don't fill indicator. This has an added advantage: since I'm making
one DSEARCH call to get the count, I can initially set the count parameter
to 0 and make the call, and then arrange DSEARCH so that this means "only
count filenames satisfying the pattern." For the next call, after the
QuickBASIC code has allocated a string array of the proper size, I can
simply leave the count as nonzero, eliminating some complexity from the
BASIC code as well.

At this point I had in mind a DSEARCH that would take a search path, a
count parameter (one that, on input, meant "only count or fill" depending
on whether it was zero or nonzero), and a string array for the result.
When I looked up the definition of the find first/find next calls in my
MS-DOS documentation, I saw that there was a file attribute input
parameter for the DOS call that would allow me to find subdirectories,
volume labels, and other such files. So I added a parameter indicating the
file attribute to search for. At first, I added this as another subfield
of the count parameter, but the manipulations were getting a little
sticky──I had the most significant bit of count indicating count/fill and
the low byte indicating the search attribute──and it was a little
convoluted. I decided that separating them wouldn't waste much space and
would make for much clearer code. (Such are the internal wars between an
assembly language hacker and a fan of structured program design.)

Finally, in the process of writing and testing DSEARCH, I found a problem
in the find first/find next functions. If, for example, you wanted to
search for subdirectories, you'd think you would set the subdirectory bit
in the search attribute and call find first/find next until no more files
were found. Unfortunately, the way MS-DOS interprets this search is "find
all files with the subdirectory bit set or with no bits set" so that it
returns all normal files as well as all subdirectory files. This makes
DSEARCH a little less flexible, especially in searching for volume labels,
for example.

I fixed this behavior in the DSEARCH code simply by checking each file
found to see if it matched the search attribute exactly, refusing to count
or return it if not. However, I could envision applications that would
want to find all normal files, and since other bits exist in the attribute
search (archive, system, hidden, and read-only, for example), it might
sometimes be necessary to find files the way MS-DOS does. So, I added one
final parameter that controls the search strategy. If the selective search
parameter is zero, the search is done just the way MS-DOS would do it; if
it's nonzero, the search excludes all files that don't match the search
attribute exactly.

In its final form, DSEARCH is called using this syntax:

  CALL DSEARCH(path$, attr, count, selective, array ofs)

All parameters but the first are integers. They must be integers since the
routine has to know the exact format of the call in order to behave
properly. This should come as no surprise.


Testing DSEARCH

So now we've designed DSEARCH and have written it absolutely bug-free the
first time, right? Well, maybe you're a different kind of programmer than
I am. I always make mistakes. So what's a good strategy for testing
assembler routines?

Whenever I write assembly language I like to count on using a symbolic
debugger such as SYMDEB, CodeView(R), or Periscope (each has its own
individual charm). With subroutines, though, this can be a little more
difficult than with standalone code. You must interrupt the execution just
at the point of your routine. Luckily, the symbolic debuggers allow you to
execute up until a label, such as "g dsearch," and do that painlessly.
This is the way I usually end up debugging QuickBASIC subroutines. It's a
little more involved than QuickBASIC development normally is, because the
module has to be incorporated into the code at link time, meaning the
editor environment is a little less convenient than usual. Of course, you
can use the /l switch when invoking QuickBASIC and load a user library
that you've constructed with BUILDLIB, but you still have the problem of
popping out to a debugger when your routine is executed. There are ways to
do this──such as imbedding an INT 3 in the code and then loading QuickBASIC
under the debugger──but, by and large, I prefer to use the standalone
compile/link option.

When using this option, be sure to specify the /MAP parameter to the
linker, so that it writes all public symbols to a MAP file. SYMDEB will
use this file to create a SYM file (with the MAPSYM utility). Periscope
will use the MAP file directly or create its own file, but the symbolic
debug is nearly crucial in my mind.

Also, I usually use the /O option on the QuickBASIC compile. Either way
will work, and omitting /O makes the LINK go much more quickly, but the
resulting executable is much easier to follow when /O is used. The run-
time version uses software interrupts for BASIC calls, whereas the /O
compile uses far calls to named routines and fewer in-line data switches
(in which a routine looks at memory at its return address and performs
different functions based on that data). This can make it difficult to set
breakpoints in the code.

So compile (while testing) with /O, link with /M, and then use a symbolic
debugger. This has the added advantage of saving all your work to disk
each time you try out the assembler code. It's so easy to hang the
machine, especially if you're new at assembly language interfacing, that I
can't stress this benefit enough. Dangerous things happen with great
frequency when you're designing routines like DSEARCH; be sure you're
prepared.

There are several things to be on the lookout for. Be sure the stack is
cleaned up properly on exit and that DS and BP are reset to their initial
values. I've made this mistake at least a hundred times, and I expect to
do it again. The STRUC tricks discussed earlier will save you a lot of
grief in such matters.

Also, be sure you know what you're accessing and where it is in reference
to the parameters. Does the location contain the address of the parameter
or the value itself? Check that you've got the right offset for the
parameter. In particular, watch for mistakes associated with the GROUP and
the OFFSET operator, as described earlier.


General Design Issues

Let's summarize the considerations behind designing the DSEARCH subroutine.
First, it's important to select a task that's difficult to do directly in
QuickBASIC──otherwise, why spend the time and effort on developing an
assembly language version, which is much harder than writing BASIC code.
Of course, speed is always an issue, too. Some screen operations can be
done much more quickly in assembler code, as we'll see later.

Another big consideration is the syntax and setup of the BASIC call. You
want to have a clean division of effort between the assembler routine and
the BASIC driver code. Carefully choosing what to let the assembler do and
what to let BASIC do can make for much cleaner, more readable code. For
example, the code using the combined count/attribute parameter described
earlier looked like this:

  attr% = &H10
    'find all files and subdirectories
  count% = &H8000 OR attr%
    'set hi bit to fill array
  call dsearch (path$, count%, arrofs%)

It's clearer in the BASIC code simply to add a separate attribute
parameter:

  attr% = &H10
    'find all files and subdirectories
  count% = 1
    'fill array, don't just count
  call dsearch (path$, count%, attr%, arrofs%)

The change in this example is intentionally minor. It doesn't make that
much difference in the way the calling code looks, and it does add slightly
to the complexity of the DSEARCH routine. However, any extra clarity helps
the matter, in my opinion.

Much more of an issue is whether to provide a full directory search
routine or simply some sort of interface to the find first/find next calls
that MS-DOS already has. I decided that I could do nearly the same thing
with INT86 already, and the object was to make a routine that was not too
complex in assembly language (because I prefer to write in BASIC), but
that saved some time writing in BASIC and provided a tool that I could
use. I think that putting the looping/selection code in assembler
accomplishes this. I can now issue a call/allocate/call and get a whole
list of files rather painlessly. Also, I've managed to provide a little
more flexibility with the selective search parameter.

The thing to keep in mind is that often the task can be accomplished in
many different ways. Performing part of it in BASIC and part in assembler
affords you the option of writing the way you write most clearly. Some
extra thought in the design and modification stage will pay off in the
final version and as you later reuse your toolbox of assembler routines.


Demonstration

In order to demonstrate what DSEARCH can do for a BASIC application and,
not coincidentally, to help with the design and modification of the routine,
I decided I'd write a user interface similar to those used for entering
filenames in many popular PC products. I wanted to provide a directory and
allow cursor key selection of the filename desired (in a manner similar to
the editor in QuickBASIC). Similarly, I thought I'd provide subdirectories
as a possible selection, allowing the user there to change directories and
see a new filename list. I decided to make all this happen in relatively
flexibly sized and colored windows as well, and perhaps to provide the whole
routine as a prewired, drop-it-in subroutine that readers could use in their
own code.

As I started writing, though, things got out of hand. I kept adding little
features, tweaking this and that, thinking, "It would sure be neat if you
could do this," and so on. So now I have a pretty useful file- and
directory-browsing and maintenance utility that I call BROWSE. The program
starts by displaying a directory of the currently logged path and drive,
courtesy of two DSEARCH calls, and then presents a window of definable
size and color containing as many filenames and subdirectory names as will
fit. The cursor keys move a reverse-video bar cursor around the filenames,
and when one is selected with the return key, BROWSE determines whether
the name is that of a subdirectory or a file. If the former, the directory
is changed and a new window of filenames presented. If the latter, the
file is sent to one of two routines──TypeIt or ListIt──that use QuickBASIC's
SHELL statement either to TYPE file | MORE for one-way browsing or to
invoke the popular shareware file browser LIST for browsing the file
bidirectionally. Also, I've assigned F1 to change logged disk drives and
Alt-D to delete a file (if you decide that the file you just browsed isn't
worth keeping). Alt-X terminates the program.

The code is fairly well commented, so each routine doesn't need a lot of
discussion. I do want to mention, though, that the SaveScreen and
RestoreScreen routines are never called. They turned out to be relatively
slow, and I've ended up with a sort of standalone demonstration, anyway.
They certainly can be used, though, with the dummy ScreenData array, to
implement a clean "save the old screen, make a new window, and then put
the old screen back" sort of application.

Actually, one solution I really like is to write a pair of short assembly
language routines to perform the "save screen to array" and "restore
screen from array" functions. Routines such as these really do benefit
from assembly language's terseness and direct access to machine hardware,
and ones I've written for the IBM(R) PC and for the Zenith(R) Z-100 are very
nice to use. Snappy window response is one thing that increases a user's
confidence in an application, and for raw speed, this is a perfect
application.

Another feature I'd like to point out is the INT86 routine. More and more,
MS-DOS implementations of languages include some nice facility for calling
DOS interrupts directly to perform system services that might be a bit
difficult to accomplish in other ways. Before QuickBASIC, if you wanted to
call the DOS Change Directory function from BASIC, you would have had to
write a short assembly language routine to allow the interface. Well,
that's exactly what INT86 provides: a way to interface directly to
software interrupts without all the fuss and bother of writing your own
assembler routine. For a function such as Change Directory, which really
does only one thing, INT86 is the perfect solution. I've used INT86 to
implement the get/set current path/disk functions in BROWSE, in
subroutines, and I think the result is elegant and handy.


Room for Improvement

Of course, BROWSE isn't perfect, nor is the set of routines included in
BROWSE going to be perfect for your own directory interface. You'll
undoubtedly be able to enhance the utility to meet your own needs──for
example, by adding a pair of better save/restore routines to help give
QuickBASIC that extra punch that makes a professional application stand
out from the crowd. Combining the powerful new QuickBASIC control
structures and statements, the INT86 interface to MS-DOS and BIOS
services, and custom assembly language routines of your own design, it's
easy and worthwhile to create sophisticated applications with QuickBASIC,
a language that's no longer just for beginners.


Figure 1:  Code Listing for DSEARCH.ASM

;;     DSEARCH.ASM
;;     Written by Dan Mick

        page    62,132

data    segment public 'data'
dta_ofs dw      ?               ;save area for BASIC's DTA
dta_seg dw      ?
fill    db      ?               ;"fill array" flag
selflg  db      ?               ;"selective search" flag
search_attr     db      ?       ;files' search attribute
filcnt  dw      ?               ;count of files found
path    db      64 dup (0)      ;search path, passed from BASIC

;DTA structure for findfirst, findnext calls (see DOS documentation)

dta     db      21 dup (0)      ;reserved area (used for findnext)
attrib  db      ?               ;attribute of file found
time    dw      ?               ;time of last write
date    dw      ?               ;date
fsize   dd      ?               ;filesize (32 bit integer)
fname   db      13 dup (0)      ;ASCIIZ filename: name.ext,00
data    ends

dgroup  group   data

code    segment byte public 'code'
        assume  cs:code, ds:dgroup
        .xlist
;
;CALL DSEARCH(path$, attr, count, selective, array ofs)
; path$ is a normal string variable with the path for the
; directory search. It be a full pathname, including filename
; and extension fields. To get all the files in the default
; directory on C:, for instance, path$="C:*.*", or all the files
; in subdir on the current drive, path$="subdir\*.*".
; attr is the MS-DOS file attribute for the search. See below for
; a description. Each bit represents a file attribute.
; count is an input and an output parameter. On input, if it is
; zero, the files will not be put in the array, only counted.
; This is provided so that a dynamic array may be allocated after
; the size is determined by dsearch(). If count is non-zero on
; input, the filenames will be placed in the array.
; In either case, the count of filenames found is returned.
; selective is a flag indicating how to do the counting and
; searching process. If the file attribute 10 is specified, for
; example, DOS will return all those filenames that have the
; subdir attribute as well as those that don't. If selective is
; set nonzero, the non-subdir files will not be returned. What
; this does, basically, is allow a "filter normal files" selection
; so that subdirs and volume labels may easily be found.
; array offset is a pointer (offset only for strings) to a string
; array big enough to hold all files found in path$. The size
; required may be determined as described below.
;
; The attribute byte:
;
;           -------------------------------------
;           | X | X | X | A | D | V | S | H | R |
;           -------------------------------------
;
;  X is don't care, A = archive, D = subdirectory, V = volume label,
;  S = system, H = hidden, R = read-only.
;
        .list

stk     struc
bbp     dw      ?               ;BASIC's bp
retadd  dd      ?               ;return address
arrofs  dw      ?               ;param: array offset
select  dw      ?               ;param: filter normal files flag
count   dw      ?               ;param: count flag/count return
attr    dw      ?               ;param: search attr
path$   dw      ?               ;param: pathname to search
stk     ends

;calculate value to pop with RET n at end of proc (length of params)
popval  =       (offset path$) + (size path$) - (offset arrofs)

strdes  struc                   ;BASIC string descriptor structure
len     dw      ?               ;word length of string
strptr  dw      ?               ;pointer to string space
strdes  ends

dsearch proc    far             ;it's a far proc to BASIC
        public  dsearch         ;let LINK know about this one

        push    bp              ;save BASIC's bp
        mov     bp,sp           ;address stack as it exists now

        mov     [filcnt],0      ;initialize file count

        mov     si,path$[bp]    ;get pointer to string descriptor
        mov     cx,len[si]      ;cx = path length
        mov     si,strptr[si]   ;si -> path string data
        lea     di,path         ;di -> place to fill
        rep     movsb           ;move pathname to our data area
        mov     byte ptr es:[di],0 ;make sure it's an ASCIIZ string

        mov     si,count[bp]    ;get pointer to fill flag in di
        mov     cx,[si]         ;cx = fill flag
        mov     byte ptr [fill],0 ;set flag to "no" first
        or      cx,cx           ;nonzero?
        jz      nofill          ;nope
        mov     byte ptr [fill],0ffh ;yes, set flag

nofill: mov     di,select[bp]   ;get pointer to selective flag in di
        mov     cx,[di]         ;cx = selective flag
        mov     byte ptr [selflg],0 ;set flag to "no" first
        or      cx,cx           ;nonzero?
        jz      nosel           ;nope
        mov     byte ptr [selflg],0ffh ;yes, set flag

nosel:  mov     di,attr[bp]     ;point at search attribute param
        mov     cx,[di]         ;and get it

        mov     ah,2fh          ;get BASIC's DTA
        int     21h
        mov     [dta_ofs],bx
        mov     [dta_seg],es    ;and save it
        lea     dx,dta          ;set DTA to our area here
        mov     ah,1ah
        int     21h

        mov     di,arrofs[bp]   ;di -> first string descriptor
        mov     di,[di]         ;di = first string descriptor offset
        mov     bx,ds           ;set up es to string array segment
        mov     es,bx           ; (BASIC's data segment)
                                ;now es:di -> first string descriptor

        mov     ah,4eh          ;find first matching file
        xor     ch,ch           ;clear hi part of attribute
        lea     dx,path         ;ds:dx points to search path
        int     21h
        jnc     ok              ;if error, cy set
        cmp     ax,18           ;no files found?
        jz      exit            ;yes, don't report error
        mov     [filcnt],0ffffh ;set count to -1 to report path error
        jmp     short exit      ;and leave

ok:     call    countit         ;rets cy if should save, incs filcnt
        jz      findnext        ;no fill if zr set
        call    move_filename   ;do the move

findnext:
        mov     ah,4fh          ;find next function
        int     21h             ;do it
        jc      exit            ;if error, must be no more files
        call    countit         ;count, return cy if should save
        jz      findnext        ;if zr set, don't save name
        call    move_filename   ;move it
        jmp     short findnext  ;and keep hunting

exit:   push    ds
        lds     dx,dword ptr [dta_ofs] ;get BASIC's DTA
        mov     ah,1ah                 ;set DTA fn.
        int     21h
        pop     ds
        mov     di,count[bp]    ;di -> fattr for count return
        mov     ax,[filcnt]     ;get file count
        mov     [di],ax         ;put file count in fattr
        pop     bp              ;restore BASIC's BP
        ret     popval          ;return and pop parameter pointers
dsearch endp

countit         proc
;

;Check file attribute if selective, and update count or not based on
;result. Return flag saying whether or not to move filename, too,
;based on updated count and the "fill" flag.

;
         cmp     [selflg],0ffh  ;are we selective?
         jnz     bump           ;nope, count it
         cmp     [attrib],cl    ;is the attr just what we want?
         je      bump           ;yes, count this file
         cmp     cl,cl          ;nope, set ZF
         jmp     short dontfill ;and skip to exit
bump:    inc     [filcnt]       ;update counter
         cmp     [fill],0       ;now, are we filling?
dontfill:                       ;get here from jne, so NZ
         ret                    ; return with NZ if filling
countit         endp

move_filename   proc    near
;
;es:di points to string descriptor to fill with filename from DTA
;
        push    di              ;save pointer to descr. length part
        mov     di,es:strptr[di] ;and point instead to string data
        lea     si,fname        ;si -> filename (ASCIIZ)
moveloop:
        lodsb                   ;get filename character
        or      al,al           ;is it 0? (end of string?)
        jz      done            ;yes, quit moving data
        stosb                   ;no, store
        jmp     short moveloop  ;and continue
done:   test    byte ptr [attrib],10h ;is this a subdir?
        jz      notsub          ;no
        mov     al,'\'          ;yes, store a trailing backslash
        stosb
notsub: pop     di              ;di -> length in s.d. again
        add     di,4            ;move to next s.d. pointer
        ret
move_filename   endp

code    ends
        end     dsearch


Figure 2:  DSEARCHT.BAS is a "Test Program" for DSEARCH.

'dsearcht.bas
'simple program to check dsearch routine and make sure it's
'working. The idea is to call it just enough to check out
'its capabilities, and perhaps vary its input by small
'amounts, so that you get a controlled test where there are
'no likely bugs in the BASIC code.

rem $dynamic
DIM A$(0)

start:
input "Path for directory? (wildcards allowed): ",path$
if instr("\:",right$(path$,1)) then path$=path$+"*.*"

'get number of filenames in path
attr%=&H10:arrofs%=0:count%=0:selective%=0
call dsearch(path$,attr%,count%,selective%,arrofs%)

'now count% has actual filename count for redim, or -1 if error
'(other than file not found, that is.)

if count%=-1 then print "Bad pathname":goto start
redim a$(count%)

print "There are "count%"filenames in path "path$"."
'set up a$ so that dsearch doesn't have to change length
for i=0 to count%-1:a$(i)=space$(12):next i

arrptr=varptr(a$(0))
arrofs%=int(arrptr)             'explicit conversion

call dsearch(path$,attr%,count%,selective%,arrofs%)

for i= 0 to count%-1:print i;":";a$(i):next i
goto start


Figure 3:  Code Listing for BROWSE.BAS

'BROWSE.BAS - a file browser/directory maintenance routine
'To compile in the editor environment, make a user library by
'assembling DSEARCH.ASM, using BUILDLIB to add it and USERLIB.OBJ
'to a user library (call it MYLIB.EXE, for instance), and then
'use the /L switch to load QB, as in "QB BROWSE /L MYLIB.EXE".
'To compile standalone, use "LINK BROWSE+DSEARCH+USERLIB...."

defint a-z
dim winrec(8) 'holds window dimensions, colors for text and borders

'the following arrays and constants are used for int86()
dim inary(7), outary(7)
const ax = 0, bx = 1, cx = 2, dx   = 3
const bp = 4, si = 5, di = 6, flag = 7

'actions returned by PageAndSelect
const disk.change = 1, delete.file = 2, type.file = 3, quit = 4
action.flag = 0          'set by PageAndSelect to one of the above

rem $dynamic
dim filename$(0)
dim shared ScreenData (0,0)

def fn.min(x,y)
   if x < y then fn.min = x else fn.min = y
end def

def fn.max(x,y)
   if x > y then fn.max = x else fn.max = y
end def

'************************ MAIN PROGRAM **************************

call GetCurrDisk(origdisk$)
call GetCurrPath(origdisk$,origpath$)

disk$ = origdisk$

'get a count of all normal and subdir files

Do
   call GetCurrPath(disk$,CurrPath$)
   searchpath$ = disk$ + ":" + CurrPath$ + "*.*"

   attr = &H10 : count  =  0 : selective = 0
   arrofs = int(varptr(filename$(0)))
   call dsearch (searchpath$,attr,count,selective,arrofs)

   ' This should never happen!...
   if count = -1 then
      locate 25,1:print "Invalid path: "searchpath$
      print "Strike a key...":a$ = input$(1):end
   end if

   redim filename$(count-1)
   for i = 0 to count-1:filename$(i) = space$(12):next i

   arrofs = int(varptr(filename$(0)))
   call dsearch (searchpath$,attr,count,selective,arrofs)

   'now filename$(0 to count-1) have all the files and subdirs
   winrec(1) = 1  : winrec(2) = 1    'upper left of window  (row,col)
   winrec(3) = 10 : winrec(4) = 80   'lower right of window
   winrec(5) = 0  : winrec(6) = 11   'fg/bg color
   winrec(7) = 10 : winrec(8) = 0    'border colors, fg/bg

   call makewindow (winrec())

   'do window/cursor/path manipulation.

   call PageAndSelect(winrec(),filename$(),action.flag,f$)

   'f$ comes back as fixed-length (12) string
   ' ...strip off trailing blanks
   i = instr(f$," ") : if i > 0 then f$ = left$(f$,i-1)

   select case action.flag
      case quit
         call SetCurrDisk(OrigDisk$)
         call SetCurrPath(OrigPath$)
         cls : end
      case disk.change
         cls : print "Enter new disk drive letter: ";
         disk$ = input$(1) : print disk$;
         if disk$ > "Z" then disk$ = chr$(asc(disk$) and &HDF)
         call SetCurrDisk(disk$)
         action.flag = 0
      case type.file
         if right$(f$,1) = "\" then
            CurrPath$ = CurrPath$ + f$
            call SetCurrPath(CurrPath$)
         else
            call ListIt (disk$+":"+CurrPath$+f$)
         end if
      case delete.file
         kill (disk$+":"+CurrPath$+f$)
   end select

   action.flag = 0
loop while (1)

'****************************************************************

sub makewindow (winrec(1)) static
y1 = winrec(1):x1 = winrec(2):y2  = winrec(3):x2  = winrec(4)
fc = winrec(5):bc = winrec(6):bfc = winrec(7):bbc = winrec(8)
wid  =  x2-x1+1 : height = y2-y1+1

const vert    = 186, upright = 187, lowright = 188
const lowleft = 200, upleft  = 201, horiz    = 205

color bfc,bbc
locate y1,x1:print chr$(upleft);string$(wid-2,horiz);chr$(upright);
for i = 2 to height-1
   locate y1+i-1, x1:print chr$(vert);
   locate y1+i-1, x2:print chr$(vert);
next i
locate y2,x1:print chr$(lowleft);string$(wid-2,horiz);chr$(lowright);

call clearwindow(winrec())
end sub

'****************************************************************

sub clearwindow (winrec(1)) static
y1 = winrec(1):x1 = winrec(2):y2  = winrec(3):x2  = winrec(4)
fc = winrec(5):bc = winrec(6):bfc = winrec(7):bbc = winrec(8)
wid  =  x2-x1+1 : height = y2-y1+1

color fc,bc
for i = 2 to height-1
   locate y1+i-1,x1+1:print string$(wid-2," ");
next i
end sub

'****************************************************************

sub savewindow (winrec(1)) static
y1 = winrec(1):x1 = winrec(2):y2  = winrec(3):x2  = winrec(4)
fc = winrec(5):bc = winrec(6):bfc = winrec(7):bbc = winrec(8)
wid = x2-x1+1 : height = y2-y1+1

for i = x1 to x2
   for j = y1 to y2
      ScreenData(j-y1,i-x1) = screen(j,i,1) * 256 + screen (j,i,0)
   next j
next i
end sub

'****************************************************************

sub restorewindow (winrec(1)) static
y1 = winrec(1):x1 = winrec(2):y2  = winrec(3):x2  = winrec(4)
fc = winrec(5):bc = winrec(6):bfc = winrec(7):bbc = winrec(8)

for j = y1 to y2
   locate j,x1
   for i = x1 to x2
      d = ScreenData(j-y1,i-x1)
      bc = (d\256)\8 : fc = (d\256) mod 8 : color fc,bc
      print chr$(d and &hff);
   next i
next j
end sub

'****************************************************************

sub PageAndSelect_
  (winrec(1),file$(1),action.flag,FileSelected$) static

'This routine does all the work here. It assumes MakeWindow
'has been called before entering, and does all key processing and
'subsequent window updates. action.flag is returned as the action
'for the main program to take (see the constants in the main program
'for a list of possible actions).

shared disk$,CurrPath$,count

'second codes from extended keys used in PageAndSelect
const up = 72, down = 80, left = 75, right = 77
const f1 = 59, AltD = 32, AltX = 45

y1 = winrec(1):x1 = winrec(2):y2  = winrec(3):x2  = winrec(4)
fc = winrec(5):bc = winrec(6):bfc = winrec(7):bbc = winrec(8)
wid = x2-x1+1 : height = y2-y1+1

locate y1,x1+1:color bfc,bbc:print " ";disk$;":";CurrPath$;" "
locate y2,x1+1
print "RET-Type file  AltD - delete file  F1 - chg disk  AltX - quit"
locate y1,x2-1-9:print count;"files"

NamesPerLine = (wid-2)\15 'how many filenames on each line in window
lines = height-2          'how many usable lines inside window
FilesPerWindow = lines * NamesPerLine

StartIndex = 0  'first file$() to be displayed in window
CurrIndex  = 0  'current file$() being highlighted
NewIndex   = 0  'updated file$() index after moving cursor

do
   call clearwindow(winrec())
   limit = fn.min (ubound(file$) - StartIndex, FilesPerWindow - 1)
   color fc,bc : locate y1+1
   for i = 0 to limit step NamesPerLine
      for j = 0 to NamesPerLine-1
         locate ,x1 + 1 + j*15
         if StartIndex + i + j <= ubound(file$) then
            print file$(StartIndex+i+j);
         else
            print space$(15);
         end if
      next j
   print
   next i

   'initialize highlight bar position
   BarRow = y1+1 : BarCol = x1 + 1

   do
      color fc,bc : locate BarRow,BarCol : print file$(CurrIndex);
      CurrIndex = NewIndex
      BarRow = (CurrIndex-StartIndex)\NamesPerLine + y1 + 1
      BarCol = (CurrIndex mod NamesPerLine)*15 + x1 + 1
      color bc,fc : locate BarRow,BarCol : print file$(CurrIndex);

      'wait for extended character (look for cursor keys) or RETURN
      GetKey:
         a$ = "" : while a$ = "" : a$ = inkey$ : wend
         if a$ = chr$(13) then
            action.flag = type.file
            FileSelected$ = file$(CurrIndex)
            exit do
         end if
      if len(a$)<>2 then goto GetKey

      redraw = 0  'flag to indicate when window must be redrawn
                  'and in which direction the cursor was moving
      select case asc(right$(a$,1))
         case up
            NewIndex = fn.max (0,CurrIndex-NamesPerLine)
            if NewIndex < StartIndex then redraw = -1
         case down
            NewIndex = fn.min (ubound(file$), CurrIndex+NamesPerLine)
            if NewIndex > StartIndex + FilesPerWindow - 1 _
             then redraw = 1
         case left
            NewIndex = fn.max (0,CurrIndex-1)
            if NewIndex < StartIndex then redraw = -1
         case right
            NewIndex = fn.min (ubound(file$), CurrIndex + 1)
            if NewIndex > StartIndex + FilesPerWindow - 1 _
             then redraw = 1

         case f1
            action.flag = disk.change : file.selected = -1 : exit do
         case AltX
            action.flag = quit : file.selected = -1 : exit do
         case AltD
            action.flag = delete.file
            FileSelected$ = file$(CurrIndex) : exit do
         case else
            sound 440,2:sound 220,2
      end select
   loop while redraw = 0

   if action.flag then exit do
   'otherwise, fall through and redo the entire "do" loop
   select case sgn(redraw)
      case -1
        StartIndex = fn.max (StartIndex - FilesPerWindow , 0)
      case 1
        StartIndex = fn.min (StartIndex + FilesPerWindow , _
                             ubound(file$))
   end select
   CurrIndex = StartIndex
loop while not action.flag

end sub

'****************************************************************

sub     GetCurrPath(disk$,CurrPath$) static
shared inary(),outary()

'first set up 64-byte work area for Int 21h Function 47h
' (get current directory)
CurrPath$ = space$(64) : pathptr = sadd(CurrPath$)

inary(dx)=asc(disk$)-65+1
inary(si)=pathptr 'pointer to area to fill
inary(ax)=&H4700  'function number
call int86(&H21,varptr(inary(0)),varptr(outary(0)))

'strip off trailing NUL, rest of blanks
CurrPath$=left$(CurrPath$,instr(CurrPath$,chr$(0))-1)
CurrPath$="\"+CurrPath$
if CurrPath$ <> "\" then CurrPath$=CurrPath$+"\"
end sub

'****************************************************************

sub GetCurrDisk(disk$) static
shared inary(),outary()
inary(ax) = &H1900
call int86(&H21,varptr(inary(0)),varptr(outary(0)))
disk$ = chr$(65 + (outary(ax) and 255))
end sub

'****************************************************************

sub SetCurrDisk(disk$) static
shared inary(),outary()

inary(ax) = &H0E00
inary(dx) = asc(disk$) - 65
call int86(&H21,varptr(inary(0)),varptr(outary(0)))

end sub

'****************************************************************

sub SetCurrPath(path$) static
shared inary(),outary()

newpath$=path$
if newpath$<>"\" then newpath$=left$(newpath$,len(newpath$)-1)
newpath$=newpath$+chr$(0)

inary(ax) = &H3B00
inary(dx) = sadd(newpath$)
call int86(&H21,varptr(inary(0)),varptr(outary(0)))

end sub

'****************************************************************

sub TypeIt (path$) static
   cls
   shell ("type "+path$+" | more")
   locate 25,1:print "Any key to continue...";
   a$=input$(1):cls
end sub

'****************************************************************

sub ListIt (path$) static
   shell ("list "+path$)
   cls
end sub

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

Ask Dr. Bob!

Undocumented 34H Call

Dear Dr. Bob,

Can you shed any light on the undocumented MS-DOS(R) call 34H that was
briefly mentioned in the article on TSR standards in the December 1986
issue ("Moving Toward an Industry Standard for Developing TSRs," MSJ,
Vol. 1 No. 2)? It's not clear how it can be used. Can I check this flag to
determine if it's safe to pop up my terminal program?──MF

Setting AH to 34H and generating an Interrupt 21H returns a pointer to
some interesting information. Since this particular function is
undocumented, all of the information we've discovered about it has come
from people who specialize in DOS spelunking.

DOS will return a pointer in the register pair ES:BX, which points to the
location in DOS where the first byte of the Critical Section Flag exists.
If your TSR program checks this flag and makes sure it is set to zero
before attempting to issue a call to DOS, you'll be pretty safe. Depending
on the DOS version, you should check the byte after (DOS 2.1), before (DOS
3.0 and 3.1, if not Compaq-DOS), or 01AAH bytes before (Compaq-DOS 3.0).
You should also check to make sure you're not in the middle of a BIOS
Interrupt 13H (Disk I/O) or 16H (Keyboard Service Request).

Hooking into the 28H interrupt chain allows you to call DOS at times when
the Critical Section Flag would normally indicate that DOS was busy. But
be warned: you cannot generate any calls to DOS with AH <= Ch from the
Interrupt 28 chain.

Remember, of course, that since this is undocumented and unsupported by
Microsoft, there is no guarantee that it will work or that it will not
disappear in future versions of DOS.


Dynamic Dialog Boxes

Dear Dr. Bob,

I am building a Windows application that requires my dialog boxes to be
dynamic, that is, I need to be able to add or remove controls under
"program control." Do I need to write my own version of the dialog box
routines in order to accomplish this? Or are there existing Windows
functions to handle this situation? What does Windows 2.0 provide?──DBL

Yes, you can add and remove dialog box controls at any time by using the
normal CreateWindow and DestroyWindow functions. Dialog box controls are
ordinary child windows, and when you call CreateDialog or DialogBox, it
simply does a series of CreateWindow calls to create the child controls.
You can create additional child windows at any time, even after the dialog
box is already visible, by simply doing additional CreateWindow calls.

The only problem under Windows 1.x was that there was no easy way to
control the ordering of the child windows, that is, the Tab key sequence.
Under Windows 2.0, the new SetWindowPos function takes care of this. There
are also new CreateDialogIndirect and DialogBoxIndirect functions, which
take a dialog template in memory instead of from a resource, providing
another way to build a dialog box on the fly. See Figure 1 for a Windows
2.0 coding example.


Pascal vs. C Calling Conventions

Dear Dr. Bob,

Why do OS/2 functions use the Pascal calling convention? What are the pros
and cons of the C and Pascal calling conventions?──KT

In the Pascal calling convention the caller pushes the arguments onto the
stack and the function that is called removes the arguments from the
stack. Since there are multiple calls to the function, this replaces
multiple stack cleanups with a single stack cleanup. Consider the pseudo
code example shown in Figure 2.

You get the idea. It's marginally faster on the 8086 because you can embed
the pop into the ret──something like ret(1)──however, the speed improvement
is not noticeable.

The Pascal calling convention reverses the order of parameters; rather
than pushing right to left (as in C), you push left to right. The benefit
of the C calling convention is that you can use a variable number of
parameters. The benefit of the Pascal calling convention is that it
produces smaller code.

In general you want to use the Pascal calling convention for all functions
that don't have variable-length parameter lists so that you get smaller
code. Microsoft uses the Pascal calling convention extensively in its own
products for this reason.

It's simple to declare which calling convention you are using with
Microsoft(R) C because the header files that contain the prototypes for the
libraries declare the functions CDECL so that you can use the compilation
switch that says "assume Pascal calling convention for all functions" and
the libraries will still work. Your program will contain a mixture of
Pascal calls for all the functions you wrote and C calls to the library
routines.


Pragmas and Function Prototypes

Dear Dr. Bob,

I am a relative newcomer to C programming. Although I have a fairly good
understanding of the language, I've been confused recently by several new
terms introduced by the ANSI C committee. First, what is a pragma? I
remember reading about pragmas in a book on ADA, but now I keep seeing
references to them in relation to C. Second, what are function prototypes
and why are they needed?──RJW

Welcome to the wonderful world of standards, where things we're used to
always change. A pragma, as defined in ADA, is simply a compiler
directive. In C, pragmas selectively enable or disable certain features of
the compiler. The programmer can specify, within the code listing itself,
where to turn compiler options on and off. For example, in Microsoft C
Version 5.0 there is the loop_opt pragma, which is used to control loop
optimization locally. The programmer might globally invoke loop
optimization at the compiler command line (cl /Ol in this case) and then
locally control which loops should be optimized (see Figure 3). Figure 4
is a list of the pragmas that are available in Microsoft C 5.0.

Function prototypes are function declarations that include a list of the
names and types of formal parameters associated with the function. A
function prototype establishes the name, return type, and storage class of
a function. A function prototype declaration precedes the actual
definition of a function and is used by the compiler for argument type
checking, to establish the return type of a function that is of a type
other than int, to initialize pointers to functions before those functions
are defined, and to reflect that a variable number of arguments, or no
arguments, will be passed. Figure 5 illustrates the use of function
prototypes. The prototype for printf is predefined. The use of the ellipse
indicates a variable number of arguments. The prototype for addreals
indicates that there is a return value of double, rather than a default
value of int. The prototype declaration for addreals informs the compiler
of this, which allows addreals to be called before it is defined. The
forward declaration also establishes the function argument types, allowing
for type checking. It is highly recommended that all new code use function
prototyping.


Figure 1:  Windows 2.0 Coding Example

/* Add an edit control to hwndDlg, giving it a dialog ID of D_NEWITEM.
 * Place it right after the existing item whose ID is ID_OTHERITEM.
 */

        hwndChild = CreateWindow("Edit", "",WS_CHILD | WS_TABSTOP |
                                 WS_BORDER,X, Y, nWidth, nHeight,
                                 hwndDlg, ID_NEWITEM, hInstance,
                                 NULL);

        if( ! hwndChild ) {
        /* couldn't create it - out of memory! */
        }

        SetWindowPos(hwndChild, GetDlgItem( hwndDlg, ID_OTHERITEM ),
                     0, 0, 0, 0,SWP_NOMOVE | SWP_NOSIZE |
                     SWP_SHOWWINDOW);


  Figure 2

foo:
    push ax
    call goo
    pop ax
                                    ∙
                                    ∙
                                    ∙
    push ax
    call goo
    pop ax
                                    ∙
                                    ∙
                                    ∙
    push ax
    call goo
    pop ax
                                    ∙
                                    ∙
                                    ∙
    ret

goo:
    ret

versus

foo:
    push ax
    call goo
                                    ∙
                                    ∙
                                    ∙
    push ax
    call goo
                                    ∙
                                    ∙
                                    ∙   ; no pop - saved a byte
    push ax
    call goo
                                    ∙
                                    ∙
                                    ∙
    ret

goo:
    pop ax
    ret


Figure 3:  Example of Pragma Usage

Command line invokes global loop optimization: cl \Oal optimum.c

/* optimum.c  */
                                    ∙
                                    ∙
                                    ∙
main()
{
                                    ∙
                                    ∙
                                    ∙
#pragma loop_opt(off)
/* turn loop optimization off */
                                    ∙
                                    ∙
                                    ∙
#pragma loop_opt(on)
/* turn loop optimization back on */
                                    ∙
                                    ∙
                                    ∙
}


Figure 4:  Pragmas Available in Microsoft C 5.0

Pragma       Local Effect

loop_opt     Turns loop optimization on and off.

pack         Specifies packing alignment for structures.

intrinsic    Specifies which functions are compiled as intrinsic functions.

function     Specifies which functions are compiled as standard function
             calls.

same_seg     Tells the compiler to assume that specified variables are
             allocated in the same far data segment.

alloc_text   Specifies modules to be grouped into a specified far code
             segment.

check_stack  Turns stack checking on or off.


Figure 5:  Example of Function Prototyping

/* Function prototyping */

#include <stdio.h>

/* stdio.h contains a function prototype for printf:
 *      int printf(const char *format[,argument]...);
 */

main()
{
        float a;
        float b,c.d;

        double addreals(double x, double y, double z);
        /* function prototype declaration for addreals */
                                    ∙
                                    ∙
                                    ∙
        a = addreals(b,c,d);

        printf("\t%f\n \t%f\n \t%f\n \t%f\n ",a,b,c,d);

}

        double addreals(double x, double y, double z)
        /* function definition for addreals */
        {
                                    ∙
                                    ∙
                                    ∙
        return(x+y+z);
        }


════════════════════════════════════════════════════════════════════════════


Vol. 2 No. 5 Table of Contents


Microsoft(R) Excel for Windows: Meeting the Demands of a New Generation

Designed to take full advantage of the latest generation in personal
computers, Microsoft Excel for Windows offers a host of advanced spreadsheet
functions, Lotus(R) 1-2-3(R) compatibility, and perhaps most interesting to
developers, a powerful macro language for custom applications.


Interprogram Communication Using Windows' Dynamic Data Exchange

Microsoft(R) Excel supports Windows Dynamic Data Exchange (DDE), a public
message protocol. DDE allows concurrently executing Windows applications to
exchange information in real time. The article, and accompanying sample
program Maze, illustrate this process.


Designing for Windows: An Interview with the Microsoft(R) Excel Developers

Microsoft Excel for Windows is the most significant program to be developed
in the Windows environment to date. MSJ talks with the design team for a
behind-the-scenes look at the challenges involved in bringing the successful
Macintosh(TM) spreadsheet to Windows.


A Strategy for Building and Debugging Your First MS-DOS(R) Device Driver

At first glance, writing device drivers might seem to be a tricky and
confusing venture. Using the prototype communications program MDM_DRV as a
model, this article explains how to write a character device driver and
suggests some useful debugging tips and techniques.


Microsoft(R) C Optimizing Compiler 5.0 Offers Improved Speed and Code Size

The latest version of Microsoft's professional C compiler generates faster
code and includes improved development tools, an enhanced version of the
CodeViewer(R) debugger, a full library of graphic routines, new libraries
of routines for BIOS/DOS access, and comes with QuickC(TM).


Ask Dr. Bob


EDITOR'S NOTE

Past issues of MSJ have provided an inside look at some of the advanced
applications currently being developed by such companies as Reuters, Aldus,
and Plexus. The recent introduction of Microsoft(R) Excel offers PC users
one of the most important general purpose applications now available──one
that we believe is going to have a significant impact on applications
development in the 80286 and 80386 world. Microsoft Excel is also the first
full-scale Windows application program, making the Windows presentation
manager graphical interface a serious development environment.

With that in mind, we have opted to present a detailed view of Microsoft
Excel, one that explores what its introduction means to the applications
developer. Since it is a Microsoft product, we risk blowing our own
horn──but it also means we can give our readers a behind-the-scenes look
at the software development process that led to the PC version.

Sophisticated, windows-based applications such as Microsoft Excel have the
ability to communicate with each other in real time by taking advantage of
DDE-Dynamic Data Exchange──the communications protocol that is part of
Windows. Kevin Welch, another of our Windows experts, takes you on a tour of
DDE with his MAZE program. MSJ presents the full documented source code for
MAZE, which will provide significant insights into the workings of
DDE.

Though OS/2 and the Windows graphical interface are clearly trends for the
future, there is, of course, a major MS-DOS and XENIX(R) world out there.
This issue tackles the somewhat esoteric world of MS-DOS(R) device drivers.
Future issues will continue to provide thorough explorations into these
environments.

The next issue of MSJ continues its coverage of OS/2 with an in-depth study
of the Presentation Manager. In addition, Ray Duncan will begin his two-part
discussion of OS/2 bimodal device drivers──perhaps the most difficult piece
of the OS/2 puzzle for software developers to understand. Stay tuned.

All our source code listings can now be found on DIAL, CompuServe(R), BIX,
and two public access bulletin 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

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) 1987 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, Microsoft Press, Multiplan, CodeView, and the
Microsoft logo are registered trademarks of Microsoft Corporation. QuickC is
a trademark of Microsoft Corporation. IBM is a registered trademark of
International Business Machines Corporation. Apple is a registered trademark
of Apple Computer, Inc. Macintosh is a trademark of Apple Computer, Inc.
Hayes is a registered trademark of Hayes Microcomputer Products, Inc. Lotus
and 1-2-3 are registered trademarks of Lotus Development Corporation. AT&T
and UNIX are registered trademarks of American Telephone and Telegraph
Company. CompuServe is a registered trademark of CompuServe
Incorporated.

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

Microsoft Excel for Windows: Meeting the Demands of a New Generation

Jared Taylor☼

Microsoft(R) Excel for Windows is a completely rewritten version of the
best-selling spreadsheet program for the Apple(R) Macintosh(TM). A crucial
consideration in developing the PC version was to abide by an important
standard in the MS-DOS world: open architecture. In hardware, open
architecture has given the IBM(R) PC and compatibles great flexibility and
has enabled third parties to expand the basic machine in ways never imagined
by IBM. Likewise, open architecture in software should provide Microsoft
Excel with the same kind of flexibility.

Many of the features are specifically designed to make the program easy to
customize. The file format, for example, is fully documented and intended
not to be machine-specific. The macro language allows custom design of
menus, dialog boxes, and commands that are just as powerful as those built
into the program. Macros are the gateway to interprogram communications and
even permit access to user-written programs and Windows libraries. This
external access extends the Microsoft Excel environment far beyond its own
system of rows and columns. Users will gain from the advantages of open
architecture, but independent applications developers will benefit most from
it.

This article provides a look at features and explores a few of the
development possibilities of the Microsoft Excel macro language, with an
emphasis on custom routines, menus, and dialog boxes.


Appearance

Microsoft Excel's most obvious departure from spreadsheet convention is its
appearance. Although it is designed to run under Windows, it comes with a
run-time Windows module that lets you use it as a standalone program. In
either case, Microsoft Excel adds the rich, graphical features of Windows to
the usual spreadsheet environment of rows and columns.

Not only can you display different worksheets on different parts of the
screen, you can also control how a spreadsheet looks. For example, you can
vary the height of rows as well as the width of columns, so as to display
characters of any size. You can have up to four different font styles, such
as boldface and italic, in a single spreadsheet, and display them in any
color you choose. You can use color informatively as well as decoratively,
by highlighting negative numbers, say, in red. You can add shading and
borders to cells or entire ranges for dramatic emphasis. The visual result
is as different from conventional spreadsheets as the output of desktop
publishing programs is from that of word processors.


Spreadsheet Functions

Microsoft Excel lets you build analytical models of unequaled power and
flexibility within a very large matrix of 256 columns and 16,384 rows. It
has 131 built-in functions, 42 more than Lotus(R) 1-2-3(R), which is the
industry standard, and functions don't require a special leading character
like the at sign (@). Unusual functions found in Microsoft Excel include
PRODUCT(), which is analogous to SUM() except that it multiplies the values
referenced within the parentheses; FACT(), which returns the factorial value
of a reference; and GROWTH() and LOGEST(), which give values and parameters
of an exponential trend.

All functions can be quickly called up in alphabetical order in a function
menu. From there, it's easy to cut and paste them into formulas, eliminating
typos. You can custom design your own functions by using the program's macro
language. If you wanted to calculate the volume of a sphere, you could
design a custom function that returned volume as a function of radius, and
it would appear in the cut-and-paste function menu along with the built-in
functions.

Microsoft Excel gives 21 standard number and date formats for display
purposes. If these aren't enough, you can design your own to include special
symbols and display conventions. For financial reporting you can eliminate
the last three, six, or nine digits so as to display numbers in thousands,
millions, or billions.

Versatility of display extends to printing, as Microsoft Excel makes full
use of color and laser printers. With proper graphics and laser printers,
you can reproduce all the fonts, shading, and colors that the program
supports on screen. A tax preparation worksheet, for instance, could produce
reports that look identical to a 1040 form. Formatting commands permit you
to include automatic headers, footers, date and time stamps, titles, and
page numbers. When necessary, Microsoft Excel can print wide spreadsheets in
landscape orientation. Its preview feature takes the guesswork out of
printing complex documents, letting you look at screen images of a worksheet
page-by-page before you send it to the printer.

Microsoft Excel has built-in database commands similar to current popular
spreadsheets. Data records are made up of rows with single cells as fields.
You can have as many different databases in one sheet as you like, but
databases must fit into memory. You can sort by three simultaneous keys, as
well as find and extract records according to mathematical and Boolean
criteria. An innovative database command lets you display records as
"forms": fields appear stacked on top of each other in a window rather than
strung out in a row across the screen, which makes it easier to examine and
enter  records.


Taming Recalcs

Several pioneering steps have been taken to reduce the wait for
recalculation. First of all, an intelligent minimal recalc is used. Instead
of painstakingly recalculating every formula each time you make even a minor
change, the program recalculates only those formulas that are affected by a
change. If you do have a recalculation that takes time, this won't stop you
from continuing your work. As soon as you  hit a key, calculation will
suspend and won't start up again until you pause.


Eliminating Errors

Microsoft Excel offers several auditing tools to help keep complex models
free of errors. For example, you can annotate individual cells with
explanations of spreadsheet logic. Annotations can be of any length and are
entered and retrieved through windows that can be sized, moved, or hidden.

There is a tool to show both the dependent and antecedent cells for any
formula, which is essential to tracking spreadsheet references. You can
either list these in an information window or highlight them on the screen.

Even if you aren't thinking about maintenance or auditability while you
build a model, you can document the relationships in formulas after the
model is built. First, you would name the cells that contain the crucial
values in your model. Then, with the Apply Names command, you could
substitute names for any cell reference in any formula in the worksheet.
F15* B29 becomes a much more understandable Profits* Tax_Rate in any
formula that references those cells.

Moreover, the program's naming conventions make it particularly easy to give
cells useful names. For example, in a spreadsheet that shows monthly sales
by region, the data will be bounded by a top row of month labels and a left
column of region names. If you highlight this entire area and use the Create
Names command, every data cell automatically receives a unique name. The
cell at the intersection of the April column and the Southwest row will be
known by the name April, Southwest, and formulas that reference April and
Southwest alone will operate on the entire column or row. If you need to
analyze models on paper, you can print out a formula display rather than
current values.

There are seven different formula error values to help keep worksheets
honest. For example, #DIV/0! obviously means you have tried to divide by
zero, and #NAME? means you have used an incorrect name or one that Microsoft
Excel doesn't recognize. More specific error messages let you fix problems
more easily. A search-and-replace feature also aids troubleshooting. You can
locate strings in formulas or labels and replace them selectively or en
masse. And, if you should happen to make a mistake, you can snatch it back
with the Undo command.


Graphs

Microsoft Excel has 44 different predesigned charts based on seven basic
types. All can be customized at will, and you can store any chart formats
you design for future use. Since graphs are displayed in their own windows,
you can examine them while you are working with the underlying data.

For more sophisticated graphic  jobs, you can overlay one chart onto
another, so that you can put a lot of data into a single chart and even have
two vertical indexes. You could combine a high-low-close stock chart that
has a vertical scale from $30 to $50 with a volume chart for the same stock
that has a vertical scale from 2,000 to 30,000. Vertical scales can be
linear, logarithmic, or semilogarithmic, and you can print even the most
elaborate chart without leaving the program.

If necessary, charts can refer to data from more than one spreadsheet.
Series formulas that reference labels and data ranges link the spreadsheets
to the underlying data. For increased flexibility, you can edit series
formulas just as freely as any other formula.

If you find that you often work with the same combination of active
spreadsheets or graphs, you can save the whole thing in a bundle as a
"workspace." The names, relations, and settings of these different documents
are saved together in a small file. When you reopen the workspace, the
entire package is redisplayed as you left it.


Linking

Spreadsheet linking, one of the most powerful features of Microsoft Excel,
meets the needs of those users frustrated by the current generation of two-
dimensional spreadsheets. Links let you write formulas in a spreadsheet that
refer to values in other spreadsheets. The obvious use for external
references of this kind is in consolidations; the "roll-up" spreadsheet
would contain many references to the component, or subsidiary,
spreadsheets.

However links do serve other purposes. Since subsidiary worksheets are all
standalone models, different users can work with them simultaneously──which
is much handier than stuffing subsidiary models into a huge consolidation
model that  can be used by only one person at a time.

Another use for links is to display the same data in different ways. Often,
a single basic set of data must be put into reports that vary in detail or
layout. Instead of reconstructing the same data set for each report, links
can extract from the common data set only the necessary portions and display
them in a variety of formats.


Command Macros

Unlike 1-2-3 and most other spreadsheets now available, Microsoft Excel
stores macros separately from worksheets. Macro sheets have their own
characteristics but otherwise behave exactly like any other document created
by Microsoft Excel. By effectively storing "programs" separately from
"data," you can use the same library of useful routines on any number of
different worksheets. This also facilitates running routines that work on a
sequence of different worksheets──printing, formatting, or consolidation,
for example.

The Microsoft Excel macro language has 355 commands and statements. The
majority of these are command equivalents, which automate normal spreadsheet
functions. The SEARCH() and REPLACE() commands in the macro language, for
example, activate the spreadsheet commands of the same name. All of the
display features, such as mixed fonts, variable row height, color, and
multiple windows, can be controlled with a series of eight different format
commands, included in which are FORMAT.TEXT() and FORMAT.SIZE().
MAIN.CHART() specifies the elements from predefined chart types. In all
cases, the syntax of the macro command is as close as possible to that of
the spreadsheet command.

You'll hardly ever have to write these commands, though, because the macro
recorder does it for you. Turn on the recorder via the Macro menu, enter
commands, and Microsoft Excel records them automatically as macro script.
Even if you make typing mistakes, the recorder writes script to produce your
final result rather than recording canceled menu choices or characters typed
and then deleted.

The recorder runs in two different modes. In relative mode, cursor movements
are recorded from the original cursor position; the macro will play back
differently if you start it with the cursor in different positions.
When running in absolute mode, cursor movements are recorded by cell
address, and the macro runs identically no matter how you start it. You can
switch between modes any time during the recording. If you need to, you can
stop recording entirely, while you do other spreadsheet work. Then, when you
start again, you can choose either to add keystrokes to the first macro or
start a new one. Keystroke recording is the easiest method for building the
structure of a macro, since you can always edit it later.

There are two ways to run a macro from the keyboard. You can choose the
Macro Run command from a menu and then pick the macro's name from a list.
Even if the macro has no name, if you know the cell in the macro sheet where
it begins, you can enter an address at the Run command. A quicker way is to
start a macro with a Ctrl-key combination by assigning macros to any
alphanumeric key. Micosoft Excel distinguishes between plain keys and
shifted keys, so it is possible to have different macros running on, say,
Ctrl-P and Ctrl-Shift-P.


Advanced Macros

There are three kinds of macro functions that do more than just duplicate
commands: control functions, customizing functions, and value-returning
functions. Control functions, such as FOR(), NEXT(), GOTO(), and WHILE()
govern macro execution. Customizing functions let you write menus and dialog
boxes. Value-returning functions, which return information based on
arguments, are the most similar to regular spreadsheet functions. Some are
macro equivalents of normal spreadsheet functions like AVERAGE(), AREAS(),
SUM(), or ASIN(). Others are exclusive to macros. OFFSET(), for example,
tells you the address of a cell or cells that are so many columns across and
so many rows up (or down) from another cell. LINKS() returns the names of
all documents that the current sheet is linked to. The nature of value-
returning functions is similar enough to normal spreadsheet functions to
allow you to cross over. You can write value-returning functions of your own
that work just like spreadsheet functions.

Figure 1 shows a number of macros that all conform to standard Microsoft
Excel notation. The first line, which contains the name of the macro and the
Ctrl-key combination that runs it, is optional. All statements and commands
begin with the equal sign, which indicates a formula, and all of the macros
end with =RETURN(). This returns control to whatever process actually
started the macro, be it keyboard input or a sub-routine call.

Macro sheets display formulas rather than values, which is the opposite of
normal Microsoft Excel worksheets. However, just as a worksheet cell might
display the value 5 but contain the formula =sum(2,3), cells in macro sheets
contain underlying values for their formulas. Formulas that return a value
contain that value. Others, like command-equivalent macros that wouldn't
normally return a value, have the value FALSE before they execute and TRUE
after they have executed successfully. If they have been run but failed to
execute, they have the value FALSE or an error value.

This value-displaying character of macro script has two important functions:
the value can be very useful in debugging macros because it gives you clues
to why a macro failed, and macros store user input and returned values in
the cells that contain the script, not someplace else.

The first macro in Figure 1 is an example of this. The INPUT statement
produces a simple dialog box (see Figure 2☼) with the quoted string as a
prompt. The second argument, 1, specifies that the prompt will accept only
numbers, but you can specify many different acceptable data types. When
INPUT executes and gets a response from the user, the value of the response
becomes the underlying value of the cell containing INPUT. This explains
why the COLUMN.WIDTH command in the line below takes, as its argument, the
cell address B2, which is where the value is.

The second macro sends an alert message (see Figure 3☼). Since this type of
alert doesn't give the user any chance to reply, you'd use it for purely
informational messages that don't have to be acted on. The ALERT() statement
takes a message string as its argument, as well as an indicator of the type
of alert. (There is no second argument in this macro, but there is in macro
14.) Here the quoted text is combined by means of the ampersand with a name
(name_3), which is a constant. The constant happens to be 10, but it could
be a value, a cell address, or something else. The exclamation mark before
the name means that the macro looks for the name in the currently active
sheet. Without it, the macro would look for name_3 in the macro sheet by
default.

Macro 3 uses the powerful DISPLAY command to change the display in the
active sheet. It displays formulas instead of values, turns off grid lines,
leaves headings on, displays zero values, and paints the row numbers and
column letters red. (Red is the third of eight different color choices.)
Macro 4 returns the display to normal.

Macros 5 and 6 enter  formulas directly into a worksheet cell. In macro 5,
for example,  the formula goes into cell B6 in the current sheet (because of
the exclamation point) and sums a range. The first argument must be quoted,
and in this case uses the alternate-cell addressing convention of Microsoft
Excel, which is compatible with Multiplan(R). In conventional notation, the
summed range is B2:B4. The next macro uses the same notation to enter a
formula one column over and one row down from the cursor.

Macro 7 returns information about the currently open document. Its argument
can be a number from 1 to 26, and each number returns different information
about the document: document type, path of file, numbers of the first or
last rows used, fonts used, protection status, type of chart, and so on. In
this case, the value returned is TRUE if changes have been made to the
worksheet since it was last saved and FALSE otherwise. This type of
information is invaluable for applications developers.

Macro 8 is unlike the others in Figure 1 in that it is a function macro.
Once it's written, you can use it in a worksheet as you would any other
built-in function. You would distinguish it from the other macros by
indicating that it is a function macro when you named it.

Function macros have a simple structure. They state the arguments required
for the function and then return a value based on those arguments. In macro
8, there is only one argument, the radius of a sphere. The number 1 means
the argument must be a number. RETURN cubes this value and multiplies by pi
to give the volume of the sphere. As long as the sheet containing this macro
is open, you can use the Sphere function in any worksheet.

Macros 9 and 10 work together: macro 9 uses the ON.TIME command to execute
macro 10 at a certain time. In this case, the time of execution is one ten-
thousandth of 24 hours from NOW(). When that time is reached, the routine
named Time (macro 10) in the macro sheet named SJT.XLM (the sheet containing
all these macros) executes. It pops up an alert box that tells you your
toast is done, but it could say anything.

The ON.TIME command has an optional argument that lets you specify how long
the subordinate macro should wait before executing, in case the program is
running a different  macro, displaying an error message, or doing anything
else that would keep it from running a macro. If you leave that argument
out, the subsidiary macro then waits as long as it has to before it takes
control.

The next pair of macros also governs conditional execution. Macro 11 remaps
the Home key so as to run the macro named Home (macro 12) in SJT.XLM. The
Home routine displays an alert box and then remaps the Home key to its
normal function by using the ON.KEY command without an argument. An ON.CLICK
macro that could redirect mouse clicks would be handy, but there is no such
thing, nor is there a way to use more than one mouse button.

The last three macros can run only if there are other applications running
under Windows Version 2.0. Macros 13 and 14 run a routine depending on
whether data has changed in another worksheet. The ON.DATA command causes
the macro named Data (macro 14), in macro sheet SJT.XLM, to run whenever new
information is received via a Dynamic Data Exchange (DDE) link in a
different sheet, named STOCK.XLS. When Data runs, it puts up an alert box
that refers to the cell in STOCK.XLS that holds the value of IBM stock. The
last argument in the alert, the number 1, indicates that the user has a
choice of OK or Cancel. These responses govern the TRUE/FALSE value of the
cell containing the ALERT command. If it is FALSE, the new ON.DATA command
deactivates the previous one by calling it without a macro argument. A
routine like this could take some kind of action if the user chose OK and
gave the ALERT cell the value TRUE.

The routine could, for instance, run a macro like macro 15, which uses the
EXEC command to start up a communications program under Windows 2.0 and the
SEND.KEYS command to start commands in that application as if they were
typed at the keyboard. EXEC's arguments are the name of the app and the type
of window it is to run in; the number 1 specifies a normal, full-sized
window. The macro waits three seconds for the terminal app to load and then
sends a series of keystrokes, which follow a certain syntax: the percent
sign means the next key, or all keys in parentheses, are Alt-shifted,
nonalpha keys are in curly braces, and so on. The final argument is a wait
log which, when TRUE, suspends macro operation until the sent keys are
processed in the terminal app. When FALSE, the macro just keeps running.

The EXEC/SEND.KEYS combination is obviously a powerful way for applications
developers to link programs. Microsoft Excel can load, run, close, and read
the output from any program that runs under Windows 2.0. However, compared
with DDE, these are simpleminded routines that do no more than mimic
operator keystrokes. DDE is a much more elegant means of interprogram
communication, as it allows Windows 2.0 apps to run both as servers and
clients and to automatically establish two-way links. One program can have
more than one active DDE link at a time, and all programs have access to the
command primitives of the others and can share memory objects.

Two unusually powerful macro commands are REGISTER and CALL, which run
Windows library routines or your own C or FORTRAN routines from within
Microsoft Excel. REGISTER defines the procedure you want to run and
specifies the number and data types of the procedure's arguments; it returns
a text value. CALL uses this value as one of its arguments, along with the
arguments you want to pass to the procedure that is to run. You cannot use
CALL without also having returned a value with REGISTER.

The most likely use of REGISTER/CALL would be to run a custom-designed
spreadsheet function that was too involved or complex to be efficiently
written as a function macro. However, REGISTER and CALL actually push and
pop the command stack, so if you use them incorrectly they can blow up your
application.


Menus and Dialog Boxes

The macro language has a series of commands that let you design custom
command menus and dialog boxes. These work just like the ones designed into
Microsoft Excel, so users will be perfectly comfortable with them. Figure 4
shows the macro script for a fairly detailed custom menu, and Figure 5☼
shows how this menu would appear on the screen. Menu building follows a set
macro pattern as in the initialization macro in cells D53 to D60. The first
procedure defines the new menu bar; then the text and macro references for
the different menu choices are defined. This information, indicated by cell
range addresses like D8:G16, is all referenced to the ADD.BAR command in
cell D53. The new menu bar is not displayed until the macro executes the
SHOW.BAR command in the last line, and the reference to D53 activates the
entire menu.

The structure of the menu references themselves is simple. Each ADD.MENU
command specifies a menu item on the menu bar, and the cells within the
specified range are the choices available in the drop-down box under that
menu item. An ampersand in the name of the choice means that the letter that
follows is underlined in the menu, and you can select that choice by hitting
the letter on the keyboard. A single dash in a menu just draws a line to
separate different menu choices. Column E contains references to macros that
are executed when the user makes a choice. Column G is information about
each menu choice that appears.

The full gamut of menu control is available to applications developers. For
example, you can make a menu item gray to show that it isn't currently
available. When a menu item displays choices that the user can toggle on or
off, you can display the choice with or without a check mark beside it,
depending on its status. You can even make changes to standard menu bars.
If, for example, you were designing an application that might jump the
tracks if the user had access to the windowing commands, you could remove
them from the program menu.

In this application, one of the menu options lets the user sort data. Since
there are many different ways to sort the data, there must be a way for the
user to make choices. The solution is to use the macro script in Figure 6
to produce the custom dialog box in Figure 7☼.

The DIALOG.BOX command calls the box in much the same way as the menu
commands call a menu, but it does so in a single step. In Figure 6☼, the
macro in I26:I28 specifies the dialog box reference in B3 to H24 and
displays the box immediately. The numbers in column B identify the type of
item in the dialog: list box, text box, option button, and so on. The next
four columns specify the vertical and horizontal location and height and
width of the item. Column H is essential; user choices are stored here. The
text strings in I10 to I18 are referenced in two of the choices and are
options for the user (see Figure 7☼).

In designing a dialog box, it's a bother to have to come up with all the
numbers in columns C through F for location and size. If you leave them out,
Microsoft Excel tries to arrange the contents of the dialog box for you, but
the results are not ideal. Microsoft is working on a developer's utility to
let you design dialog boxes by drawing them directly on the screen.

All by itself, the script in Figure 6 will display the dialog box in
Figure 7☼ and record user choices in column H. However, in order for these
choices to mean anything, they have to be referenced by macros that evaluate
them.


Security

No matter how sophisticated an application is, it can fall on its face if
users can tamper too easily with the underlying scripts. Microsoft Excel
offers several ways to secure your applications. First, although a macro
sheet has to be in memory to operate, it can be hidden or reduced to an
icon. Most inexperienced users won't think to look for hidden windows, and
if you remove the windows command from the menu bar, it will make tampering
even harder.

Macros can normally be aborted by hitting Escape, but you can remap the
Escape key to keep routines secure. Your application will always be
vulnerable when you hand the screen over to the user for data entry. The
simplest corrective here is to protect worksheets and allow data entry only
in unprotected cells. If necessary, you can remove the protection commands
from the menu.

If you need even greater protection, you can avoid turning the screen over
to the user even for data entry. For virtually bulletproof apps, you can
funnel all user input through dialog boxes. If your program files are
password-protected and are called from a hidden macro file containing a
camouflaged password, you'll be able to keep most users out. Even so, any
security measures that you design can probably be defeated by anyone who
tries hard enough.


Turning Point

The release of the PC version of Microsoft Excel is a turning point for
Microsoft and the world of MS-DOS(R). Microsoft Excel is the first full-
scale Windows application program, making the Windows graphical interface a
serious development environment.

This article has touched on only a few of the commands and options available
to applications developers. The menu/dialog box combination in Figures 4
through 7 is typical of the manner in which sophisticated commands can be
made available to inexperienced users through custom programming. Combined
with the inherent power of Microsoft Excel, the flexibility of the macro
language, and the means to communicate with other programs, this open
software architecture should make Microsoft Excel a very solid platform for
applications development.


Figure 1:  Sample Macros

╓┌────┌─────┌────────────────────────────────────────────────┌───────────────╖
     A     B                                                 C

1    1     Column_Width (b)                                  10
2          "=INPUT(""Column Width?"",1)"
3          =COLUMN.WIDTH(B2)
4          =RETURN()
     A     B                                                 C
4          =RETURN()
5
6    2     Alert_Names (e)
7          "=ALERT(""Total is ""&!name_3)"
8          =RETURN()
9
10   3     Display Macro (c)
11         "=DISPLAY(TRUE,FALSE,TRUE,TRUE,3)"
12         =RETURN()
13
14   4     Display Macro (d)
15         "=DISPLAY(TRUE,TRUE,TRUE,TRUE,1)"
16         =RETURN()
17
18   5     Formula (h)
19         "=FORMULA(""=sum(r2c2:r4c2)"",!$B$6)"
20         =RETURN()
21
22   6     Formula (l)
23         "=FORMULA(""=test"",""R[1]C[2]"")"
     A     B                                                 C
23         "=FORMULA(""=test"",""R[1]C[2]"")"
24         =RETURN()
25
26   7     Getdoc (G)
27         =GET.DOCUMENT(4)
28         =RETURN()
29
30   8     Sphere
31         "=ARGUMENT(""radius"",1)"
32         =RETURN(radius^3*PI())
33
34   9     On_Time (s)
35         "=ON.TIME(NOW() +0.0001,""sjt.xlm!Time"")"
36         =RETURN()
37
38   10    Time
39         "=ALERT(""Toast is done"")"
40         =RETURN()
41
42   11    On_Key (r)
     A     B                                                 C
42   11    On_Key (r)
43         "=ON.KEY(""{HOME}"",""sjt.xlm!Home"")"
44         =RETURN()
45
46   12    Home
47         "=ALERT(""you pressed the Home key"")"
48         "=ON.KEY(""{HOME}"")"
49         =RETURN()
50
51   13    On_Data (t)
52         "=ON.DATA(""Stock.xls"",""sjt.xlm!Data"")"
53         =RETURN()
54
55   14    Data
56         "=ALERT(""IBM is now at ""&'A:\STOCK.XLS'!$C$2,1)"
57         "=IF(B56=FALSE,ON.DATA(""Stock.xls""))"
58         =RETURN()
59
60   15    Terminal (v)
61         "=EXEC(""terminal"",1)"
     A     B                                                 C
61         "=EXEC(""terminal"",1)"
62         =WAIT(NOW)() +3/86400)
63         "=SEND.KEYS(""%{f10}"",TRUE)"
64         "=SEND.KEYS(""%(fo)c:*.*~"")"
65         "=SEND.KEYS(""%(ft){ESC}"",TRUE)"
66         "=SEND.KEYS(""%(cc)9600%c{right}~"",TRUE)"
67         "=SEND.KEYS(""%(fb)"",TRUE)"
68         =RETURN()


Figure 4:  Macro Script for a Sample Custom Menu

╓┌───┌───────────────────────────┌─────────────────────────┌─────────────────►
    D                           E                         G

7   Custom menu Bar ref
8   &Employee Records                                     "Add, delete, save
9   &New Employees              Payroll.XLM!Add           Add a new employee
10  &Retire Employee            Payroll.XLM!Delete        Remove an employee
11  &Save Records               Payroll.XLM!Save          Save database
    D                           E                         G
11  &Save Records               Payroll.XLM!Save          Save database
12
13  Page &Setup                 Payroll.XLM!PageSetup     Adjust output layou
14  &Print Records              Payroll.XLM!Print         Print database
15
16  &Quit . . .                 Payroll.XLM!Quit          Quit to Microsoft E
17
18  &Employee Data                                        Edit and find data
19  Change &Address             Payroll.XLM!ChangeAddress Change address of s
20  Change &Rate                Payroll.XLM!ChangeRate    Change pay rate of
21  Change &Hours Worked        Payroll.XLM!ChangeHours   Change number of ho
22  &Find employee . . .        Payroll.XLM!FindEmp       Find an employee
23
24  &Organize                                             "Edit layout, organ
25  &Hide field                 Payroll.XLM!Hide          Hide a data field
26  &Un-hide field              Payroll.XLM!UnHide        Unhide a data field
27
28  &Sort                       Payroll.XLM!Sort          Sort employee
29
30  Re&port                                               Choose pre-defined
    D                           E                         G
30  Re&port                                               Choose pre-defined
31  &Employee List              Payroll.XLM!EmpList       List with all data
32  Pe&rsonal Data List         Payroll.XLM!PerList       "List with only nam
33  &Wage List                  Payroll.XLM!WageList      "List with only nam
34  &Phone List                 Payroll.XLM!PhoneList     List with only name
35
36
37  &Format                                               Edit display option
38  &Dollar signs               Payroll.XLM!Numbers       Dollar signs on mon
39  &Borders around employees   Payroll.XLM!dbBorders     Outline of employee
40  Borders around &summary bar Payroll.XLM!SumBorders    Outline of summary
41  &Gridlines                  Payroll.XLM!Gridlines     Database gridlines
42
43  &Alignment . . .            Payroll.XLM!Alignment     Data alignment
44  &Font . . .                 Payroll.XLM!Font          Change font
45
46  &Info . . .
47  &Introduction . . .         Payroll.XLM!Intro         Introduction to Cus
48  &Original Bar               Payroll.XLM!Revert        Revert to original
49  &About . . .                Payroll.XLM!About         Display Program inf
    D                           E                         G
49  &About . . .                Payroll.XLM!About         Display Program inf
50
51
52  Initialize
53  =ADD.BAR()
54  "=ADD.MENU(D53,D8:G16)"
55  "=ADD.MENU(D53,D18:G22)"
56  "=ADD.MENU(D53,D24:G28)"
57  "=ADD.MENU(D53,D30:G34)"
58  "=ADD.MENU(D53,D37:G44)"
59  "=ADD.MENU(D53,D46:G49)"
60  =SHOW.BAR(D53)


Figure 6:  Macro Script for a Sample Custom Dialog Box

╓┌───┌───┌────┌────┌───┌──────┌───────────────┌───┌──────────────────────────╖
    B   C    D    E   F      G               H     I

2                                                │ SORT DIALOG BOX
    B   C    D    E   F      G               H     I
2                                                │ SORT DIALOG BOX
3                                                │
4   5        5               Sort Records        │
5   14  5    15   70  95     First Key           │
6   15  10   30   60  35     I10:I18         4   │
7   14  10   70   60  35     Sorting Order       │
8   11                                       1   │ Sort Dialog
9   12  15   82   50  10     Ascending           │ List Box ref
10  12  15        50  10     Descending          │ Last Name
11  14  80   15   70  95     Second Key          │ First Name
12  15  85   30   60  35     I10:I18         5   │ No. and Street
13  14  85   70   60  35     Sorting Order       │ "City, State"
14  11                                       2   │ Zip Code
15  12  90   82   50  10     Ascending           │ Phone Number
16  12  90        50  10     Descending          │ Hourly Rate
17  14  155  15   70  95     Third Key           │ Hours Worked
18  15  160  30   60  35     I10:I18         9   │ Amount Due
19  14  160  70   60  35     Sorting Order       │
20  11                                       2   │
21  12  165  82   50  10     Ascending           │
    B   C    D    E   F      G               H     I
21  12  165  82   50  10     Ascending           │
22  12  165       50  10     Descending          │
23  1   170  115             OK                  │
24  2   170  130             Cancel              │
25                                               │
26                                               │ Sort
27                                               │ =DIALOG.BOX(B3:H24)
28                                               │ =RETURN()

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

Interprogram Communications Using Windows' Dynamic Data Exchange

Kevin P. Welch☼

Late in 1985, Microsoft released Windows to the world. Since that time
programmers have been exploring the many capabilities that this new
environment offers. New concepts like module definitions, resource scripts,
and dynamically linked libraries are becoming part of the everyday
vernacular of the Windows programmer.

One of Microsoft Windows' more exciting capabilities is that of interprocess
communication. In the past, MS-DOS(R) applications existed more or less as
islands unto themselves, seldom sharing information with one another in a
seamless and integrated fashion. With Windows, programs can now be written
to function as part of a much larger interconnected system that includes
both local and remote applications. The data from the PC down the hall and
from the mainframe at the corporate office can now be part of your
application.


Interprocess Communication

Windows accomplishes this level of interconnectivity using three distinct
mechanisms: the clipboard, shared memory, and messages. The clipboard lets a
user share a selected data object with all interested applications in the
system. In most cases the user manually copies the data object into the
clipboard (usually with a select-and-copy operation), followed by one or
more paste operations into participating programs. Certain applications that
are especially interested in the current clipboard contents can, at their
request, become part of the clipboard viewer chain. These programs are then
notified whenever the user makes any subsequent changes to the clipboard. In
general the clipboard is a temporary repository of information and is suited
only to transactions that require the manual involvement of the user.

Shared memory is another mechanism through which cooperating applications
can communicate. This is typically accomplished using either a dynamically
linked library (that normally has only one data segment) or commonly held
handles to global memory. Changes to the shared memory are available
instantly with very little overhead to all participating applications.
Unfortunately, most applications that use only shared memory for
interprocess communication are limited to data exchange within a group of
programs that were designed to work together.

The third, and perhaps most natural, interprocess communication mechanism
utilizes standard Windows messages: two or more applications send messages
notifying each other that data is available and provide some means for
obtaining it. In most cases the actual data is passed through the clipboard
or by means of a shared global memory handle. As one can imagine,
cooperating applications must recognize the messages and process each of
them according to a predefined protocol.

One refinement of message-based interprocess communication is DDE (Dynamic
Data Exchange). DDE is a published message protocol for the exchange of data
between Windows programs; it consists of a small set of messages and data
structures designed to support this interaction. Any Windows application
that has a window and a message-processing loop can be modified to accept
these messages, allowing it to participate in a DDE conversation.

Because DDE is a published protocol, software developers wishing to use it
can do so in their applications. Applications written to support DDE will
then be able to participate in an integrated desktop, sharing data with
other applications in the Windows environment.


Uses of DDE

DDE is best suited for situations that require user-independent interprocess
communication. In most cases the user would be expected to establish links
manually, but from then on each application would share data independently
either as it became available or on specific request. Figure 1 lists some
of the many possibilities that exist for using DDE.


DDE Overview

First and foremost, DDE is a message protocol. No changes to the Windows
environment are necessary in order to use this standard. Each application is
responsible for implementing the message protocol and for managing the
conversations in which it participates.

The communications model used by DDE follows the client-server paradigm. In
this model the client application initiates a conversation with one or more
servers. When the server application decides to participate, it acknowledges
the client, and the conversation is established. Note that data is not
automatically transmitted once the server acknowledges the client. For this
to happen, the client must request a one-time transfer of data, or advise
the server that it wishes to be provided with a specific item of information
on a periodic basis. You will note that data flows from the server to the
client and continues until either application decides to terminate the
conversation.

A DDE conversation is established by utilizing a special set of predefined
Windows messages. These messages are transferred between applications using
the standard Windows SendMessage and PostMessage commands. The general
syntax used (described here for the PostMessage case) is shown in Figure 2.

The lParam portion of the message usually consists of an atom combined with
a data handle or a status word. The exact composition of this parameter
depends on the type of message being sent. For those of you unfamiliar with
atoms, an atom is an integer number that uniquely identifies a character
string. The character strings are stored in an atom table either on a local
or global basis. Locally stored character strings result in atoms that can
be used only within one instance of an application. On the other hand,
globally stored character strings produce atoms that can be used throughout
the entire Windows environment. This enables two cooperating applications to
share the same character string in a consistent fashion. Note that in
Windows Version 2.0x, global atom management services are supplied by
Windows itself; in Version 1.0x, these services are made available through
calls to the dynamically linked library DDE.EXE.

The functions listed in Figure 3 are present in Windows 2.0 and facilitate
the creation, inspection, and deletion of global atoms. The set of messages
shown in Figure 4, each with its associated lParam definition, comprises
those used in the DDE protocol. The low and high words are combined into a
double word with the MAKELONG macro defined in WINDOWS.H.

The various atoms (normally indicated by a lowercase "a" before the variable
name) are used to reference the type of data supplied/wanted or the type of
conversation desired. The hData, hCommands, and hOptions words are handles
to global data structures (created using calls to GlobalAlloc); they contain
the physical data being passed. In cases where data is being requested or
acknowledged, the cfFormat word is used. One of the predefined or registered
clipboard data formats, cfFormat enables the client application to request
information in a specific rendering.

The atom portion of each message is used to implement a three-level
communications hierarchy: application, topic, and item. The application atom
is used when a conversation is being initiated and specifies the name of the
program that is to respond. If the application atom is NULL, any interested
application can respond. The topic refers to a logical data context. Topics
can consist of filenames (for those applications that are file-based) or any
other program-specific character strings that represent related groups of
data. Finally, an item refers to a single data object that can be
transmitted using a DDE exchange. An item could refer to a single integer, a
character string, or even to a more complex data structure such as a bitmap
or metafile. DDE conversations are established using the application and
topic atoms. It is within this context that the client asks the server to
furnish real-time data for a particular data item.

To illustrate this communications hierarchy, consider a Microsoft Excel
spreadsheet user who wishes to track the price of several computer stocks on
the New York Stock Exchange. The user has access to a Windows application
called QUOTE that in turn has access to NYSE data (NYSE). The DDE
conversation between the spreadsheet and the stock quotation program would
proceed according to the following discussion.

The user initiates the conversation by supplying the name of the server
application and the particular topic of interest, then uses the resulting
communication channel to request quotes on specific stocks. In the example
given, the following application and topic would be broadcast to all
interested applications:

  application|topic
  QUOTE|NYSE

Upon receiving notification of this request, the stock quotation application
sends a positive acknowledgment back to the spreadsheet. By means of a
spreadsheet formula, the user can then request to be informed whenever a
particular stock quotation changes. For example, to be notified whenever a
change in the selling price of IBM, Digital Equipment, and Hewlett-Packard
stock occurs, the user need simply define three separate data items and
request the QUOTE application to periodically advise the spreadsheet. These
data items may be given as:

  item
  IBM
  DEC
  HP

In actual practice, the spreadsheet user would probably initiate the
conversation and define the real-time remote references for three separate
cells using the following formulas:

  = QUOTE | NYSE ! IBM
  = QUOTE | NYSE ! DEC
  = QUOTE | NYSE ! HP
Once some of the building blocks that make up DDE are understood, it becomes
easier to explore the actual message protocol and investigate how to
implement this scheme in a Windows application.


Initiating a Conversation

As mentioned earlier, two participating applications must engage in a DDE
conversation prior to the real-time exchange of data. The program initiating
the conversation is regarded as the client application, while the program
responding to the client and furnishing the data is known as the server
application.

Both the client and server applications must possess a message queue and
must contain a separate window for each conversation they engage in. The
reasons for these requirements are that in order to receive a message, an
application needs a message-processing loop, and also, that DDE messages are
sent between two cooperating client and server windows. Applications that
need to engage in more than one conversation normally define several hidden
windows whose express purpose is to process DDE messages. The resulting
window handle then uniquely identifies the conversation.

A DDE conversation is initiated by the client application using a
SendMessage in the following format. Note that the "──1" window handle will
cause the initiate message to be sent to all active windows (including the
client window itself):

  SendMessage( (HWND)-1, WM_DDE_INITIATE,hMyWnd,MAKELONG(aApp,aTopic));

The SendMessage will not return until all the receiving applications in the
sequence either process or ignore the initiate message. These applications,
after examining the aApp and aTopic atoms, can respond with a positive
acknowledgment if interested in participating in a conversation. The
pseudocode fragment in Figure 5 illustrates the kind of code present in a
server application that responds to an initiate message.

Note that both the client and server windows use the Windows SendMessage
command when part of an initiate sequence. This is the only case in the DDE
specification where SendMessage is used. In all other situations a
PostMessage command is issued. The reason why SendMessage is used in this
case is so that the client application can guarantee that all interested
servers have responded before it proceeds with subsequent initiation steps.
If a PostMessage command was sent, the client would have no way of knowing
when to expect a response to its initiate request.


Permanent Data Links

Once a DDE conversation has been established, the client application can
advise the server that it is interested in periodic updates on a particular
item of data. This "link" constitutes a permanent data stream (or "hot
link") and continues either until it is explicitly halted or until either
application terminates. Note that the initiation of a conversation does not
automatically create this data link: the client must explicitly ask the
server to send information as it becomes available.

To start this data stream the client application advises the server that it
wants periodic updates on a particular data item. Besides providing an atom
that represents the data item, the client defines a special global memory
data structure that specifies how the data is to be transmitted and in what
format it is to be sent. The code fragments in Figure 6 demonstrate how the
client can advise a server.

The Options data structure (defined as part of the DDE specification)
determines if the server expects an acknowledgment to each data item, how
the data is formatted, and so on. The server is obliged to respond with
either a positive or negative acknowledgment to each advise message. The
response enables the client to verify whether the server is truly acting on
the advise notification.

In situations where the server cannot supply the data in the requested
format, the server responds with a negative acknowledgment. After receiving
this acknowledgment, the client can then ask the server to furnish the same
data item in a different format. This process may be repeated until a format
is found that both applications can accept.

Once the data link is established, the server notifies the client whenever
the data changes. If the fNoData flag (part of the options data structure)
is true, the server simply informs the client but does not render the
information. The client application, at its discretion, can then request the
latest version of the data by performing a regular request transaction. In
the normal case the server allocates a block of global memory, formats the
information, then informs the client through a data transaction. The code
fragment in Figure 7 illustrates this process.

Upon receiving the data transaction, the client is expected to extract the
desired information and respond to the server according to the defined
option flags──a process that may result in releasing the global memory and
removing the atom that referenced the data item. This removal prevents the
accumulation of unused portions of memory and incorrect atom reference
counts. In addition, depending on the kind of transaction, the client may be
expected to acknowledge the server.

The data link between the two continues until either the server or the
client application terminates or until the client requests the data flow to
be stopped. To stop the data flow, the client performs an unadvise
transaction with the server for a particular data item, a process that is
normally accomplished using a code fragment such as the following:

  /* define data atom */
  aItem = GlobalAddAtom((LPSTR)"DataItem" );
  /* turn off data flow */
  PostMessage(hClientWnd,WM_DDE_UNADVISE,hMyWnd,MAKELONG(NULL,aItem));


One-Time Data Transfer

The DDE specification includes two separate mechanisms through which one-
time data transfer can be accomplished. The first of these is the request
transaction, in which the client asks the server to supply a specific data
item. This is done through the following code fragment:

  PostMessage(hServerWnd,WM_DDE_REQUEST,hMyWnd,MAKELONG(cfFormat,aItem));

If the server has access to the specified item and can render it in the
requested format, it supplies the information using a normal data
transaction. If the server is unable to satisfy the request (for any
reason), it responds with a negative acknowledgment.

The second mechanism, called a poke transaction, is a one-time transaction
in which the client sends data to the server. In this case the client
renders the data in a format of its choice and notifies the server. The code
that performs this transaction is very similar to that which transmits real-
time data using the WM_DDE_DATA message. The only exception is that in this
transaction a WM_DDE_POKE message is used.

The server is expected to reply with a positive acknowledgment if it is able
to accept the data. In cases when the server is unable to process the data
due to format or some other reason, it responds with a negative
acknowledgment. These acknowledgments take the following form:

  PostMessage(hClientWnd,WM_DDE_ACK,hServerWnd,MAKELONG(wResponse,aItem));


Remote Command Execution

One of the more interesting DDE transactions is the remote execution of a
series of commands by another application. Remote command execution is
accomplished using the execute message. The client formats a null-terminated
command string and transmits it to the server. The command string syntax
proceeds according to the rules listed in Figure 8. The examples in
Figure 9 demonstrate how this syntax can be used to generate meaningful
command sequences.

The client application is responsible for allocating and defining the block
of global memory that contains the text command sequences. The resulting
data handle is then passed to the server using the following code fragment:

  PostMessage(hServerWnd,WM_DDE_EXECUTE,hClientWnd,MAKELONG(NULL,hCommands));

The server, upon receipt of the command string, attempts to perform the
requested actions. If successful, it responds to the client with a positive
acknowledgment; if unsuccessful, a negative acknowledgment is sent. The net
result of this transaction is that the client is able to use the server as a
"black box" to perform certain operations that the client may not be able to
accomplish itself.


Ending a Conversation

As with most other things, good times often need to come to an end. For a
DDE conversation, this happens when either application terminates or when
the data link is no longer meaningful. Note that both the server and the
client application can end the conversation by performing a terminate
transaction. The receiving application is obligated to terminate the
conversation and respond with a matching terminate message. The transaction
is accomplished using the following code fragment:

  PostMessage(hServerWnd,WM_DDE_TERMINATE,hClientWnd,MAKELONG(NULL,NULL));

The application initiating the terminate transaction is obligated to wait
until all matching terminate responses have been received before closing.
During this period, it is expected that the terminating application will not
send any additional messages and will accept without processing all incoming
messages. (This includes the deletion of all atoms and the release of
allocated global memory.) In addition, the originator should be careful not
to respond to the matching terminate responses so as to eliminate the
possible cyclid condition in which both applications keep responding to the
terminate message ad infinitum.


DDE Maze

Perhaps the best way to understand the DDE protocol is to see how it is used
in an actual application. The DDE Maze program is just such an application;
besides demonstrating several kinds of DDE transactions, it also functions
as a general-purpose programming model that can be used for real-time
animations.


Overview

Conceptually, the DDE Maze application involves the real-time animation of a
ball that "bounces" around the client region of a window. The client region
contains a number of randomly positioned holes, or rectangles, into which
the ball can disappear. Each hole is numbered, and represents a conversation
with another instance of the Maze application. When the ball falls into a
hole, a DDE transaction occurs in which the ball is transferred to the
corresponding Maze instance. The ball then continues to bounce in the new
instance until it falls into another hole and is transferred yet again. This
process continues indefinitely until the last Maze instance is terminated or
the Windows session is closed.

The Maze application is a particularly interesting demonstration of DDE in
that it simultaneously supports multiple client and server conversations.
Although the source code is somewhat long, it is surprisingly small when you
consider the complexity of operations performed.

Two kinds of holes can exist in each Maze: black ones and white ones. (Refer
to Figure 10☼ for the following discussion.) The black ones represent
conversations in which the client application has asked to be notified
whenever the ball falls into a hole. In this situation the ball is
transferred to the client using a standard DDE data transaction. The white
holes represent active conversations in which the client application does
not want to be notified. In cases such as this the animation continues as if
the hole were not present.

When you bring up the first instance of the Maze application, it will be
positioned in the upper left-hand corner of your display and will contain
the caption "DDE Maze - #1." Subsequent instances of Maze will be serially
numbered and will result in a row-oriented tiling of the window until the
screen is completely covered. Each new instance of Maze will attempt to
start a conversation with each of the existing instances. The result of
these processes is the display of a randomly arranged set of holes
representing each successful conversation. Each of these holes will be
either black or white, depending on the notification status of the instance
they represent.

The system menu of each Maze instance contains two special menu options. The
first of these is the "Grab the ball" option. Selecting this option toggles
the ball advisory status for the current Maze instance. When this option is
checked, other Maze instances are notified that the current window wishes to
be informed whenever the ball can be transferred.

The second menu option, "Grab the focus," controls how the system focus is
handled. If this option is checked, the window will automatically capture
the system focus whenever the ball is transferred. When activated it results
in an interesting display as the focus shifts from window to window,
especially when you have several overlapping Maze applications passing the
ball among one another.


Communications Model

As previously mentioned, from a DDE perspective each instance of Maze is
simultaneously a client and a server. Although this might seem a little
confusing at first, it helps to separate the Maze functionality into two
alter egos: a client side and a corresponding server side.

From a client perspective, Maze starts life by initiating a conversation
with each of the servers using the application atom of Maze and the topic
atom of Ball. Each of the servers, given that they are able to participate
in another conversation, respond with a positive acknowledgment to the
client. In doing so each of the servers appends the client window handle to
its respective client list and displays a white hole in its window. At this
point the server does not know the client's window number, and indicates
this by displaying a question mark in the hole.

After receiving an acknowledgment from each server, the client informs each
server of its window number and current advisory status. Each server in turn
updates its display by changing the question mark to a number and indicating
the client's active advisory status by using a black or white rectangle.

To capture the bouncing ball, the user must select the "Grab the ball"
option from the client system menu. When this happens the client advises
each of its servers that it now wants to grab the ball (done by using the
Grab atom). Each of the servers in turn receives and acknowledges this data
advisory and responds by changing the client's hole from white to black.
When the "Grab the ball" option is disabled, the client unadvises each of
its servers, and a corresponding reversal of the initial advisory occurs.

During this time the ball continues to be animated and is passed from window
to window in a visibly random manner──a process that continues until the
client window closes. When this happens the client unadvises and terminates
the conversation with each of its servers. If the client has the ball at the
time the client window closes, it automatically transfers the ball to the
first interested server.

From a server perspective the ball is a commodity that is animated for a
short period of time and then transferred to a client (upon receipt of
which, the client becomes the server for a while). Once a new Maze instance
has initiated a conversation, the server is responsible for animating the
ball, checking with each iteration to see if it   has entered an active
hole.

When the ball enters an active hole, the server disables animation and
notifies the respective client of the situation. The client then responds by
continuing the animation of the ball in its window until another hole is
encountered.

Besides maintaining a conversation by means of the bouncing ball, the server
is also capable of reporting vital statistics on the current state of
animation. Such reporting enables other interested applications to peek into
Maze and be advised of the current operating status. For example Microsoft
Excel, a spreadsheet (with perhaps a corresponding chart) could be defined
as an application that uses this information in a real-time fashion.


General Structure

The Maze application is created using six separate files (see Figures 12
through 17). The MAZE file contains the various directives used by the
Microsoft Make utility. The MAZE.LNK and MAZE.DEF files contain standard
directives used by the linker when building the Maze. MAZE.H is a header
file, referenced by the Maze source code, that contains general application
definitions and data structures. The MAZE.D file is like MAZE.H, but
contains only function definitions. In practice it is commonly useful to
separate these definitions from the header file so as to avoid recompiling
the entire application when new functions are created or the parameters of
old ones change. Finally, MAZE.C contains all the C source code for the
application.

Note that the application does not have an associated resource (.RES) file.
This is because Maze does not have a menu, but uses only static resources
and uses its client area as an icon.

MAZE.C consists of nine functions that can be grouped into five distinct
categories. MazeWndFn is by far the largest and most complicated function
and is responsible for processing all the messages dispatched to it by the
message retrieval loop. This function, like most other message-processing
routines, is basically a very large switch statement that handles every
message on a case-by-case basis. Figure 11 summarizes the major functions
in the source code.

WinMain is the entry point for the Maze application and is responsible for
creating the Maze and for retrieving/dispatching all related system
messages. However, unlike most other Windows applications, the message-
retrieval loop is designed to incorporate the animation requirements of the
bouncing ball.

Ball.bIsBouncing, a global flag,  controls whether animation of the ball is
being performed. When this flag is logically true, a PeekMessage operation
is performed on the application message queue. If any message is present, it
is translated and dispatched in the normal fashion. Note that the
PeekMessage function returns true even if a WM_QUIT message is encountered.
In a normal message-retrieval loop, this message is not dispatched to the
window; hence the check for this special condition.

In cases when the ball is not bouncing, a normal GetMessage operation is
performed, and the message is translated and dispatched to the Maze window.
The message-retrieval process continues until either GetMessage or
PeekMessage encounter a WM_QUIT message. When this happens, the loop is
broken and Maze is destroyed.

The design of the message-processing loop is such that the animation of the
maze proceeds only when there are no messages to be processed. This enables
the maze to coexist with other applications without consuming undue
microprocessor time.

MazeWndFn is by far the most complicated portion of the Maze source code. It
is responsible for handling both the standard Windows messages and those
relating to the various DDE conversations in progress. Throughout this
function three main data structures are used; Maze, containing maze-related
variables; Ball, containing ball-related variables; and Link[#], a list of
active DDE communications links.

Note that MazeWndFn is responsible for handling both the client and server
sides of a DDE conversation. This combination results in a function that
supports more DDE messages than either a dedicated client or server would
individually. Furthermore, this function incorporates several safeguards
that prevent the possibility of having the window become a server to
itself.

The following discussion lists each of the messages processed by MazeWndFn
and describes in algorithmic terms what is being accomplished.

WM_CREATE is the Maze window created using the WS_OVERLAPPEDWINDOW style
option defined in WINDOWS.H. The first 15 instances of  (MAZE_ROWS *
MAZE_COLS) in the application are tiled into the display by using the
MoveWindow command. Additional instances use the default sizing supplied by
Windows.

WM_SYSCOMMAND is generated whenever the user selects one of the system menu
options (or moves or resizes the window). Two distinct actions are performed
in response to this message. When the user toggles the "Grab the ball" menu
option, Maze advises or unadvises each server respectively. Note that the
Link data structure is a list of client conversations; however, in this case
each of these clients is also a server. When the user toggles the "Grab the
focus" menu option, an internal data flag is changed and the menu updated.

WM_GETMINMAXINFO is a new Windows 2.0 message generated in conjunction with
the CreateWindow function call and enables the window to adjust the default
minimum and maximum tracking sizes supplied by the system. In this case we
adjust the minimum tracking height to prevent the user from defining a
window height less than the current hole height.

WM_SIZE involves the randomization of the Maze display, which consists of
the random repositioning of each of the client holes using the Link data
structure. In addition, the ball is randomly positioned and assigned a new
direction, with +1 meaning down or right and ──1 left or up. Note that in
several places the Maze sends a WM_SIZE message to itself when the display
needs randomizing. In many ways this is equivalent to defining a separate
function to handle the WM_SIZE message and calling it whenever necessary.

WM_PAINT causes the application to draw all the holes using their currently
defined positions. Of particular interest is how the text and background
colors are manipulated in order to achieve black on white and white on black
text for the numbers inside the client holes.

WM_CLOSE is received when the user first attempts to close the Maze window.
In normal situations the default window procedure destroys the window and
returns. However, when the ball is bouncing, Maze must transfer the ball to
the first interested client, thus eliminating the possibility of losing the
ball when the user closes the window.

Also included under this message is the termination of all active DDE
conversations. An unadvise operation for each active data item is required,
followed by a terminate transaction. Note that Maze is not allowed to be
destroyed until each of the clients has responded to the terminate
transaction. This precaution eliminates a situation in which an application
attempts to respond with an invalid window handle.

WM_DESTROY, as is the case with all other Windows applications, is the
second-to-last message received by the window (the last one being
WM_NCDESTROY). This message causes the WM_QUIT message to be posted and
brings about the subsequent ending of the message retrieval loop.

WM_DDE_INITIATE is received whenever a client wishes to initiate a DDE
conversation. The proposed conversation is examined and a response made
only if the initiate message is deemed acceptable. The conversation is
established by appending the caller to the client list and sending an
appropriate acknowledgment. Note how the client is asked to reciprocate
and also act as a server. The Maze.bInitiate flag prevents recursive
reciprocation of this process and enables the correct establishment of
the conversation.

WM_DDE_ADVISE is received whenever a client wishes to be advised on a
particular data item. The application searches to see if a communications
link has been established and checks the conditions under which the client
wishes to be notified. Note that the application is obligated to respond to
the caller and should send a positive acknowledgment only when the advisory
conditions are satisfactory. This enables the caller to continue polling the
application using different clipboard formats until a mutually agreeable one
is found.

WM_DDE_UNADVISE is received whenever a client wishes to unadvise the server
on a particular data item. As is the case with the advise message, the
application searches to verify the communications link and posts an
appropriate acknowledgment. Note how the display is invalidated whenever the
advisory status changes. This enables the user to verify the current
communications state of each data link visually.

WM_DDE_REQUEST is received whenever a client desires a one-time notification
on a particular data item. The Maze application supports only requests for
the Statistics data item in the CF_TEXT format. If the request is
acceptable, an immediate data transaction is generated. Note that the
transaction is specially flagged to indicate that it is in response to a
request.

The DDE request transaction is normally encountered immediately after a data
link is made and an advisory established, enabling the client to immediately
obtain the data in question without waiting for the next notification.

WM_DDE_POKE and WM_DDE_DATA The poke message is received whenever a client
wishes to inform a server of its window number. On the other hand, the data
message is received when the server passes the ball to a client. This
section of the application can be particularly confusing, since the same
code is used for both the client and server sides of the conversation.

Data transmitted by the Maze application is in the CF_TEXT format and
consists of two numbers separated by tab characters. In the poke case, the
first value is the window number followed by the client window focus state.
This focus state is logically true if the client currently has possession of
the system focus. In the data case, the first value represents the window
number of the hole into which the ball fell followed by the server focus
state. If Maze.bGrabFocus is true and the server currently has the focus,
the client manually sets the focus to itself.

WM_DDE_ACK in normal situations is received in response to an initiate or
advisory operation. This is perhaps one area in which Maze takes a rather
cavalier attitude to acknowledgements. The application currently takes any
action only when a response to an initiate operation is received. When this
occurs, the global atoms are deleted, and the server is informed of the
client's window number and current advisory status.

A sophisticated DDE application would probably want to handle situations in
which negative acknowledgements were received. In such cases, any dangling
portions of global memory would need to be released and associated atoms
deleted.

WM_DDE_TERMINATE is received when a client or server wishes to end a
conversation. If the link is still active, the caller is unadvised on all
active data items and a matching terminate is sent. In cases where the
application is waiting to be destroyed, the terminate messages are counted
until the last response is received, upon which the window is destroyed.

CreateMaze is responsible for the creation and initialization of the Maze
application, including the definition of all global atoms. If no previous
instance of the program exists, the first instance is assigned a window
number of one, and the window class is registered. If a previous instance
is present, a search process is initiated to find the lowest unused window
number. The resulting value then becomes the assigned window number.

Once the window is created, an initiate message is sent to all windows in
the environment. This is accomplished by performing a SendMessage to a
window handle of -1 with the generic maze topic and the aBall atom. Only
when each interested application responds to the initiate message is the
Maze window made visible.

DestroyMaze is called only after the main window message-processing loop is
terminated. This function is responsible for removing all defined global
atoms and returning the final exit code to the system.

Animation of the bouncing ball is accomplished using the BounceBall
function. At each iteration, this function checks to see if the ball has
entered one of the client area holes. If the ball has not fallen into a
hole, a new position is calculated from its current position. This
calculation considers the ball's current direction of motion and its
reflections off the client area boundaries. The new ball position is
indicated by retrieving the window display context and inverting the ball's
location. One interesting variant of this code is to insert an InvertRect
function call prior to calculating the new ball position. This eliminates
the trail the ball leaves behind as it bounces around the window.

If the ball has fallen into a hole, it is passed to the respective client
using a DDE data transaction and removed from the current window. In
addition, the ball's elapsed time in Maze is calculated, and all interested
clients informed. The first data value furnished represents the number of
milliseconds the ball was bouncing in Maze. The second one represents the
current area (in pixels) of the Maze client area. One would expect that, on
average, the time spent in a particular Maze instance would be proportional
to its window area.

The Advise function is one that could be part of almost any generic DDE
application. The sole purpose of this function is to advise a server that a
client is interested in a particular data item. In this case the advisory
process is limited to asking for data without acknowledgment in the CF_TEXT
format.

Note that Advise does not make any assumptions regarding the success or
failure of the GlobalAlloc, GlobalLock, or PostMessage functions. One would
expect GlobalAlloc to fail in situations where memory is scarce, but few
realize that GlobalLock can also fail when one is using expanded memory.
Although Maze does not attempt to recover from this situation, a serious
implementation of the DDE specification should be able to handle this case.

Data is transferred between two Maze instances using the Transmit function.
As with the Advise routine, this function involves the allocation and
locking of a portion of global memory. However, unlike the Advise routine,
the Transmit function also formats the supplied data as parameters into the
global memory block. After unlocking the memory, the handle is transmitted
to the client as part of a data or poke transaction.

The FindLink function is a utility used by the Maze application to find a
particular communications link. Given the window handle of the caller, it
searches the client list and returns the communications channel used. Using
this function, the Maze can determine the exact characteristics of the link
in a robust and link-independent fashion.

Lastly, the BumpGlobalAtom function increments the reference count for a
specified global atom. This is useful when sending information as part of a
data or poke transaction. The DDE specification requires that the recipient
of such a transaction delete the atoms sent. BumpGlobalAtom allows the
server application to increment this count without having access to the
original character string.


Building the Maze

As previously mentioned, the Maze application is created from six separate
files. To build the program, you will need to have a Microsoft C Compiler
(Version 4.0 or greater) and have access to a copy of the Windows 2.0
Software Development Kit (SDK). The SDK will contain the WINDOWS.H include
file as well as the WINSTUB.EXE program specified in the STUB line of
MAZE.DEF.

Using the Make utility that comes with the SDK, you can compile and link the
Maze program by using the following command:

  MAKE MAZE

Note that the MAZE.DEF file makes the assumption that the WINSTUB.EXE
program is present in your \BIN directory. If this is not the case, you will
have to change this to reflect your current hard disk directory structure.


Using Maze

The DDE Maze, unlike many other Windows applications, is best viewed in
large numbers. After creating the application, it is suggested that you
start up at least three instances by selecting the program in the MS-DOS
executive and starting it in an iconic form. When you have these icons
displayed at the bottom of your screen, you turn on the "Grab the Ball"
option and restore each to its suggested size. The end result will be a
tiled arrangement of the Maze instances with the ball visible and bouncing
from Maze to Maze.

Using Maze, you can experiment by bringing up additional Maze instances and
observing how new conversations are displayed in the client area of each
existing instance. You can turn on the "Grab the focus" option and also
watch the input focus being passed along with the bouncing ball. This can be
used to create an interesting visual effect when various overlapping windows
are present in the system.


Microsoft(R) Excel

One of the more interesting experiments you can conduct with the Maze
application is with Microsoft Excel. Using Microsoft Excel, you can query
Maze and create a spreadsheet that incorporates the latest bouncing ball
statistics. To do this you would bring up the spreadsheet program and create
an array formula containing the following text:

  =Maze|Ball!Statistics

To create this array formula, you would select two adjacent cells (by
dragging the mouse) and type in the formula. After typing the text, you
would make it part of the spreadsheet by using the Ctrl-Shift-Enter
keychord, which creates an array formula in which the two data items
supplied by Maze are displayed in adjacent columns. After entering the
keychord, you will be asked to choose from the list of available instances.
This is because Microsoft Excel supports only one communications link per
remote link (supporting more wouldn't really make much sense in a
spreadsheet anyway).

Once you select the instance you wish to communicate with, the values in the
two adjacent cells should change as the ball bounces between Maze instances.
The first data value represents the number of milliseconds the ball spent in
the referenced Maze before "falling" into a hole. The second data value
represents the area (in pixels) of the Maze client area.

By repeating this same operation, you can establish remote links to each of
the Maze instances currently executing. The end result is a table of data
values that change in response to the bouncing ball. If you wish you can
create a chart that uses this data and view the results as they change in
real time.

If you wish to establish an external reference to a particular Maze instance
(rather than having to manually select which one you want to talk to), you
can enter the following formula, in which the number sign (#) represents the
Maze instance number:

  =Maze#|Ball!Statistics


Inside DDE

Supporting DDE in an application can present some formidable challenges,
especially when multiple conversations are being simultaneously maintained
in a general fashion. Unfortunately the previous code fragments (and even
the Maze application to a lesser extent) are relatively simplistic in their
implementation of the DDE specification.

When designing a commercial application that supports DDE, you are best
advised to refer to the original DDE specification (available on DIAL or
directly from Microsoft). In most respects this document is a comprehensive
and authoritative guide to the protocol; however, you will probably have to
refer to various supplemental programs (such as Maze) before the scheme of
things becomes apparent.

Despite its completeness, the DDE specification is rather weak in some
areas. Perhaps one of its most significant shortcomings lies in the area of
message processing and synchronization. As mentioned, DDE is based on the
orderly processing of all system messages. However, in situations where
multiple conversations are being held, it is very difficult to keep track of
the exact state of each transaction. It is deceptively simple to create a
situation where two applications engage each other in an endless DDE
slugging match.

This might best be illustrated by the manner in which Maze uses two
simultaneous client-server conversations to emulate peer-to-peer
interaction. Since Maze responds to each interesting initiate message with
both an acknowledgment and another initiate transaction, one can  see how
two instances could become incessantly locked. Further complicating the
situation is the fact that the first initiate transaction is also received
by the Maze instance that originated it.

Another situation, perhaps even more insidious, concerns the way in which an
application responds to negative acknowledgments (or even the lack of any
acknowledgment if one is expected). The Maze application blithely ignores
such situations, opening the way for conditions in which unreferenced
portions of global memory are accumulated and global atoms left needlessly
bumped. A sophisticated DDE application (Microsoft Excel, for example) is
forced to compensate for this situation by tracking the exact state of each
transaction. Although such tracking yields some measure of security when
communicating with unruly or uncooperative applications, it dramatically
increases the complexity of the DDE handling portion of the application.

A second underemphasized area of great importance is that of shared memory
and global atoms. Great care must be taken to allocate and release such
resources correctly. Each of the DDE transactions has specific
synchronization rules that must be followed to ensure that resources remain
available. One situation in which this is particularly important is the
handling of global atoms in a data transaction. In this case the server is
expected to bump the atom reference count prior to initiating the
transaction. The client, on the other hand, is expected to delete the atom
and re-add it if an acknowledgment is expected.

Failure to bump or re-add the atom results in a condition where the atom
mysteriously disappears after several DDE transactions. Similarly, failure
to correctly delete the atom results in abnormally high reference counts
that could potentially mask other communications problems.

A third area barely covered in the DDE specification concerns the system
topic. This is a special topic (using the name "System") that furnishes a
context for items of general information available to partners in a DDE
conversation. Supporting this topic involves the creation of a new (probably
hidden) window and the definition of several data items for broadcast to
interested clients. Although not mentioned in the specification, it is
highly recommended that DDE applications support the system topic. When this
topic is available, potential conversants can conduct enquiries regarding
the current application status or obtain a list of available topics and data
renderings supported by the server.

One extension to the current DDE specification is that of paste links. A
paste link is a manually defined data link where the server provides the
conversation parameters. Upon user request, the server application would
supply the clipboard with a specially formatted "Link" data object. This
object consists of three null-terminated strings which contain the name of
the server, available topic, and data item. Any potential client, including
Microsoft Excel, can recognize this format and establish the conversation
using the parameters present on the clipboard.

One final area not covered by the DDE specification is that of debugging.
Unfortunately no commercial tool is currently available that facilitates
the interception and interpretation of DDE messages on a global basis. In
the interim, the code fragment in Figure 18 may be of some use as you try
to instrument at least your side of the conversation.

This routine is called in much the same way as the standard printf function,
with the difference being that the resulting string is displayed using a
system message box. The examples in Figure 19 illustrate how it could be
used.

Note that although the debugging monitor will look good in most situations,
problems arise when it is used in conjunction with a message transmitted
by a SendMessage function call. This is because it effectively blocks the
application that is waiting for the SendMessage call to return. Because of
this, the monitor function should not be used to trap WM_DDE_INITIATE
messages.


Conclusion

By now, hopefully, you have a much better feel for the way in which Dynamic
Data Exchange operates and are ready to try experimenting with the protocol
in your application. To be fair, a consistent and well-structured
implementation of DDE is a considerable development challenge; however, the
benefits obtained by the seamless integration of data between applications
more than outweigh the additional effort.

Of all the capabilities offered by Windows, DDE represents one of the most
exciting new prospects, paving the way to a world of consistent and easy-to-
use applications that cooperate together as never before. Despite its many
shortcomings and simplistic approach, the DDE Maze further demonstrates the
potential of this message protocol. With a little effort you  can easily
generalize the concepts presented here into a robust and efficient
formulation upon which the DDE modules of your application can be based.


Figure 1:  Uses of DDE

Hot links to real-time data, such as to stock market updates, scientific
instruments, or process control instrumentation.

Mission-critical transactions (often using automated data links), such as
airline reservations or commodity price reporting systems.

Creation of compound documents, such as a word processing document that
includes a chart produced by a graphics program. Using DDE, the chart will
change inside the document when the underlying data is modified.

Interapplication data queries, such as a spreadsheet querying a database
application for accounts past due.

Intercomputer data queries, such as a Windows application requesting data
from the PC down the hall or the corporate mainframe.


Figure 2:  General Syntax Used to Invoke DDE

PostMessage( hWndDest, wMessage, wParam, lParam )

hWndDest = destination window handle
wMessage = DDE message number
wParam = sender's window handle
lParam = assorted message arguments


Figure 3:  Global Atom Functions

aAtom = GlobalAddAtom( lpString )

This function adds the character string referenced by lpString to the
global atom table and returns the atom number. If the string is already
present in the table the function returns the existing atom value and
increments the string's reference count by 1.

nLength = GlobalGetAtomName( aAtom, lpString, nSize )

This function retrieves a copy of the character string associated with the
atom and places it in the buffer provided. The length returned is 0 if the
specified atom is invalid.

aAtom = GlobalFindAtom( lpString )

This function searches the atom table for the character string referenced
by lpString and returns the atom associated with it. A NULL atom is
returned if the string is not present in the table.

aOldAtom = GlobalDeleteAtom( aAtom )

This function deletes the atom and, if the atom's reference count is zero,
removes the associated string from the global atom table.


Figure 4:  DDE Messages Summary

╓┌──────────────────┌─────────────────────────────┌──────────────┌───────────►
                                                 ┌─Arguments in IParem─┐
DDE Message        Purpose                       Low Word      High Word

WM_DDE_INITIATE    Request initiation of a       aApplication  aTopic
                   conversation

WM_DDE_TERMINATE   Terminate a current           (reserved)    (reserved)
                   conversation

WM_DDE_ACK         Acknowledge a DDE message
                     In reply to INITIATE        aApplication  aTopic
                                                 ┌─Arguments in IParem─┐
DDE Message        Purpose                       Low Word      High Word
                     In reply to INITIATE        aApplication  aTopic
                     In reply to EXECUTE         wStatus       hCommands
                     In reply to all others      wStatus       aItems

WM_DDE_REQUEST     Request to server             cfFormat      aItem
                   to provide a data item

WM_DDE_DATA        Notify a client application   hData         aItem
                   of the availability of data

WM_DDE_ADVISE      Request to a server to        hOptions      aItem
                   supply an update for a data
                   item whenever it changes

WM_DDE_UNADVISE    Request to a server that a    (reserved)    aItem
                   specified data item should
                   no longer be updated

WM_DDE_POKE        Request to an application     hData         aItem
                                                 ┌─Arguments in IParem─┐
DDE Message        Purpose                       Low Word      High Word
WM_DDE_POKE        Request to an application     hData         aItem
                   to accept an unsolicited
                   data item

WM_DDE_EXECUTE     Sends a string to a server    (reserved)    hCommands
                   application to be processed
                   as a series of commands


Abbreviations Used Above:

aName:    An atom of word length (16 bits)
hName:    A handle of word length to a global memory object
cfFormat: A registered clipboard format number of word length
wName:    Any other word-length parameter


Figure 5:  Responding to WM_DDE_INITIATE

aApp =         application atom provided by client
aTopic =       topic atom client
aClientWnd =   handle to client application (who initiated conversation)

if ( (aMyApp == aApp) || (aApp == NULL) ) {
     if ( aTopic ) {
        if ( aMyTopic == aTopic ) {
            SendMessage(
                hClientWnd,
                WM_DDE_ACK,
                hMyWnd,
                lParam
            );
        } else {
            for ( Each supported topic )
                SendMessage(
                    hClientWnd,
                    WM_DDE_ACK,
                    hMyWnd,
                    MAKELONG(aApp,aMyTopic)
                );
        }
     }
}


Figure 6:  Client Advising a Server

/* create global data block */
hOptions = GlobalAlloc( GHND|GMEM_DDESHARE, (DWORD)sizeof(OPTIONS) );
if ( hOptions ) {

    /* lock options data structure */
    lpOptions = GlobalLock( hOptions );
    if ( lpOptions ) {

        /* define advise options data structure */
        lpOptions->fAck = TRUE;
        lpOptions->fNoData = FALSE;
        lpOptions->cfFormat = CF_TEXT;

                GlobalUnlock( hOptions );

        /* advise server to send data */
        PostMessage(
            hServerWnd,
            WM_DDE_ADVISE,
            hMyWnd,
            MAKELONG(hOptions,aItem)
        );

    }
}


Figure 7:  Posting Data to a Client

/* allocation global block of memory & define item atom */
hMemory = GlobalAlloc( GHND|GMEM_DDESHARE, (DWORD)sizeof(DATA) );
if ( hMemory ) {

    /* lock allocated data block - may fail with expanded memory! */
    lpMemory = GlobalLock( hMemory );
    if ( lpMemory ) {

        /* define data flags */
        lpMemory->fAck = TRUE;
        lpMemory->fNoData = FALSE;
        lpMemory->cfFormat = CF_TEXT;

        /* DEFINE GLOBAL MEMORY HERE */
        lstrcpy( lpMemory->lpData, lpDataString );

        /* unlock data & define item atom */
        GlobalUnlock( hMemory );
        aItem = GlobalAddAtom( "DataItem" );

        /* post data to client */
                    PostMessage(
            hClientWnd,
            WM_DDE_DATA,
            hMyWnd,
           MAKELONG(hMemory,aItem)
        );

    }
}


Figure 8:  Rules for Building Command Strings

<execute string> := <token>...<token>
<token>          := [ <opcode string> ]
<opcode string>  := <opcode>|<opcode><parm list>
<opcode>         := application defined string token
<parm list>      := ( <parameter>,<parameter>,...<parameter> )
<parameter>      := application defined string token


Figure 9:  Command String Examples

[file][open("sample.txt")][print]
[dial("1-800-123-4567")][connect][transmit("sample.txt")][disconnect]
[datafile][find("money","devaluation")][report]


Figure 11:  Major Sections of MAZE.C

╓┌───────────────┌───────────────────┌───────────────────────────────────────╖
Function        Category           General Description

WinMain         mainline           system mainline and vessage-retrieval
                                   loop
───────────────────────────────────────────────────────────────────────────
MazeWndFn       Window related     maze window function and message
                                   processing

CreateMaze                         initialization of maze and initial
                                   communications

DestroyMaze                        termination of maze and atom removal
───────────────────────────────────────────────────────────────────────────
BounceBall      animation          animates bouncing ball inside maze
───────────────────────────────────────────────────────────────────────────
Advise          communications     advise server on particular data item

Function        Category           General Description

Transmit                           transmit data using POKE or DATA
───────────────────────────────────────────────────────────────────────────
FindLink        utility functions  find a communications link using window
                                   handle

BumpGlobalAtom                     increment global atom reference count


Figure 12:  MAKE is the MAZE Make file

CFLAGS=-c -u -AS -FPa -Gsw -Os -Zep

maze.obj: maze.h maze.c
   cl $(CFLAGS) maze.c

maze.exe: maze.obj maze.def maze.lnk
   link4 @maze.lnk


Figure 13:  MAKE.DEF is the MAZE Definitions File

NAME                    Maze

DESCRIPTION             'Dynamic Data Exchange Maze'

STUB                   '\BIN\WINSTUB.EXE'

CODE                    MOVEABLE
DATA                    MOVEABLE MULTIPLE

HEAPSIZE                2048
STACKSIZE               2048

EXPORTS
     MazeWndFn          @1


Figure 14:  MAZE.LNK is the MAZE Link file

maze /align:16
maze
maze
slibw
maze.def


Figure 15:  MAZE.H is the Maze header file

/*
 * DDE MAZE - HEADER FILE
 *
 * LANGUAGE : Microsoft C
 * MODEL    : Small
 * STATUS   : Operational
 *
 * 09/22/87 - Kevin P. Welch - initial creation.
 */

#define MAZE_COLS               3
#define MAZE_ROWS               5

#define SC_GRAB_BALL            0x0100
#define SC_GRAB_FOCUS           0x0101

#define HI                      HIWORD(lPrm)
#define LO                      LOWORD(lPrm)

#define RGB_BLACK               RGB(0x00,0x00,0x00)
#define RGB_WHITE               RGB(0xFF,0xFF,0xFF)

/*
 * BALL DATA STRUCTURE DEFINITIONS
 *
 */

#define BALL_WIDTH         6
#define BALL_HEIGHT        6

#define RANDOM_MOTION      (((rand()%2)*2)-1)

#define H_BOUNCE(a,b)      ((a<=0)?1:((a>=b)?-1:Ball.iHorzMotion))
#define V_BOUNCE(a,b)      ((a<=0)?1:((a>=b)?-1:Ball.iVertMotion))

#define ADVISE_OFF         (!Link[i].bAdviseBall)

#define OUTSIDE_LEFT       (Ball.rPosn.left<=Link[i].rHole.left)
#define OUTSIDE_RIGHT      (Link[i].rHole.right<=Ball.rPosn.right)
#define OUTSIDE_TOP        (Ball.rPosn.top<=Link[i].rHole.top)
#define OUTSIDE_BOTTOM     (Link[i].rHole.bottom<=Ball.rPosn.bottom)

#define OUTSIDE_WIDTH      ((OUTSIDE_LEFT)||(OUTSIDE_RIGHT))
#define OUTSIDE_HEIGHT     ((OUTSIDE_TOP)||(OUTSIDE_BOTTOM))

#define OUTSIDE_HOLE       ((ADVISE_OFF)||(OUTSIDE_WIDTH)||(OUTSIDE_HEIGHT))

typedef struct {
     RECT         rPosn;           /* current ball position */
     long         lTimeIn;         /* time ball entered maze */
     int          iHorzMotion;     /* vertical ball motion offset */
     int          iVertMotion;     /* horizontal ball motion offset */
     BOOL         bIsBouncing;     /* ball bouncing flag */
} BALL;

/*
 * MAZE DATA STRUCTURE DEFINITIONS
 *
 */

#define DISPLAY_WIDTH               (Maze.wWidth+BALL_WIDTH)
#define DISPLAY_HEIGHT              (Maze.wHeight+BALL_HEIGHT)

typedef struct {
     WORD          wNum;            /* maze window number */
     WORD          wLinks;          /* number of maze links */
     WORD          wWidth;          /* width of maze client area */
     WORD          wHeight;         /* height of maze client area */
     BOOL          bInitiate;       /* in initiate flag */
     BOOL          bGrabBall;       /* grab the ball flag */
     BOOL          bGrabFocus;      /* grab the focus flag */
     WORD          wGoingAway;      /* maze going away counter */
} MAZE;

/*
 * COMMUNICATION LINK DATA STRUCTURE DEFINITIONS
 *
 */

#define MAX_LINK               32

#define HOLE_WIDTH             20
#define HOLE_HEIGHT            14

typedef struct {
     HWND          hWnd;            /* client window handle */
     WORD          wNum;            /* client window number */
     RECT          rHole;           /* client hole position */
     BOOL          bAdviseBall;     /* advise client of ball flag */
     BOOL          bAdviseStat;     /* advise client of stats flag */
} LINK;

/*
 * DYNAMIC DATA EXCHANGE DEFINITIONS
 *
 */

#define ACCEPTED               0x8000
#define REJECTED               0x0000

#define WM_DDE_INITIATE        0x03e0
#define WM_DDE_TERMINATE       0x03e1
#define WM_DDE_ADVISE          0x03e2
#define WM_DDE_UNADVISE        0x03e3
#define WM_DDE_ACK             0x03e4
#define WM_DDE_DATA            0x03e5
#define WM_DDE_REQUEST         0x03e6
#define WM_DDE_POKE            0x03e7
#define WM_DDE_EXECUTE         0x03e8

#define SEND(a,b,c)            SendMessage(a,b,hWnd,c)
#define POST(a,b,c)            PostMessage(a,b,hWnd,c)

#define DDE_INITIATE(a,b,c)    SEND(a,WM_DDE_INITIATE,MAKELONG(b,c))
#define DDE_TERMINATE(a)       POST(a,WM_DDE_TERMINATE,0L)
#define DDE_ADVISE(a,b)        Advise(a,hWnd,b)
#define DDE_UNADVISE(a,b)      POST(a,WM_DDE_UNADVISE,MAKELONG(0,b))
#define DDE_ACK(a,b,c)         POST(a,WM_DDE_ACK,MAKELONG(b,c))
#define DDE_DATA(a,b,c,d,e)    Transmit(a,hWnd,WM_DDE_DATA,b,c,d,e)
#define DDE_POKE(a,b,c,d,e)    Transmit(a,hWnd,WM_DDE_POKE,b,c,d,e)

typedef struct {
     WORD     fEmpty:12;       /* reserved for future use */
     WORD     fResponse:1;     /* in response to request */
     WORD     fRelease:1;      /* release data */
     WORD     fNoData:1;       /* null data handle ok */
     WORD     fAck:1;          /* Ack expected */
     WORD     cfFormat;        /* clipboard data format */
     BYTE     info[30];        /* data buffer */
} DATA;

typedef DATA *          PDATA;
typedef DATA FAR *     LPDATA;


Figure 16:  MAZE.D is the MAZE Function Definitions File

/*
 * DDE MAZE - FUNCTION DEFINITIONS
 *
 * LANGUAGE  : Microsoft C
 * MODEL     : Small
 * STATUS    : Operational
 *
 * 09/22/87 - Kevin P. Welch - initial creation.
 *
 */

VOID          BounceBall    ( HWND );
WORD          DestroyMaze   ( WORD );
ATOM          BumpGlobalAtom( ATOM );
BOOL          FindLink      ( WORD *, HWND );
BOOL          Advise        ( HWND, HWND, ATOM );
HWND          CreateMaze    ( HANDLE, HANDLE, WORD );
BOOL          Transmit      ( HWND, HWND, WORD, ATOM, BOOL, LONG, LONG );

LPSTR FAR PASCAL     lstrcpy( LPSTR, LPSTR );
LONG  FAR PASCAL   MazeWndFn( HWND, WORD, WORD, LONG );


Figure 17:  MAKE.C is the Source Code Listiong for MAZE

/*
 * DDE MAZE - SOURCE CODE
 *
 * LANGUAGE  : Microsoft C
 * MODEL     : Small
 * STATUS    : Operational
 *
 * 09/22/87 - Kevin P. Welch - initial creation.
 *
 * The DDE Maze demonstrates how Dynamic Data Exchange (DDE) can be
 * used in multiple server, multiple client conversations. The maze
 * supports two distinct data items - one used in passing the animated
 * ball between instances of the Maze, the other for reporting vital
 * Maze statistics to an interested listener.
 *
 * Note that the DDE techniques demonstrated in this program are
 * a simplification of the general specification. In ALL cases
 * where you observe differences between this application and the
 * specification, please follow the published protocol.
 *
 * Special thanks to Ed Fries, Geoff Nichols, and David West as
 * they helped me see the forest from the trees!
 *
 */

#include <windows.h>
#include "maze.h"
#include "maze.d"

ATOM          aWnd;           /* window number item */
ATOM          aBall;          /* bouncing ball topic */
ATOM          aGrab;          /* grab the ball item */
ATOM          aStat;          /* statistics item */
ATOM          aAnyMaze;       /* generic maze atom */
ATOM          aThisMaze;      /* this maze atom */

MAZE          Maze;           /* maze data structure */
BALL          Ball;           /* ball data structure */
LINK          Link[MAX_LINK]; /* maze communications link */

/*
 * MAZE MAINLINE & MESSAGE PROCESSING LOOP
 *
 *     hInst          current instance handle
 *     hPrevInst      previous instance handle
 *     lpsCmd         execution command line string
 *     wCmdShow       initial show-window option
 *
 * This mainline is responsible for calling the initialization and
 * termination routines in addition to processing and distributing
 * all incoming messages. Note the revised message processing
 * loop used to animate the bouncing ball.
 *
 */

WORD PASCAL WinMain( hInst, hPrevInst, lpsCmd, wCmdShow )
     HANDLE          hInst;
     HANDLE          hPrevInst;
     LPSTR           lpsCmd;
     WORD            wCmdShow;
{
     MSG             Msg;          /* current system message */
     HWND            hWnd;         /* maze window handle */

     /* create & initialize maze */
     hWnd = CreateMaze( hInst, hPrevInst, wCmdShow );

     /* message processing loop */
     do {

          /* retrieve next message */
          if ( Ball.bIsBouncing ) {
               if ( PeekMessage(&Msg,NULL,0,0,TRUE) ) {
                    if ( Msg.message != WM_QUIT ) {
                         TranslateMessage( &Msg );
                         DispatchMessage( &Msg );
                    }
               } else
                    BounceBall( hWnd );
          } else
               if ( GetMessage(&Msg,NULL,0,0) ) {
                    TranslateMessage( &Msg );
                    DispatchMessage( &Msg );
               }

     } while ( Msg.message != WM_QUIT );

     /* destroy maze & exit */
     exit( DestroyMaze(Msg.wParam) );

}

/*
 * MAZE WINDOW MESSAGE PROCESSING FUNCTION
 *
 *     hWnd               window handle
 *     wMsg               window message number
 *     wPrm               additional message info
 *     lPrm               additional message info
 *
 * This function processes all the messages related to the maze
 * window, including all the DDE messages required in order to
 * participate in one or more conversations. Note that this
 * window function handles DDE messages for both the client and
 * the server sides of the conversation!
 *
 */

LONG FAR PASCAL MazeWndFn( hWnd, wMsg, wPrm, lPrm )
     HWND          hWnd;
     WORD          wMsg;
     WORD          wPrm;
     LONG          lPrm;
{
     WORD          i;             /* channel number */
     LONG          lAck;          /* acknowledgement of message */

     /* initialization */
     lAck = FALSE;

     /* process message */
     switch( wMsg )
          {
     case WM_CREATE : /* create window */

          /* adjust window position if tileable */
          if ( Maze.wNum <= MAZE_ROWS*MAZE_COLS )
               MoveWindow(
                    hWnd,
                    (Maze.wWidth*((Maze.wNum-1)%MAZE_COLS)),
                    (Maze.wHeight*((Maze.wNum-1)/MAZE_COLS)),
                    Maze.wWidth,
                    Maze.wHeight,
                    TRUE
               );

          break;
     case WM_SYSCOMMAND : /* system command */

          /* process sub-message */
          switch( wPrm )
               {
          case SC_GRAB_BALL :

               /* inform all clients */
               if ( Maze.bGrabBall ) {
                    Maze.bGrabBall = FALSE;
                    for (i=0; i<Maze.wLinks; i++)
                         DDE_UNADVISE( Link[i].hWnd, aGrab );
               } else {
                    Maze.bGrabBall = TRUE;
                    for (i=0; i<Maze.wLinks; i++)
                         DDE_ADVISE( Link[i].hWnd, aGrab );
               }

               /* update system menu */
               CheckMenuItem(
                    GetSystemMenu(hWnd,FALSE),
                    SC_GRAB_BALL,
                    (Maze.bGrabBall)?MF_CHECKED:MF_UNCHECKED
               );

               break;
          case SC_GRAB_FOCUS :

               /* adjust state & system menu */
               Maze.bGrabFocus = (Maze.bGrabFocus) ? FALSE : TRUE;
               CheckMenuItem(
                    GetSystemMenu(hWnd,FALSE),
                    SC_GRAB_FOCUS,
                    (Maze.bGrabFocus)?MF_CHECKED:MF_UNCHECKED
               );

               break;
          default :
               lAck = DefWindowProc( hWnd, wMsg, wPrm, lPrm );
               break;
          }

          break;
     case WM_GETMINMAXINFO : /* get window size info */

          /* set minimum tracking height */
          ((LPPOINT)lPrm)[3].y = 5 * HOLE_HEIGHT;

          break;
     case WM_SIZE : /* window being sized */

          /* adjust animation statistics */
          Ball.lTimeIn = GetCurrentTime();

          /* calculate hole adjusted window dimensions */
          Maze.wWidth  = LOWORD(lPrm) - HOLE_WIDTH;
          Maze.wHeight = HIWORD(lPrm) - HOLE_HEIGHT;

          /* randomly position all holes */
          for ( i=0; i<Maze.wLinks; i++ ) {
               Link[i].rHole.left   = rand() % Maze.wWidth;
               Link[i].rHole.top    = rand() % Maze.wHeight;
               Link[i].rHole.right  = Link[i].rHole.left + HOLE_WIDTH;
               Link[i].rHole.bottom = Link[i].rHole.top  + HOLE_HEIGHT;
          }

          /* calculate ball adjusted window dimensions */
          Maze.wWidth  = LO - BALL_WIDTH;
          Maze.wHeight = HI - BALL_HEIGHT;

          /* randomly position bouncing ball */
          Ball.rPosn.left = rand() % Maze.wWidth;
          Ball.rPosn.top = rand() % Maze.wHeight;
          Ball.rPosn.right = Ball.rPosn.left + BALL_WIDTH;
          Ball.rPosn.bottom = Ball.rPosn.top + BALL_HEIGHT;

          /* assign ball movement direction */
          Ball.iHorzMotion = RANDOM_MOTION;
          Ball.iVertMotion = RANDOM_MOTION;

          break;
     case WM_PAINT : /* window needs painting */

          {
               PAINTSTRUCT   Region;        /* temporary paint structure */
               BYTE          sInterior[8];  /* temporary text string */

               BeginPaint( hWnd, &Region );

               /* draw all holes in window */
               for ( i=0; i<Maze.wLinks; i++ ) {

                 /* select appropriate text & brush colors */
                 if (Link[i].bAdviseBall || Link[i].bAdviseStat ) {
                     SetBkColor( Region.hdc, RGB_BLACK );
                     SetTextColor( Region.hdc, RGB_WHITE );
                     SelectObject( Region.hdc, GetStockObject(BLACK_BRUSH));
                 } else {
                     SetBkColor( Region.hdc, RGB_WHITE );
                     SetTextColor( Region.hdc, RGB_BLACK );
                     SelectObject( Region.hdc, GetStockObject(WHITE_BRUSH));
                 }

                 /* draw black hole  */
                 Rectangle(
                         Region.hdc,
                         Link[i].rHole.left,
                         Link[i].rHole.top,
                         Link[i].rHole.right,
                         Link[i].rHole.bottom
                    );

                    if ( Link[i].wNum )
                         sprintf( sInterior, "%u", Link[i].wNum );
                    else
                         strcpy( sInterior, "?" );

                    DrawText(
                         Region.hdc,
                         sInterior,
                         strlen(sInterior),
                         &Link[i].rHole,
                         DT_CENTER|DT_VCENTER|DT_NOCLIP|DT_SINGLELINE
                    );

               }

               EndPaint( hWnd, &Region );

          }

          break;
     case WM_CLOSE : /* end it all */

          if ( Maze.wLinks ) {

               /* pass ball to first interested client */
               if ( Ball.bIsBouncing ) {
                    for (i=0; (i<Maze.wLinks)&&(!Link[i].bAdviseBall); i++);
                    DDE_DATA(
                         Link[ (i<Maze.wLinks) ? i : 0 ].hWnd,
                         aGrab,
                         FALSE,
                         (LONG)Maze.wNum,
                         (LONG)((GetFocus()==hWnd)?TRUE:FALSE)
                    );
               }

               /* notify all clients */
               for (i=0; i<Maze.wLinks; i++) {
                  if (Link[i].bAdviseBall) DDE_UNADVISE(Link[i].hWnd,aBall);
                  if (Link[i].bAdviseStat) DDE_UNADVISE(Link[i].hWnd,aStat);
                  DDE_TERMINATE( Link[i].hWnd );
               }

               /* wait till someone pulls the plug */
               Maze.wGoingAway = Maze.wLinks;

               Maze.wLinks = 0;
               Ball.bIsBouncing = FALSE;
               SetWindowText( hWnd, "Maze dying..." );

          } else
               DestroyWindow( hWnd );

          break;
     case WM_DESTROY : /* end it all! */
          PostQuitMessage( 0 );
          break;
     case WM_DDE_INITIATE : /* initiate DDE conversation */

          /* process message if interesting request */
          if ( (hWnd!=wPrm)&&(LO==aAnyMaze||LO==aThisMaze)&&(HI==aBall) ) {

               /* check if caller already a client */
               if ( !FindLink(&i,wPrm) ) {

                  /* add caller to list of links */
                  i = Maze.wLinks++;

                  Link[i].wNum = 0;
                  Link[i].hWnd = (HWND)wPrm;
                  Link[i].bAdviseBall = FALSE;
                  Link[i].bAdviseStat = FALSE;

                  /* re-add global atoms */
                  BumpGlobalAtom( HI );
                  BumpGlobalAtom( aThisMaze );

                  SEND( wPrm, WM_DDE_ACK, MAKELONG(aThisMaze,HI) );

                  /* ask caller to become a server also */
                  if ( (!Maze.bInitiate)&&(LO==aAnyMaze) ) {
                       Maze.bInitiate = TRUE;
                       DDE_INITIATE( wPrm, aAnyMaze, aBall );
                       Maze.bInitiate = FALSE;
                  }

                  /* rearrange window & update display */
                  SEND(hWnd,WM_SIZE,MAKELONG(DISPLAY_WIDTH,DISPLAY_HEIGHT));
                  InvalidateRect( hWnd, NULL, TRUE );

               }

          }

          break;
     case WM_DDE_ADVISE : /* advise client on DDE item */

          {
               WORD     wAnswer;   /* answer to advise message */
               LPDATA   lpData;    /* temporary advise structure */

               /* initialization */
               wAnswer = REJECTED;

               /* search for link */
               if ( FindLink(&i,wPrm)&&((HI==aGrab)||(HI==aStat)) ) {

                    lpData = (LPDATA)GlobalLock( LO );
                    if ( lpData ) {

                         if ( lpData->cfFormat == CF_TEXT ) {

                              wAnswer = ACCEPTED;
                              Link[i].bAdviseBall |= (HI==aGrab);
                              Link[i].bAdviseStat |= (HI==aStat);

                              GlobalUnlock( (HANDLE)LO );
                              GlobalFree( (HANDLE)LO );

                              InvalidateRect( hWnd, NULL, TRUE );

                         } else
                              GlobalUnlock( LO );

                    }

               }

               /* respond to message */
               DDE_ACK( wPrm, wAnswer, HI );

          }

          break;
     case WM_DDE_UNADVISE :       /* stop advising client on DDE item */

          {
               WORD     wAnswer;  /* answer to advise message */
               LPDATA   lpData;   /* temporary advise structure */

               /* initialization */
               wAnswer = REJECTED;

               /* search for link */
               if ( FindLink(&i,wPrm)&&((HI==aGrab)||(HI==aStat)) ) {

                    wAnswer = ACCEPTED;
                    if (HI==aGrab) Link[i].bAdviseBall=FALSE;
                    if (HI==aStat) Link[i].bAdviseStat=FALSE;

                    InvalidateRect( hWnd, NULL, TRUE );

               }

               /* respond to message */
               DDE_ACK( wPrm, wAnswer, HI );

          }

          break;
     case WM_DDE_REQUEST :         /* client requestion data item */

          {
               WORD     wAnswer;   /* answer to advise message */
               LPDATA   lpData;    /* temporary advise structure */

               if ( FindLink(&i,wPrm)&&(HI==aStat)&&(LO==CF_TEXT) )
                    DDE_DATA(
                        Link[i].hWnd,
                        aStat,
                        TRUE,
                        (Ball.bIsBouncing)?GetCurrentTime()-Ball.lTimeIn:0L,
                        DISPLAY_WIDTH*(LONG)DISPLAY_HEIGHT
                    );
               else
                    DDE_ACK( wPrm, REJECTED, HI );

          }

          break;
     case WM_DDE_POKE : /* client providing unsolicited data item */
     case WM_DDE_DATA : /* server providing data item */

          {
               WORD     wData1;       /* window number */
               WORD     wData2;       /* focus state */
               LPDATA   lpData;       /* temporary advise structure */
               BOOL     bRespond;     /* boolean respond flag */
               WORD     wResponse;    /* response to message */
               char     sString[30];  /* temporary data string */

               /* initialization */
               bRespond = TRUE;
               wResponse = REJECTED;

               /* search for link & check data item */
               if ( FindLink(&i,wPrm)&&((HI==aGrab)||(HI==aWnd)) ) {

                    /* retrieve data - lock may not succeed */
                    lpData = (LPDATA)GlobalLock( LO );
                    if ( lpData ) {

                         /* check format */
                         if ( lpData->cfFormat == CF_TEXT ) {

                            /* extract data */
                            lstrcpy( (LPSTR)sString, lpData->info );
                            sscanf( sString, "%u\t%u", &wData1, &wData2 );

                            /* process data */
                            if ( HI == aGrab ) {

                              /* grab ball */
                              Ball.bIsBouncing = TRUE;
                              Ball.lTimeIn = GetCurrentTime();
                              if (Maze.bGrabFocus && wData2) SetFocus(hWnd);

                              /* randomize display */
                              SEND(
                                   hWnd,
                                   WM_SIZE,
                                   MAKELONG(DISPLAY_WIDTH,DISPLAY_HEIGHT)
                              );

                            } else
                                 Link[i].wNum = wData1;

                            /* Determine if acknowledgement required */
                            if ( !lpData->fAck ) {
                                 bRespond = FALSE;
                                 GlobalDeleteAtom( HI );
                            } else
                                 wResponse = ACCEPTED;

                            /* unlock memory & free if required */
                            if ( lpData->fRelease ) {
                                 GlobalUnlock( LO );
                                 GlobalFree( LO );
                            } else
                                 GlobalUnlock( LO );

                            /* force system to repaint display */
                            InvalidateRect( hWnd, NULL, TRUE );

                         } else
                              GlobalUnlock( LO );

                    }

               }

               /* respond to caller */
               if (bRespond) DDE_ACK(wPrm,wResponse,HI);

          }

          break;
     case WM_DDE_ACK : /* DDE acknowledgement */

          if ( Maze.bInitiate ) {

               /* delete atoms - since bumped */
               GlobalDeleteAtom( LO );
               GlobalDeleteAtom( HI );

               /* inform server of window number */
               DDE_POKE(
                    wPrm,
                    aWnd,
                    FALSE,
                    (LONG)Maze.wNum,
                    (LONG)((GetFocus()==hWnd)?TRUE:FALSE)
               );

               /* inform server of current advise status */
               if ( Maze.bGrabBall )
                    DDE_ADVISE( wPrm, aGrab );

          }

          break;
     case WM_DDE_TERMINATE : /* end a DDE conversation */

          if ( FindLink(&i,wPrm) ) {

               /* respond with an unadvise on all items */
               if (Link[i].bAdviseBall) DDE_UNADVISE(wPrm,aBall);
               if (Link[i].bAdviseStat) DDE_UNADVISE(wPrm,aStat);

               /* remove caller from list */
               memcpy(&Link[i],&Link[i+1],(Maze.wLinks-i-1)*sizeof(LINK));
               Maze.wLinks--;

               /* respond with a matching terminate & update display */
               DDE_TERMINATE( wPrm );
               InvalidateRect( hWnd, NULL, TRUE );

          } else
               if ( (Maze.wGoingAway)&&(Maze.wGoingAway-- == 1) )
                    DestroyWindow( hWnd );

          break;
     default : /* pass on to default */
          lAck = DefWindowProc( hWnd, wMsg, wPrm, lPrm );
          break;
     }

     /* return result */
     return( lAck );

}

/*
 * CREATE DDE MAZE
 *
 *     hInst          current instance handle
 *     hPrevInst     previous instance handle
 *     wCmdShow     initial show window command
 *
 * This function creates and initializes the Maze, including the
 * definition of all global atoms. A handle to the maze window is
 * returned if the entire process is successful.
 *
 */

static HWND CreateMaze( hInst, hPrevInst, wCmdShow )
     HANDLE     hInst;
     HANDLE     hPrevInst;
     WORD     wCmdShow;
{
     WORD          i;             /* temporary loop variable */
     HWND          hWnd;          /* new maze window handle */
     HMENU         hMenu;         /* system menu handle */
     WORD          wQueue;        /* queue length counter */
     BOOL          bSearch;       /* boolean search flag */
     BOOL          bPresent;      /* number present flag */
     WNDCLASS      WndClass;      /* window class structure */
     BYTE          sCaption[64];  /* current window caption */

     /* perform instance specific initialization */
     if ( !hPrevInst ) {

          Maze.wNum = 1;

          Ball.bIsBouncing = TRUE;
          Ball.lTimeIn = GetCurrentTime();

          memset( &WndClass, 0, sizeof(WNDCLASS) );

          WndClass.lpszClassName = (LPSTR)"MazeWindow";
          WndClass.hCursor       = LoadCursor(NULL,IDC_ARROW);
          WndClass.lpszMenuName  = (LPSTR)NULL;
          WndClass.style         = CS_HREDRAW | CS_VREDRAW;
          WndClass.lpfnWndProc   = MazeWndFn;
          WndClass.hInstance     = hInst;
          WndClass.hIcon         = NULL;
          WndClass.hbrBackground = (HBRUSH)(COLOR_MENU + 1);

     } else {

          GetInstanceData( hPrevInst, (NPSTR)&Maze, sizeof(MAZE) );
          GetInstanceData( hPrevInst, (NPSTR)Link, MAX_LINK*sizeof(LINK) );

          Link[Maze.wLinks++].wNum = Maze.wNum;

          bSearch   = TRUE;
          Maze.wNum = 1;

          while ( bSearch ) {

               bPresent = FALSE;
               for (i=0; i<Maze.wLinks; i++)
                    if ( Link[i].wNum == Maze.wNum )
                         bPresent = TRUE;

               if ( bPresent )
                    Maze.wNum++;
               else
                    bSearch = FALSE;

          }

          Maze.wLinks      = 0;
          Ball.bIsBouncing = FALSE;

     }

     /* continue Maze initialization */
     if ( hPrevInst || RegisterClass(&WndClass) ) {

          /* adjust maze queue length */
          wQueue = 42;
          while ( !SetMessageQueue(wQueue--) );

          /* define caption & initial position */
          sprintf( sCaption, "DDE Maze - #%u", Maze.wNum );

          Maze.wGoingAway = 0;
          Maze.bGrabBall  = FALSE;
          Maze.bGrabFocus = FALSE;
          Maze.wWidth     = GetSystemMetrics(SM_CXSCREEN) / MAZE_COLS;
          Maze.wHeight    = GetSystemMetrics(SM_CYSCREEN) / MAZE_ROWS;

          hWnd = CreateWindow(
                    "MazeWindow",            /* class name */
                    sCaption,                /* caption */
                    WS_OVERLAPPEDWINDOW,     /* style */
                    0,                       /* x position */
                    0,                       /* y position */
                    0,                       /* width */
                    0,                       /* height */
                    (HWND)NULL,              /* parent window */
                    (HMENU)NULL,             /* menu */
                    hInst,                   /* application */
                    (LPSTR)NULL              /* other data */
               );

          if ( hWnd ) {

               /* revise system menu */
               hMenu = GetSystemMenu( hWnd, FALSE );
               ChangeMenu(hMenu,0,NULL,0,MF_APPEND|MF_SEPARATOR );
               ChangeMenu(hMenu,0,"Grab the ball",SC_GRAB_BALL,MF_APPEND);
               ChangeMenu(hMenu,0,"Grab the focus",SC_GRAB_FOCUS,MF_APPEND);

               /* define global atoms */
               sprintf( sCaption, "Maze%u", Maze.wNum );

               aAnyMaze  = GlobalAddAtom( "Maze" );
               aThisMaze = GlobalAddAtom( sCaption );
               aBall     = GlobalAddAtom( "Ball" );
               aWnd      = GlobalAddAtom( "Window" );
               aGrab     = GlobalAddAtom( "Grab" );
               aStat     = GlobalAddAtom( "Statistics" );

               /* initiate DDE conversation on ball */
               Maze.bInitiate = TRUE;
               DDE_INITIATE( -1, aAnyMaze, aBall );
               Maze.bInitiate = FALSE;

               /* seed random number & display maze */
               srand( hInst );
               ShowWindow( hWnd, wCmdShow );

          } else
               exit( 2 );

     } else
          exit( 1 );

     /* return final result */
     return( hWnd );

}


/*
 * BOUNCE BALL IN DDE MAZE
 *
 *     hWnd     maze window handle
 *
 * This function animates the DDE Maze by moving the ball around the
 * window. If the ball "falls" into a "hole" a DDE message is
 * posted to the appropriate client and the ball transferred. In
 * addition, various summary statistics are also provided for those
 * interested listeners.
 *
 */

static VOID BounceBall( hWnd )
     HWND     hWnd;
{
     WORD     i;            /* client channel number */
     HDC     hDC;           /* temporary display context */
     LONG     lElapsed;     /* elapsed time ball was bouncing */

     /* check if ball inside hole */
     for ( i=0; (i<Maze.wLinks)&&(OUTSIDE_HOLE); i++ );

     /* animate ball if not in hole */
     if ( i == Maze.wLinks ) {

          hDC = GetDC( hWnd );

          /* Note - you could erase the bouncing ball here using an
           * InvertRect call with the current ball position. Leaving it
           * out results in an interesting visual effect!
           */

          Ball.iHorzMotion = H_BOUNCE( Ball.rPosn.left, Maze.wWidth );
          Ball.iVertMotion = V_BOUNCE( Ball.rPosn.top, Maze.wHeight );

          /* compute new ball position & draw */
          Ball.rPosn.top += Ball.iVertMotion;
          Ball.rPosn.left += Ball.iHorzMotion;
          Ball.rPosn.right += Ball.iHorzMotion;
          Ball.rPosn.bottom += Ball.iVertMotion;

          InvertRect( hDC, &Ball.rPosn );
          ReleaseDC( hWnd, hDC );

     } else {

          /* pass ball to client */
          Ball.bIsBouncing = FALSE;
          DDE_DATA(
               Link[i].hWnd,
               aGrab,
               FALSE,
               (LONG)Maze.wNum,
               (LONG)((GetFocus()==hWnd)?TRUE:FALSE)
          );

          /* calculate animation statistics */
          lElapsed = GetCurrentTime() - Ball.lTimeIn;

          /* inform all statistics clients */
          for ( i=0; i<Maze.wLinks; i++ )
               if ( Link[i].bAdviseStat )
                    DDE_DATA(
                         Link[i].hWnd,
                         aStat,
                         FALSE,
                         lElapsed,
                         DISPLAY_WIDTH*(LONG)DISPLAY_HEIGHT
                    );

          InvalidateRect( hWnd, NULL, TRUE );

     }

}

/*
 * ADVISE SERVER TO SEND DATA
 *
 *     hToWnd       client window handle
 *     hFromWnd     server window handle
 *     aItem        atom representing item
 *
 * This function enables the calling routine to advise a client window
 * regarding a particular item of data. This function assumes that
 * the data is to be sent in CF_TEXT format without requiring any
 * acknowledgement. A value of TRUE is returned if the function is
 * successful.
 *
 */

static BOOL Advise( hToWnd, hFromWnd, aItem )
     HWND     hToWnd;
     HWND     hFromWnd;
     ATOM     aItem;
{
     /* local variables */
     HANDLE   hMem;            /* temporary memory handle */
     LPDATA   lpData;          /* pointer to data structure */
     BOOL     bResult;         /* result of function */

     bResult = FALSE;

     /* allocate memory & check if succeeded */
     hMem = GlobalAlloc( GHND|GMEM_DDESHARE, (DWORD)sizeof(DATA) );
     if ( hMem ) {

          /* lock the data - may not suceed with expanded memory */
          lpData = (LPDATA)GlobalLock( hMem );
          if ( lpData ) {

               /* define data structure constants */
               lpData->fAck     = FALSE;
               lpData->fNoData  = FALSE;
               lpData->cfFormat = CF_TEXT;

               /* unlock prior to sending */
               GlobalUnlock( hMem );

               /* notify server to send data */
               bResult = PostMessage(
                    hToWnd,
                    WM_DDE_ADVISE,
                    hFromWnd,
                    MAKELONG(hMem,aItem)
               );

          } else
               GlobalFree( hMem );

     }

     /* return result */
     return( bResult );

}

/*
 * TRANSMIT DDE DATA TO CLIENT
 *
 *     hToWnd       destination window handle
 *     hFromWnd     server window handle
 *     wMsg         message number to use
 *     aItem        atom representing data item
 *     bResp        data in response to a request
 *     l1           first portion data item to send
 *     l2           second portion data item to send
 *
 * This function enables the calling routine to transmit data to a
 * client window using either a DDE_DATA or DDE_POKE messgae. It is
 * assumed that the information is sent in CF_TEXT format and does not
 * require the client to respond. A value of TRUE is returned if the
 * entire process is successful.
 *
 */

static BOOL Transmit( hToWnd, hFromWnd, wMsg, aItem, bResp, l1, l2 )
     HWND     hToWnd;
     HWND     hFromWnd;
     WORD     wMsg;
     ATOM     aItem;
     BOOL     bResp;
     LONG     l1;
     LONG     l2;
{
     /* local variables */
     HANDLE     hMem;          /* temporary memory handle */
     LPDATA     lpData;        /* pointer to data structure */
     BOOL     bResult;         /* boolean result value */
     char     sString[32];     /* local string variable */

     bResult = FALSE;

     /* allocate memory & check if succeeded */
     hMem = GlobalAlloc( GHND|GMEM_DDESHARE, (DWORD)sizeof(DATA) );
     if ( hMem ) {

          /* lock the data - may not suceed with expanded memory */
          lpData = (LPDATA)GlobalLock( hMem );
          if ( lpData ) {

               /* define data structure constants */
               lpData->fAck      = FALSE;
               lpData->fRelease  = TRUE;
               lpData->fResponse = bResp;
               lpData->cfFormat  = CF_TEXT;

               BumpGlobalAtom( aItem );

               /* format the data */
               sprintf( sString, "%ld\t%ld", l1, l2 );
               lstrcpy( lpData->info, (LPSTR)sString );

               /* unlock prior to sending */
               GlobalUnlock( hMem );

               bResult = PostMessage(
                         hToWnd,
                         wMsg,
                         hFromWnd,
                         MAKELONG(hMem,aItem)
                    );

          } else
               GlobalFree( hMem );

     }

     /* return result */
     return( bResult );

}

/*
 * DESTROY DDE MAZE
 *
 *     wQuit     application exit code
 *
 * This function destroys all resources consumed by the Maze during
 * execution. Included is the removal of all global atoms. A final
 * exit code is returned by the function.
 *
 */

static WORD DestroyMaze( wQuit )
     WORD     wQuit;
{

     /* remove global atoms */
     GlobalDeleteAtom( aAnyMaze );
     GlobalDeleteAtom( aThisMaze );
     GlobalDeleteAtom( aBall );
     GlobalDeleteAtom( aWnd );
     GlobalDeleteAtom( aGrab );
     GlobalDeleteAtom( aStat );

     /* return final exit code */
     return( wQuit );

}

/*
 * FIND DDE LINK
 *
 *     pwNdx    index to link
 *     hWnd     window handle of caller
 *
 * This function finds the link references by the window handle
 * provided. The resulting link is returned to the caller. The
 * function returns TRUE if the window handle was found.
 *
 */

static BOOL FindLink( pwNdx, hWnd )
     WORD *   pwNdx;
     HWND     hWnd;
{
     WORD     i;

     for (i=0; i<Maze.wLinks; i++)
          if ( Link[i].hWnd == hWnd ) {
               *pwNdx = i;
               return( TRUE );
          }

     return( FALSE );

}

/*
 * BUMP GLOBAL ATOM
 *
 *     aAtom     atom to bump
 *
 * This function increments the reference count to the specified
 * global atom. It is assumed that the atom supplied is defined and
 * valid. The value supplied is returned.
 *
 */

static ATOM BumpGlobalAtom( aAtom )
     ATOM          aAtom;
{
     char          sName[64];

     GlobalGetAtomName( aAtom, sName, sizeof(sName) );
     return( GlobalAddAtom(sName) );
}


Figure 18:  Monitor Function

    Monitor( hWnd, Arguments )
    HWND    hWnd;
    struct {
        char    sStr[64];
    } Arguments;
        {
           char    sMessage[64];

           /* format and display debug message */
           sprintf( sMessage, Arguments );
           MessageBox( hWnd, sMessage, "Your Caption", MB_OK );

      }


Figure 19:  Using Monitor

Monitor( hMazeWnd, "WM_DDE_ADVISE: fAck=%u", lpOptions->fAck );
Monitor( hWnd, "WM_DDE_DATA: aItem = %u", aItem );

The monitor Function in Figure 18 can serve as a rudimentary debugging aid
for DDE applications. Figure 19 shows some examples of its use.

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

Designing for Windows: An Interview with the Microsoft Excel Developers

Bringing Microsoft Excel to the PC environment took more than simply
porting the program from the Macintosh version. The project team wanted to
design a spreadsheet that included a fully device-independent, graphics-
based product that would have not only the look and feel of the Macintosh
version, but would also provide substantial improvements in terms of speed
and functionality. To accomplish these objectives, the spreadsheet program
was totally rewritten using the Windows environment.

Microsoft Systems Journal spoke to the design team to learn more about the
planning, development, and cooperative effort that went into the complex
job of creating Microsoft Excel.

MSJ: What were the original goals of the Microsoft Excel project? How were
they formulated, and how were they unique to this project?

The original goal was to port the Macintosh(TM) version of Microsoft Excel
onto Windows. We realized that we had a great product running on the
Macintosh and wanted to take the opportunity to run it under the same kind
of graphical user interface on PCs.

As part of formulating the project goal, we tried to understand the unique
needs of PC users. We paid more attention to the features that were
available in existing spreadsheet products like Lotus(R) 1-2-3(R) and made
sure that our software was at least compatible or consistent in those
features. Then we added the extra features that made the Mac version of
Microsoft Excel such a success.

Microsoft Excel was developed to run under Microsoft Windows. In
what areas did Windows help the project and in what ways did it hurt?

Clearly Windows helped the project because it served as the graphical
environment that Microsoft Excel needs to run under. Windows is a full,
bit-mapped, fully supported graphical environment that includes device-
independent drivers for all the various devices that exist in the PC
world. From that standpoint, we couldn't have developed Microsoft Excel
without a product like Windows: the amount of work required to provide the
rich environment that you get with Windows would have been prohibitive.

Windows hindered us in the sense that it turned out to be a very complex
environment. Part of that is because the PC world itself is so complex──the
number of devices you have to support and the problems you have in being
device-independent makes it more difficult than the Apple world.

Microsoft Excel runs under Windows Version 2.0 and Windows/386, and it
will run under OS/2 shortly. What were the considerations in planning for
the longevity and portability of the program, and did this make the task
more difficult?

The point is that the Macintosh environment is different than the Windows
environment, and the Windows environment is different than the
Presentation Manager environment. Often you don't understand those
differences until you actually have to resolve them as we did on this
project. We've learned a great deal by moving between Macintosh and
Windows as far as understanding Microsoft Excel and how to make it more
environment-independent. We learned things we never would have guessed at
the time we started to port to Windows.

We use the concept of core code in porting from one environment to
another──that is, you separate out as much as possible that is not
environment-specific. You then take the code that is environment-specific
and try to isolate what is different about the environments and how the
code can be adapted to move between them.

Microsoft Excel will soon be modified to run under the Presentation
Manager in the OS/2 environment. The Presentation Manager is somewhat
different from Windows. Microsoft Excel will have to be changed to support
those differences──it will certainly run under the Presentation Manager
when that's ready. We feel that, after putting Microsoft Excel through two
environments, the third will be much easier.

Some of you were involved in the development of Microsoft Excel for the
Macintosh at Microsoft. What was most different about the PC project and
what did the Mac experience teach you that helped you on this project?

The aspect that was the most different on the PC project was how we
changed our memory management. There were several reasons to do that. One
reason was to get better addressability than Microsoft Excel for the
Macintosh had. Even more important was how you get more addressability
working in the PC world──currently, the usual way of doing this is by using
EMS boards. There's a requirement that typical boards have a 64Kb window,
and they can have four pages in a window. For efficiency reasons, you want
to keep your pages at less than 64Kb. Often you keep them around 16Kb so
you can have as many pages as possible. The entire design of our data
structure had to change to support that type of chunkiness.

How was the Microsoft Excel project divided into tasks?

It was divided into tasks by doing a careful examination of Microsoft
Excel for the Macintosh and creating a plan for moving the code that
existed in Microsoft Excel for the Macintosh over onto Windows. That was
assigned to various people by expertise. We used a large number of very
experienced people──people who had worked on the Macintosh product and
understood graphical user interfaces and environments. Essentially, we
assigned various areas according to expertise that people had. For
instance, an area like charting was assigned to one person who understood
how it worked in the Mac environment and became even more expert in that
area as it was modified to work under Windows.

What is your ideal size for a project team?

The team varied between eight and ten people. Somewhere after ten people
the project starts to get large: you start to develop a problem with
communication and your bandwidth starts slowing down. The more people that
are involved, the more time you have to spend informing them of changes
and making sure everyone understands what everyone else is doing. We
prefer to keep project teams under the ten-person limit if possible. If we
can't, we create separate subprojects and make very good, well-defined
interfaces between the subgroups so that they aren't required to interact
as much.

Tell us about some of the secrets and strategies that went into ensuring
that the job got done.

Our strategy is to give people a lot of responsibility so that they feel
personally involved in what they're doing and so they can take some real
pride in what they've created. We don't legislate around here, telling
people how to resolve their problems or force-feeding designs that they
must implement. It's more a situation where people become experts in the
area they are working in and they take the initiative. They also take the
responsibility for creating the solution and for getting it done on time.

Part of that is that the team members participate in the planning as well
as the schedule of the project. They make commitments as to what they will
do and how long it will take them; it's a bottom-up design. Somebody is
doing a very careful analysis of the work involved and then making a
commitment to finish that work.

Obviously, you made a major commitment to guarantee that users of 1-2-3
would have a smooth transition to Microsoft Excel. Did you sacrifice some
other features in order to accommodate Lotus users?

We made a careful analysis of the features that were available in Lotus 1-
2-3 Version 2. We made sure that we were functionally complete so that
current Lotus users wouldn't feel they had to give up some feature that
was useful to them. Adapting the Macro Translator was a very large task.
The macro language in Microsoft Excel works in a fundamentally different
way than 1-2-3 macros because we are not keystroke-based. We also didn't
map the Lotus interface directly like some other products do. We had to
have an intelligent program that could go through and figure out what the
intent of a Lotus-type macro was and then convert it into the same kind of
macro running under Microsoft Excel.

If there hadn't been a 1-2-3 standard, we probably would've done certain
things in different ways──ways we felt were more efficient and better
designed. But because we believe that Lotus 1-2-3 is a standard and that a
large group of people understand 1-2-3 and are comfortable with it, we
needed to work to that standard. So we couldn't put in all the features we
would have liked.

How important was the "make it faster" element of the project? How did you
determine when the program was as fast as you felt it was going to get?

Speed is one of the most critical issues in a spreadsheet. This is
something we learned with Microsoft Excel for the Macintosh; it's one of
the reasons it was received so well. We concentrated on speed, and the
users felt the program was what we call "crisp." It had a quick response
to what you wanted to do with it.

Because of that, we always set speed as one of our number one priorities
with Microsoft Excel. The result was that we did very serious benchmarks
of existing programs and of Microsoft Excel for MS-DOS(R) as we built it.
Every time we did a testing release, we would run all the benchmarks. If
anything had gotten slower we would find out why. Using the benchmarks let
us set some goals; using them consistently allowed us to meet those goals.
We did as much as we could using recurrent design, without throwing away
everything to squeeze all the speed out. The way you do that is to run
benchmarks, find areas you believe are slow, and then go in  and do a
detailed analysis using profiling tools. From the analysis you can
discover what is slow about it. You can then decide whether to do local
redesign if necessary or to write better code for that area.

In a graphical interface the areas that get slow are not those that do
the recalculation. What does get slow is the display──just moving those
bits on the screen. We spent a great deal of time understanding where all
the time was going in redisplaying to the screen. As it turns out, a high
percentage of the time spent in scrolling is spent moving the bits on the
screen from one place to another. The reason for this is that an Enhanced
Graphics Adapter (or EGH), which is one of our target devices, has a lot
of wait states. Just sliding over one column and moving those bits takes
a high percentage of time because the EGA card is slow. We understand it
very well and have done a great deal to make it as fast as possible.

Among other "firsts," Microsoft Excel was the first to implement Windows'
DDE (Dynamic Data Exchange) protocol. How difficult was this part of the
project, and what did you learn about implementing DDE?

Conceptually DDE was very simple. We developed a design, described what we
needed it to do, and were able to come up with a very small subset of nine
commands that would accomplish everything we needed.

The implementation, on the other hand, was quite difficult. Because DDE is
an asynchronous communication protocol, the product has to be able to
handle messages that can be sent to it at any time, no matter what the
product is doing. This is where the complexity came into it. Microsoft
Excel wasn't originally designed to be able to service messages at any
time. We needed to be able to go through and understand how we could build
in that capability. We needed to be able to understand changes in design
that would allow the program to queue a message or service it, to handle
it while inside the macro language, or to handle it during recalcs and
other time-intensive tasks. Implementation took far longer than we
expected but, in hindsight, it was because we didn't at first consider all
the issues involved in supporting this type of protocol.

The Microsoft Excel project took three years. How important was group
morale during that time?

Incredibly important. You can't do large projects that are this complex
without people being motivated and feeling a sense of ownership. We have
always had very good morale on all of our projects because people feel it
is their work that's being done.

Part of keeping the morale up is the fact that the people who are writing
the code are also involved in understanding what the features are.
Features are not legislated by someone else who writes a spec and says,
"Well we're just gonna do these features." The development group is
actively involved in doing  an analysis of those features. They try to
understand what we're trying to accomplish by including a given feature
and make suggestions about why it's good or bad.

Someone who does the wrong job usually knows it. It's very seldom the
situation where somebody has been going in the wrong direction for six
months and you say to the person, "This is terrible." Instead, a guiding
hand is offered along the way. It helps to clear the smoke when it gets to
be too much: point out what is important, perhaps make note of a better
solution, or suggest stepping back and thinking about what's been done
before proceeding forward.

Managing the project is an interesting job from the standpoint that it is
interrupt-driven. With the amount of experience and expertise on our
project teams we don't need to tell them exactly where the project is
going. We do need to watch what's happening and be involved in seeing
their decisions. We can then go about making sure that if something is
being missed or, more likely, if another group has discovered something,
that it is transmitted between groups. It must be understood that if a
group has a problem with resources, then we can get the resource for them.
If we need to change a schedule, we understand why and it's transmitted to
the project team.

Did one phase in this project stand out as being especially difficult to
manage?

The most difficult phase is trying to set expectations. There's a tendency
when you start a new project to want to do as much as you can. You've got
to balance that out against the time frame, when you'd like to deliver a
product, and what your windows of opportunity are. Microsoft Excel is not
done. We've got hundreds of other features we'd like to put into the
product.

Part of the job was to be involved in the tough decisions of looking at a
large list of possible features and saying, "This isn't important today
. . . but this is." That is by far the toughest part of the job: setting
the expectations, making sure we're doing the right thing and then going
ahead and focusing the team on realistic goals.

Was there a feature of the project that nearly got carried away?

There was nothing that really got carried away because we had the
advantage of having experienced developers working on this project.
They've been through this and know what it's like to say "Oh, that's
pretty easy" and then spend eight times longer on it than they expected.
There's a tendency to do more of an analysis and understand what's
involved before making a commitment.

How was the development of Microsoft Excel aided because of the close
proximity of the other groups, particularly the Windows and OS/2 groups at
Microsoft?

Having the Windows group close by probably helped for several reasons.
First of all, it allowed us to actually discuss interface issues. Some of
the changes that have been done for Windows 2.0 are the result of necessary
changes needed in the interface. The multiple document interface (MDI) is a
result of ours and some other ISDs. We felt it was a necessary feature to
maximize screen space for the user.

We were able to help influence the design of Windows in the same way we
were able to influence the Macintosh interface. We were actively building
complex products and had many suggestions about how to make it better. The
fact that they were "across the pond" didn't help that much──we could have
done it on the phone or with visits, as we did with Apple.

Proximity did help in fixing bugs. We were able to help the Windows group.
If we found a bug and thought it was in their code, we could isolate it
and have a Windows expert come over, look at it and see the problem. That
paid off more than anything else──really being able to get into the code
and provide assistance and communications between the two groups.

What role did Bill Gates play in the development of Microsoft Excel?

Bill was the visionary. He was one of the people who was able to say
"We've got to have a feature like this, it's very important." He helped
guide the formulation of what the product became by having those beliefs
and having reasons for those beliefs, and for being able to communicate
them.

We've been working with Bill for so long that we can now ask many of the
same questions of ourselves that Bill might ask. We can do the right
design by being more critical of ourselves. Bill, over time, has showed us
that we shouldn't be complacent and that we should think about what we are
doing. When we went into a meeting he used to ask questions we felt
foolish about because we had no answer. We've been through enough of those
situations that we're now able to ask ourselves those same questions and
answer them before we go into those meetings.

In retrospect, is there anything you would have done differently?

It would have been nice if some things had proceeded more smoothly, but
there is really nothing we would have done differently. This project was
organized using some new philosophies and new approaches to development.
While the implementation didn't always go as smoothly as we'd hoped (just
because nothing ever does), it did go exactly as we'd hoped as far as the
results we got, the way the project ran and the management. We're going to
continue to refine this same approach.

How does the group feel now that another flavor of Microsoft Excel
is out the door?

This is the start of Microsoft Excel. We've seen what it looks like on a
Mac and we've seen what it looks like on Windows. More importantly, we
have so many ideas and so many new things we'd like to implement that this
seems like just the start of a long history of great spreadsheets.

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

A Strategy for Building And Debugging Your First MS-DOS Device Driver

───────────────────────────────────────────────────────────────────────────
Also see the related articles:
  The Xmodem Protocol: Error-Free Transmission a Block at a Time
  Sample Code Fragments for MDM_DRV.ASM
───────────────────────────────────────────────────────────────────────────

Ross M. Greenberg☼

Writing device drivers is a difficult, tedious task that only operating
system gurus should ever consider. Those who attempt this feat are
eligible for extra hazard pay, must have their parents' or guardians'
permission if under age, sign certain documents relinquishing their right
to sue in the event of injury, and have at least $1 million in liability
insurance.

That may be the prevailing thought, but writing device drivers really
isn't that different from writing other programs, nor is it substantially
more difficult. This article will examine the inner workings of device
drivers, and offer some useful debugging tips and techniques.

The device driver built in this article, MDM_DRV, serves as a rudimentary
communications program, including Xmodem/checksum protocol transfers.
Although it expects to see another copy of itself on the other end of a
transfer, through a modem or hard-wire connection, it can download files
from a variety of bulletin boards and conferencing systems.

A device driver is the logical interface between the operating system I/O
subsystem and the underlying physical hardware. In some cases, the device
driver replaces or extends certain aspects of the BIOS, and thus is
responsible for administering the underlying hardware characteristics. In
other device drivers, the actual physical I/O is done solely through the
BIOS. Early versions of MS-DOS(R) did not allow for easy installation of
device drivers. Starting with Version 2.0, however, the independent
programmer could easily integrate additions into the operating system.
Because the operating system interface for device drivers is rigidly
defined, as opposed to the lack of standards for terminate-and-stay-
resident (TSR) programs, most device drivers will work with other device
drivers installed.

Device drivers come in two flavors: character and block oriented. Device
drivers that allow storage and/or retrieval of data on disks or other
types of random-access devices, are usually block-oriented device drivers.
Block device drivers transfer data in fixed amounts──hence their name.

This article will concentrate on character device drivers. Several topics
that are covered here are related to the TSR serial─interrupt program,
TSRCOMM.ASM, discussed  in "Keeping Up With the Real World: Speedy Serial
I/O Processing," MSJ, Vol. 2 No. 3.

Character device drivers must be able to operate in either of two modes:
cooked or raw. Cooked mode makes simple single-character-at-a-time
requests to the device drivers and processes certain characters, such as
carriage returns, line feeds (which may or may not be translated), Ctrl-
Z's, and Ctrl-C's in a special way. A status check is made on each I/O
request to determine if a Ctrl-C has been struck on the keyboard, and a
device driver in cooked mode allows MS-DOS to terminate the driver call if
it sees one. Since it is not required that the calling program know
whether the device is in cooked or raw mode, MS-DOS must be able to
adequately and properly translate a typical multiple byte I/O request from
the higher-level application program into the correct number of single
byte requests. MS-DOS does this by using internal buffers and finally
transferring the data into the user buffer when the request is complete.

In raw mode, no translation or other special service is expected of the
device driver or provided by MS-DOS. When in raw mode, there is no
granularity in the number of bytes requested for I/O: the device driver
must be able to properly handle a request for the specified number of
bytes. Usually, these requests do not require any internal MS-DOS buffers
and are processed in the application program's own buffer, making raw mode
considerably more efficient than cooked mode.

The user program can change the mode from raw to cooked or vice versa by
using the MS-DOS function call for IOCTL, Interrupt 21 with AH = 44H (see
the MS-DOS Technical Reference manual for more information).


Device Driver Structure

The three major parts of a device driver are the header, the strategy
routine, and the interrupt routine. The header routine describes the
capabilities and attributes of the driver, gives the character device
driver a name, and has NEAR (offset only, single word) pointers to the
strategy and interrupt routines as well as a FAR (offset and segment,
double word) pointer to the next device driver in the chain of device
drivers (the pointer chain only points one way, causing problems in
finding its beginning. The pointer to the next device driver is set by
MS-DOS immediately after the initialization routine is completed and
should be initially set to ──1 (FFFFFFFFH) in the device driver itself.

A device driver is limited to at most 64Kb, since the strategy and
interrupt routines contain only the offsets within whatever segment MS-DOS
assigns to the device driver. A graphical representation of the header can
be found in Figure 1, while a description of the attribute word bit
definitions is seen in Figure 2.


The Strategy Routine

The strategy routine is called when your device driver is first installed
on boot and for each I/O request generated by the operating system. A
single I/O request from the application program can generate multiple I/O
requests to the device driver.

The purpose of the routine is to save an address somewhere for future
processing and then return. This address, passed in the ES:BX register
pair, points to a structure called the request header (see Figure 3),
which contains information that tells the device driver what operation it
should perform.

It is important that no actual input or output operation be performed in
the strategy routine and that the address of the request header be saved
for future processing by the interrupt routine. In a true multitasking
system, this address would probably be saved in some array, and possibly
sorted using a method for optimal device usage when the interrupt routine
is later called. Under MS-DOS, a call to the interrupt routine immediately
follows the strategy routine. A point to consider is that interrupts are
enabled between the strategy call and the interrupt call, which can cause
problems if your device driver is built under the assumption that there is
"zero-time" between the two calls.


The Interrupt Routine

The interrupt routine is where all of the actual work of a device driver
is done and is, therefore, the most complicated part of the device driver
code. When this routine is called, the command byte (the third byte) of
the previously saved RH is examined, and the appropriate action is taken
based upon this value. See Figure 4 for a list of these command bytes and
their implied actions.

The interrupt routine normally uses the command byte as an index into some
dispatch table and then calls the appropriate routine for each command.
Obviously, however, you can introduce a jump table if you wish. The
request header contains all the necessary information for proper
processing of each command and informs the calling program (MS-DOS, in
most cases) of the status of the request when the routine finishes. The
status word itself is broken into a number of fields (see Figure 5). It
has an error bit to indicate that other parts of the status word contain
the particular error condition, a done bit to show that the indicated
operation was completed, and a busy bit that is primarily used to indicate
the current status of the device.

It is generally not sufficient to return only the status of the operation.
The count of the number of characters processed, where appropriate, such
as in a read or write operation, should be returned as well.

Even though the interrupt routine is not actually called as an interrupt
routine──it is called with a FAR CALL and therefore expects a FAR RET──it
should act as if it were a true interrupt service routine, saving all
registers and flags and restoring them when finished.


The Commands

The device driver commands listed in Figure 4 are built into MDM_DRV as
callable routines. The dispatch table, just below the request  header in
the assembler code listing, points to each of the routines the device
driver needs. The table is accessed by the interrupt routine, which uses
the command byte in the request header as an index into the table to find
the correct routine to call.

Once the strategy and interrupt routines are in place, the commands need
to be implemented. This is basically a straightforward procedure, as can
be seen in the code listing. Each command detailed below is found in the
same order in the code.  This structure can serve as a basic model for
almost any device driver, no matter how complicated. For further
information on writing device drivers, see either the MS-DOS Technical
Reference manual or Advanced MS-DOS by Ray Duncan (Microsoft Press(R),
1986).

Not all of the available commands are utilized by MDM_DRV, though they are
all referenced in the code for the sake of completeness. Those that are
not used are simply stubs in the code that do nothing and return.

Each command that is used follows the same general logic: it is called
from the interrupt routine with a near call, it performs its operation,
and returns a status in AX. This status is "or'd" with the done bit, and
then stored into the status word in the original request header at RH+3.
Character counts, if required for a given operation, are updated in the
individual routines. If a routine is not used for this device driver, it
returns a status indicating that it is finished with the particular
operation.

INITIALIZE DRIVER (Command 00H) is called only once, when the driver is
first installed. It should initialize the device as required for future
operation and can, if desired, print a greeting message indicating that it
has been successfully installed. For debugging purposes, you might also
want to have it output its code segment address while developing your
device driver.

You may only call MS-DOS services via Interrupt 21H with AH less than or
equal to 0CH, or with AH set to 30H to get the MS-DOS version number.
Since some of the routines are version-specific, you may want to check the
version number and set some flags or take appropriate action if the wrong
version number is found.

There are specific operations required for block devices, not covered in
this article, which involve setting up certain static tables so MS-DOS
will later know the particular attributes of the block device associated
with the driver.

When the INITIALIZE DRIVER routine is called, the double word pointer at
RH+18 points to whatever text followed after the equal sign in the
CONFIG.SYS line that caused the device driver to be invoked; this allows
you to process options if you wish. In the case of MDM_DRV, the screen
displays this string.

The highest memory address required by the device driver is returned in
the double word at RH+14, which indicates to MS-DOS such information as
where it can load the next device driver. Since the initialization routine
is only called once, its own address can be returned in the request header
for efficient utilization of memory. Remember that anything in memory
after the address you return will most likely be overwritten.

MDM_DRV uses two services available only in MS-DOS 3.x and higher: the
device open and device close routines. Earlier versions of MS-DOS did not
know that these routines exist: since they are required for operation of
the driver, the wrong operating system version causes an exit.

MDM_DRV uses the initialization routine to assure that it is running on an
MS-DOS 3.x system, then prints an error message and exits if this is not
the case. It initializes the modem port to 1200 baud, no parity with eight
data bits and one stop bit.

There are alternatives to exiting if the wrong MS-DOS version is found.
With a little more coding, the first call to a read or write routine could
call the open routine, and the last call, or a suitable pause after a
return from a read or write call, could trigger a device close call. I've
opted not to code this driver for the older 2.x version of MS-DOS; it's
time for everyone to update anyway. Finally, a greeting message is printed
out, and the initialization routine ends.

DEVICE OPEN (Command 0DH) has no defined purpose under MS-DOS, but you
can be sure it will be called in MS-DOS 3.x before any other routine,
provided the appropriate bit in the header attribute block is set (Bit 11).
The MDM_DRV code uses it for several purposes.

When first called it re-initializes the communication port to the default
setting of 1200,n,8,1 (actually, this setting can be changed later via
appropriate calls to the IOCTL function). The data carrier status is then
checked to determine if the port is active and already on-line to another
machine. If on-line, the following dialing and/or answering session is
skipped. If not on-line, a message is output to the screen requesting
either a phone number to dial or a simple return. Depending on whether a
number or other input was entered, the modem sends out a dial or answer
string and awaits carrier. A Ctrl-C causes an immediate exit.

Once carrier is detected, a very simple terminal communications routine is
entered that echoes characters entered by the keyboard to both the screen
and the comm port, and characters found on the comm port are echoed
directly to the screen. When either side of the communications session
enters an escape, both sides leave the loop and continue. Once an escape
is hit, or if there was carrier originally, the timer tick is taken over,
since timing is a critical part of the Xmodem protocol that subsequent
read or write calls will be using. Finally, control is returned to the
originating caller.

READ (Command 04H) is, by far, the most conceptually difficult routine in
MDM_DRV, because of the dual nature that the read routine requires.

The calling routine (in most cases MS-DOS) makes a request to read a
certain number of characters. The actual character count is found in
RH+18. These characters are returned to the calling program by storing
them in the sequential address found at the double word pointer at RH+14.

Xmodem, however, transfers data in fixed blocks of 128 data bytes per
block with some additional protocol information (see the sidebar on
Xmodem). It expects to see an ACK (acknowledgement) or a NAK (negative
acknowledgement) character transmitted by the receiver within a certain
specified time after sending a block to indicate whether that block was
received properly. Consider a situation in which requests are made for one
character at a time. The first call does not have any data awaiting it, so
it must wait for a full block to arrive. If that block does not check out,
meaning that the block number, its complement, and the checksum byte are
not what was expected or that a time-out occurred before there was an
entire block to process, then an immediate NAK is sent and the wait-for-a-
block loop is entered again. If the block does check out, the ACK cannot
be sent until the device is ready to receive the next full block. Now, if
the TSRCOMM program has been implemented, the ACK can be sent. (TSRCOMM
allows for asynchronous interrupts, that is, incoming characters, to be
processed properly and is available on both DIAL and the author's own
bulletin board system at (212) 889-6438.) If an asynchronous interrupt
handler is not present, however, characters will be lost if an immediate
ACK is sent out. Therefore, it is not possible to send out the ACK
immediately; it is not sent until the last character in the block is
properly processed and stored at its appropriate location in the caller's
transfer buffer. This means that subsequent calls for the next byte must
be handled with the knowledge that they are already in the middle of a
block.

This causes a problem, of course, if it takes longer to process 128
characters with driver I/O requests than is allowed between the time the
last character is sent and the ACK or NAK is expected.

What about a case in which a large number of characters are requested?
Again, the read routine must wait for a full block to arrive, then process
as many characters as allowed by the character count in the request header
before returning to the caller. If the read count requires more characters
than exist in the buffer, the routine stays in the get-a-block, process-
character loop until the count is depleted. The routine also keeps track
of how many characters are processed by incrementing the character count
as it stores each character.

When the read request is complete, control is returned to the caller. If
there are too many NAKs and the transfer has to be aborted, control is
returned with an error status set. Control is returned without an error
code, but with a "short count" when an EOT (end-of-transmission) is
received indicating that the transfer has been completed, or when read
requests issued after an EOT have been received.

INPUT STATUS (Command 06H) is not especially valuable for this device
driver, but is included nonetheless. If there is a character available (a
block has been received, but not fully processed), a status of 0 is
returned in the busy bit of the return status word at RH+3. Unless the
read count occurs in 128-byte increments, this routine will normally
return a character ready status after the first read call. There is some
cheating here: if the variable in_block is set, then obviously there are
characters outstanding.

NON-DESTRUCTIVE READ (Command 05H) merely returns what the next character
to be returned would be. Return the character in RH+13, but do not remove
it as that would affect the current input buffer. What to do if the input
buffer is empty when this call is made is not defined. Therefore, an error
code of BH (read error) is returned in the status word at RH+3, a count of
1 is returned at RH+18, and the actual character is returned as a question
mark.

FLUSH INPUT BUFFERS (Command 07H), if called, causes the input buffer to
be flushed. Since the next call may be a read request and the buffer was
flushed, the read will eventually time out, causing a NAK to be sent. The
NAK triggers the last block sent to be sent again, and everything should
continue working well.

WRITE (Command 08H) and the write subsystem are very simple when compared
with the rigors of the read subsystem. When a write request is made, the
number of bytes to be transferred is found at RH+18, as expected. The
starting transfer address is found with the double word pointer at RH+14.
Characters are transferred, one by one, into the output buffer. When the
output buffer has 128 characters in it, the send_block routine is called.
The block is sent until it is either successfully received by the other
end, indicated by the other end sending an ACK, or until enough NAKs or
time-outs have been received to indicate a transmission error.

When there are no characters left to be sent, the processed number of
characters, which may be different from the transmitted count, are
returned in RH+18, and control is returned to the calling program.

WRITE WITH VERIFY (Command 09H) makes little sense for a character device,
especially for one that uses Xmodem. Generally, it should call the write
routine and then attempt to read the data just written, compare them, and
return an error if they do not match. In the case of MDM_DRV, a simple
jump to the WRITE routine suffices.

OUTPUT STATUS (Command 0AH) should set the busy bit if the output device
is busy. Since MDM_DRV is neither buffered nor interrupt driven, this is
impossible: the write routine never returns with any outstanding work to
be done. Therefore, the busy bit is not set when control is returned to
the calling program.

FLUSH OUTPUT BUFFERS (Command 0BH) does nothing in MDM_DRV since emptying
the current output buffer would probably have an adverse effect on the
transfer underway.

DEVICE CLOSE (Command 0EH), like the DEVICE OPEN command,  is only called
if the device driver is operating under MS-DOS 3.x and the appropriate bit
is set in the header attribute word. This routine determines if there are
any outstanding bytes from a write and forces the partial block to be
output via the send_block routine if required. There will probably be
outstanding bytes, since the write routine only sends out bytes in blocks
of 128. When an ACK on the final block is received, an EOT is sent to
indicate that the transaction is complete to the remote side.

This routine is also called in the event of an aborted transfer or after a
Ctrl-C is entered on the console during a transfer. Therefore, the abort
flag is checked; if set, a CAN character is sent over the line, which
indicates to the remote that the transfer has been canceled. Obviously, if
this is the case, any partial outstanding block is not sent. If this was a
dialed call, then the Hayes(R)-compatible hangup string is sent with the
required 1-second guardband around the attention-getting triple plus signs
(+++).

Finally, DTR is dropped on the modem port, the interrupt vectors that were
stolen are returned to the state they were in, and control is returned to
the calling program.

I/O CONTROL READ (Command 03H) along with I/O CONTROL WRITE, allows
information to be passed to the calling program regarding device status.
It is not fully defined and can therefore be used for just about any
purpose.

In MDM_DRV, it passes a copy of the structure that contains the
information shown in Figure 6. This information allows an application
program to define which modem port, for example, MDM_DRV will use. This
routine allows an application program to determine what these parameters
are currently set to.

Note that the count in RH+18 indicates how many bytes you can return on
this call. Since a buffer of a limited length is usually used for this
call, passing more memory to the address specified in the double word
pointer at RH+14 could cause a write into an area that is not properly
allocated. Often, this will be a direct write into the application program
buffer, where a call may only want to return the first word, which
indicates the current communications port, for example, instead of the
entire structure.

I/O CONTROL WRITE (Command 0CH), like its cousin above, permits you to
pass information to and from the device driver. This particular command
sets internal parameters within the device driver.

Its basic use would be to do an I/O CONTROL READ request into a local
buffer first, modify the bytes you want to alter, and then issue an I/O
CONTROL WRITE using the local buffer address and the count of the
characters you wish to update.

CHECK MEDIA (Command 01H), BUILD BIOS PARAMETER BLOCK (Command 02H),
REMOVABLE MEDIA (Command 0FH), and OUTPUT UNTIL BUSY (Command 10H) are not
used by the MDM_DRV driver; however, for completeness, they are
individually included in the device driver. Usually, the address of a
common "null" routine would be used in the dispatch table.


Design Considerations

There are problems inherent in the design and writing of a device driver
unlike those of other programs. In many ways, designing and implementing a
device driver can be likened to a Windows application: your main routines
are called by an outside task, perhaps multiple times, the system will not
continue processing until you give up control, and the interface is rather
rigidly specified.

The first problem is that there is no guarantee that, simply because a
dev_open call was made that there will be a corresponding dev_close call.
Just to make the matter a bit more confusing, the simple MS-DOS COPY
command makes a set of dev_open/dev_close calls before it actually opens
the device for the appropriate I/O instruction. (The actual sequence might
be: dev_open / dev_close / dev_open/ [read|write] / dev_close.)

Another problem inherent in the device driver is the retry count.
Somewhere in the depths of MS-DOS, there is a number that is supposed to
be able to indicate how many retries you wish a device driver to attempt
before it gives up in disgust and MS-DOS outputs the ominous "Abort,
Retry, Ignore?" message (caused by Interrupt 24H, the critical error
interrupt). There does not appear to be a documented method to force only
one attempt on a device open before generating a critical error.
Therefore, there is no graceful way to abort from a "dead" line.

Since MS-DOS may not be called for any purpose within the non-INIT part of
the device driver, only the BIOS may be called for character I/O (direct
keyboard reads and direct screen writes would also work, but make the job
needlessly complex). However, this means that a control-C or control-break
will not function as you might wish, allowing you to exit from the device
open call back to the calling program, MS-DOS prompt, or even the critical
error interrupt, and is only considered another character with no unusual
attributes.

This is, actually, an extraordinarily difficult problem to deal with since
the basic nature of MS-DOS device drivers did not intend for them to be of
an interactive nature. It becomes a challenge which I have not yet had the
opportunity to address. Therefore, once you enter this device driver,
there is no real way out!

That really is the least of the problems created when one tries to do
things with MS-DOS that  it really isn't intended to do. Here's a problem
which had to be addresed as an external program: MS-DOS always opens
character device drivers in cooked mode. If you're transferring a file
which happens to have a control-Z in it, the control-Z will be processed
as an end-of-file character and the file you are transferring will be
closed prematurely. Therefore you must operate the MDM_DRV in raw mode.
Yet, unless you specifically do the programming yourself, you can't
guarantee that there will be an IOCTL call after each open to reset the
device driver to raw mode.

Setting raw mode lasts only for the given file handle's lifetime;
subsequent opens will still be cooked mode, unless you run the small TSR
program called SET_MDM. This program simply intercepts the MS-DOS
interrupt (Interrupt 21H), checks to determine if this is an "open-with-
file-handle" call and, if it is and if the filename pointed to by the
DS:DX register pair matches 'MDM' (followed by a NULL), sets the file
handle (after a successful open) to raw mode. This is done transparently
to the calling program and is quite legal, even if exceedingly ugly.

Every programmer should have a reasonable kludge to compare their worst
kludge to, and I would like to submit SET_MDM as the new standard for your
comparisons. But──as in any kludge──if it works, it is acceptable.


Room for Improvement

Since MDM_DRV is an example of how a device driver functions, the code
itself is not polished, and there are a few areas ripe for enhancement.

For example, the get_num routine does not concern itself with whether a
character entered on the keyboard is a legal ASCII character; it just sits
and waits for either a return or for up to twenty characters to be
entered. Having a nice way to exit from that routine (or from any other
routine in the device driver), as discussed above, would be high on my
list of enhancements. Further, the driver assumes that a Hayes-compatible
modem is attached to the comm port, and that assumption is hard-wired into
the dev_open routine and the dev_close routine. If MDM_DRV is used for
other modems, or with other devices attached to the serial port(s), then
some additional code will be required, perhaps through the IOCTL
functions.

The protocol used in this driver is simple Xmodem checksum. An additional
improvement would be to use an enhanced version of the protocol called
Xmodem CRC. Implementing the required enhancements requires only a little
bit more coding.

An errant abort or EOT character is not treated according to specification,
since it is accepted and acted upon immediately. The specification calls
for the first CAN or EOT to be NAKed to assure that it is not line noise
and only subsequent characters are acted upon.


Debugging

Developing every program requires patience and detective work once a
problem shows up. However, it is difficult to debug and enhance device
drivers without proper debugging tools. Unfortunately, there aren't any
tools readily available for debugging device drivers; you have to create
your own.

The first thing to do before even attempting to debug a device driver is
to have full and adequate backup and to ensure that you have some means of
rebooting your system without having the device driver automatically
installed. My method was to have a CONFIG.SYS on a bootable floppy disk
and boot with that floppy when testing out the device driver. The
following are some hints and ideas used in the creation of MDM_DRV:

  ■ Build the device driver as a normal COM program, and then have a
    simulator that calls each of the routines and lets you examine the
    registers and the request header with almost any debugger. I consider
    this somewhat tedious, and it doesn't allow you to test the device
    driver under "real" situations. But this is a decent starting approach,
    and allows you to move on to the next method.

  ■ Create a skeleton program that has no initialization routine at all.
    Have the interrupt routine simply consist of setting a register pair to
    the address of the request header you saved in the skeleton strategy
    routine. Then call an interrupt that you've reserved for yourself. Here
    is an example of a simple interrupt routine:

      interrupt proc far
        push es
        push bx
        mov  es, cs:[old_segment]
        mov  bx, cs:[old_offset]
        int  60h
        pop  bx
        pop  es
        ret
      interrupt endp

  ■ This implies, of course, that you have some sort of interrupt service
    routine set up later for the interrupt you choose. I used interrupt 60H,
    which is defined as a "user" interrupt. Something like this may suffice:

      int60 proc far
        call real_int_routine
        iret
      int60 endp

    This lets you generate your real_int_routine as a far routine and with
    a FAR return.

    The point of this particular exercise is to permit you to build TSR
    code that simply takes over the specified interrupt and from that point
    operates as if it were a device driver. After you finish debugging it as
    a TSR (which most debuggers, including CodeView(R), make pretty easy),
    you can then swap the code into the actual device driver and you're
    finished, except for the initialization routine.

  ■ The hardware-assisted Periscope debugger  makes it easy for the offsets
    of variables declared public to be associated with any code segment
    desired. If your initialization routine displays the code segment
    address upon boot, it is easy to create a dummy program that gets
    loaded into the debugger with the associated symbol map and then change
    the segment of the symbols to point to the original code segment of the
    driver. From this point, it is as if you are debugging a "normal"
    program.

  ■ Remember that there is a very limited amount of stack space available
    to the device driver when initially called. There is really no way to
    determine how much memory is available to the device driver. Your best
    bet is to save the original stack pointer and segment, then use a
    private local stack, and restore the original stack when you're
    finished. MDM_DRV does this in the interrupt routine.

  ■ Treat the device driver as if it were a hardware ISR: every register
    (including the flag register) must be restored upon exit. Critical areas
    of the device driver should operate with interrupts turned off if
    required.

  ■ Be sure that character transfer counts indicate the actual number of
    bytes transferred. Incorrect counts will cause many strange and less-
    than-wonderful things to happen.

  ■ When in doubt, make sure the device driver is not accessing any memory
    beyond the initial address you passed back in the initialization
    routine.


Figure 1:  Standard Device Driver Header

           00H ╔═══════════════════════════════════════════════╗
               ║          Link to next driver, offset          ║
           02H ╠═══════════════════════════════════════════════╣
               ║          Link to next driver, segment         ║
           04H ╠═══════════════════════════════════════════════╣
               ║             Device attribute word             ║
           06H ╠═══════════════════════════════════════════════╣
               ║         Strategy entry point, offset          ║
           08H ╠═══════════════════════════════════════════════╣
               ║        Interrupt entry point, offset          ║
           0AH ╠═══════════════════════════════════════════════╣
               ║                                               ║
               ║            Logical name (8 bytes)             ║
               ║             if character device.              ║
               ║                                               ║
               ║        Number of units (i byte) if block      ║
               ║         device, followed by 7 bytes of        ║
               ║                reserved space.                ║
               ║                                               ║
               ╚═══════════════════════════════════════════════╝


Figure 2:  Device Driver Attribute Word

╓┌──────┌────────────────────────────────────────────────────────────────────╖
Bit    Description

15     0    -    if block device
       1    -    if character device
14     0    -    IOCTL is not supported
       1    -    IOCTL is supported
13     0    -    (if Bit 15 is 0)     IBM format
       1    -         Not IBM format
       0    -    (if Bit 15 is 1)    Output-Till-Busy is not supported
       1    -         Output-Till-Busy is supported
12     0    -    Undefined
11     0    -    Use DOS 2.x calls only
       1    -    DOS 3.x, calls to Open/Close Device and removable
                 media supported, ignored if DOS is 2.x
Bit    Description
                 media supported, ignored if DOS is 2.x
10     0    -    Undefined
9      0    -    Undefined
8      0    -    Undefined
7      0    -    Undefined
6      0    -    Undefined
5      0    -    Undefined
4      0    -    Undefined
3      0    -    Normal device
       1    -    Special Clock Device
2      0    -    Normal Device
       1    -    Null device driver
1      0    -    Normal Device
       1    -    Standard Output device driver
0      0    -    Normal Device
       1    -    Standard Input device driver


Figure 3:  Request Header Record Layout

rlength db      0             ; 0  - length of pertinent data in header
unit    db      0             ; 1  - unit number (not used in MDM_DRV)
command db      0             ; 2  - the actual command
status  dw      0             ; 3  - return status
reserve db      8 dup (0)     ; 5  - reserved for DOS
media   db      0             ; 13 - media desciptor (not used in MDM_DRV)
address dd      0             ; 14 - doubleword pointer for I/O
count   dw      0             ; 18 - unsigned int count for character I/O
sector  dw      0             ; 20 - starting sector (not used in MDM_DRV)


Figure 4:  Request Header Command Bytes and Their Meanings

╓┌────────┌──────────────────────────────────────────────────────────────────╖
Command
Byte     Meaning

00H      Initialize the Device Driver. Called only on installation
         of the device driver at bootup time.

01H      Media Check. Returns a status indicating if the current
Command
Byte     Meaning
01H      Media Check. Returns a status indicating if the current
         media is changed (not used in MDM_DRV).

02H      Build the BIOS Parameter Block. Called when new media
         or media changed, this call should build the BPB and return
         its address (not used by MDM_DRV).

03H      Read IOCTL. Copy certain device driver information into
         a local buffer.

04H      Read. Retrieve a certain number of characters from the
         device driver tables.

05H      Non-destructive character read. Return the next character
         from the device, without removing it from the input buffer.

06H      Input Status. Return a status indicating if there is a
         character waiting in the input buffer.

Command
Byte     Meaning

07H      Input Flush. Empty the input buffer of the device.

08H      Write. Output a certain number of characters to the device.

09H      Write Verify. Output the characters, then reread to check
         output was correct.

0AH      Output Status. Return a status indicating if device is busy
         or free to output.

0BH      Flush Output Buffers. Empty the output buffers
         associated with the device.

0CH      Write IOCTL. Copy certain device driver information
         from a local buffer into the device driver tables.

0DH      Device Open (DOS 3.x only). Initialize the device.

Command
Byte     Meaning

0EH      Device Close (DOS 3.x only). Called preparatory to
         closing the file handle.

0FH      Removable Media Routine (DOS 3.x only). Block
         devices only.

10H      Output Until Busy (DOS 3.x only). A weird hybrid of the
         normal write function, it allows an "incomplete" output call.


Figure 5:  Device Driver Status Word

     15   0         No error
          1         Error
     14             Reserved
     13             Reserved
     12             Reserved
     11             Reserved
     10             Reserved
     9    1         Device BUSY
     8    1         Operation Completed (May still have an error
                                        condition, in which case this
                                        bit is ignored)
0──7  Specific Error Codes:
     0H   Write Protect Violation
     1H   Unknown Unit Requested
     2H   Drive Not Ready
     3H   Unknown Command Encountered
     4H   Data Error on Read
     5H   Illegal I/O Request Structure
     6H   Error on Seek Request
     7H   Unknown Media Encountered
     8H   Requested Sector Not Found
     9H   Printer Error (Paper Error)
     AH   Write Fault on Device
     BH   Read Fault on Device
     CH   General Failure on Device
     DH   Reserved
     EH   Reserved
     FH   Media Change Invalid


Figure 6:  IOCTL Structure for MDM_DRV

com_port        dw      0               ; The comm port we are
                                        ; communicating with.
                                        ; COM1 = 0, COM2 = 1, etc.
init_data       db      10000011b       ; The initialization byte. See
                                        ; the BIOS Technical Reference
                                        ; manual for details.
block_num       dw      0               ; Current block being
                                        ; transferred.
abort_xfer      dw      0               ; Whether the current transfer
                                        ; is to be aborted.
                                        ; (TRUE = YES)
nak_cnt         dw      0               ; How many NAKs have been
                                        ; sent or received.
cancel_flag     dw      0               ; Was a Ctrl-C hit?
inrequest       dw      0               ; The number of bytes not yet
                                        ; processed.
in_block        dw      0               ; Has a block been received?
was_dialed      dw      0               ; Modem was dialed.


───────────────────────────────────────────────────────────────────────────
The Xmodem Protocol: Error-free Transmission a Block at a Time
───────────────────────────────────────────────────────────────────────────

Xmodem transfers data in blocks of 128 bytes, one block at a time. Each
block has certain information added to it to ensure that the transmission
of data is error free. Often called a "checksum," this additional
information is a calculation of the data contained within the block. The
initial calculation is done by transmitting half of the conversation. The
receiving half does a similar calculation of the data when it gets the
block. It then compares its calculated result with the original
calculation, which is sent by the transmitter as part of the information
added to the block. If the two calculated values differ, the receiver will
reject the block, since it knows it received different data than what was
sent. To reject the block, the receiver merely tells the transmitter it
was received improperly, and the transmitter will then transmit the block
again. If there are too many errors, then either side may tell the other
to cancel the transfer.

The Xmodem protocol is a receiver-driven protocol; the transmitter waits
for the receiver to request that a block be sent. The request indicates
whether to send  the next block or the last block sent.

At the head of the block is an SOH (ASCII 01H), followed by a single byte
that represents the block number. The block number is followed by the
one's complement (a logical NOT) of the block number, then the 128 bytes
of data. Finally, the trailing byte is a one-byte additive checksum of the
128 data bytes. Other flavors of the Xmodem protocol include a more
complicated two-byte checksum, which is actually a cyclical redundancy
check (CRC) calculation on the 128 data bytes.

When the receiver is first invoked, it sends out a NAK (ASCII 15H), which
the transmitter uses as a synchronization byte in Xmodem checksum
transfers. If the character "C" is sent instead of the NAK, the
transmitter will use Xmodem CRC. In any case, the transmitter sends the
first block of the file, which starts at block number 1, when it receives
the NAK or the "C", or it issues an appropriate message if a reasonable
amount of time has elapsed without either being received (a time-out).

After each block is sent, the receiver performs the same calculation on
the data bytes and checks its internal checksum or CRC against the block's
version. If they differ, there was an error in transmission.

Most receivers  ignore all incoming characters until an SOH is seen, so
the SOH is certainly part of the checking scheme, allowing certain line
noise to be ignored. The block number is checked against its complement to
ensure that the block arrived intact. If the block is the one expected, an
ACK (ASCII 06H) is sent, and the data is processed, usually by saving it
in a local buffer, which is eventually written to disk. Since many disk
controllers turn off interrupt processing, the disk write is often
performed before the ACK is sent so that no characters are missed.

If the block number indicates this is the last block successfully
received, then an ACK is also sent, but the block is not processed or
saved. This happens if the remote transmitter never received an ACK for
the indicated block.

A NAK is sent if a full block is not received within the time-out period,
the next character within a block is not received within a shorter time-
out period, or any of the above checks indicate that there was an error in
transmission.

When the transmitter receives a NAK instead of an ACK, it bumps up a NAK
count, which is reset upon receipt of an ACK. When this count exceeds some
number, the transmitter will send a CAN character (ASCII 18H) to the
receiver and abort the transfer. In a protocol that is not robust, the
receiver will immediately abort the transfer, usually throwing away
whatever data has been transmitted. In the robust protocol, the CAN is
NAKed, the transmitter resends the CAN, and then both sides of the
communications session will abort the transfer. If the NAK count was not
exceeded, however, the last block sent is simply sent again. If neither an
ACK nor a NAK was received, a time-out will eventually occur, which is
handled exactly as if a NAK had been received.

Eventually, either the transfer is aborted or the end-of-file (EOF) on the
file being transmitted is reached. When EOF is reached, an  end of
transmission (EOT ASCII 04H) is sent. Again, if a nonrobust version of
Xmodem is used, the EOT is immediately acted upon. In a robust Xmodem
implementation, a NAK will be sent and the next EOT received will be acted
upon.

Xmodem is the single most popular protocol used in communications and file
transfers in the PC market for several reasons: it was developed early
(back in the seventies, the formative days of PCs), it is simple to
implement and design, it is effective, and most important of all, it
works.


───────────────────────────────────────────────────────────────────────────
Sample Code Fragments for MDM_DRV.ASM
───────────────────────────────────────────────────────────────────────────

;;           MDM_DRV.ASM     An XMODEM Device Driver
;;                           Written by Ross M. Greenberg
;;                      Sample Code Fragments Follow
                                    ∙
                                    ∙
                                    ∙
code         segment                       public 'CODE'
;;           The driver itself is one large procedure, called 'driver'

driver       proc   far
             assume cs:code, ds:code, es:code; segments should
                                             ; point to code seg
             org    0                        ; drivers must start
                                             ; at offset of zero

;;                  DEVICE DRIVER HEADER

header1      dd     -1                     ; Must be set to -1. Filled in by
             dw     0e800h                 ; DOS character device,
                                           ; IOCTL supported,
                                           ; Output till busy,
                                           ; open/close/rm supported
             dw     strat                  ; point to the strategy routine
             dw     ints                   ; point to the interrupt routine
             db     'MDM     '             ; The name of device. Left
                                           ; justified, space filled to
                                           ; eight characters

;;                  THE REQUEST HEADER

request      struc
rlength      db     0                   ; 0  - length of data in header
unit         db     0                   ; 1  - unit number (not used)
command      db     0                   ; 2  - the actual command
status       dw     0                   ; 3  - return status
reserve      db     8 dup (0)           ; 5  - Reserved for DOS
media        db     0                   ; 13 - Media desciptor.(Not used)
address      dd     0                   ; 14 - Doubleword pointer for I/O
count        dw     0                   ; 18 - unsigned int cnt for char I/O
sector       dw     0                   ; 20 - starting sector. (Not used)
request      ends

;;                  The dispatch table

dispatch:
             dw     init                ; 0x00 - init driver
             dw     media_chk           ; 0x01 - media check (Not used)
             dw     bld_bpb             ; 0x02 - Build the BPB (Not used)
             dw     rd_ioctl            ; 0x03 - Read IOCTL
             dw     read                ; 0x04 - Read 'count' characters
             dw     nd_read             ; 0x05 - Non-destructive char read
             dw     inp_stat            ; 0x06 - Input Status
             dw     inp_flush           ; 0x07 - Input Flush
             dw     write               ; 0x08 - Write count chars to
device
             dw     write_vfy           ; 0x09 - Write Verify
             dw     out_stat            ; 0x0A - Output Status
             dw     out_flush           ; 0x0B - Flush Output Buffers
             dw     wrt_ioctl           ; OxOC - Write IOCTL
             dw     dev_open            ; OxOD - Device Open (DOS 3.x)
             dw     dev_close           ; 0x0E - Device Close (DOS 3.x)
             dw     rem_media           ; 0x0F - Removable Media Routine
                                        ;        (DOS 3.x)
             dw     out_busy            ; 0x10 - Output Until Busy (DOS 3.x)

;;                  STRATEGY ROUTINE

strat        proc   far
             mov    word ptr cs:[rh_ptr], bx
             mov    word ptr cs:[rh_ptr + 2], es
             ret
strat        endp
                                    ∙
                                    ∙
                                    ∙
;;                  INTERRUPT ROUTINE

ints         proc   far
             cli
             mov    cs:[old_stack], sp
             mov    cs:[old_stack + 2], ss
             mov    sp, cs
             mov    ss, sp
             mov    sp, offset cs:new_stack_end
             sti
             PUSHALL
             push   cs
             pop    ds
             les    di, cs:[rh_ptr]
             mov    bl, es:[di.command]
             xor    bh, bh
             cmp    bx, MAX_CMD
             jle    ints1
             mov    ax, ERROR + UNK_COMMAND
             jmp    ints2
ints1:
             shl    bx, 1
             call   word ptr dispatch[bx]
             les    di, cs:[rh_ptr]          ; reset just in case
ints2:
             or     ax, DONE                 ; Set the done flag
             mov    es:[di.status], ax       ; status in structure
             POPALL
             cli
             mov    ss, cs:[old_stack + 2]
             mov    sp, cs:[old_stack]
             sti
             ret
ints         endp

;;                  READ ROUTINE

read         proc   near
             DO_PRINT                      msg_read
             mov    ax, es:[di.count]
             mov    cs:[inrequest], ax       ; save request cnt
             xor    ax, ax                   ; 0 request hdr cnt
             mov    es:[di.count], ax        ; and zero rh count
             lds    bx, es:[di.address]      ; ds:bx pnts to data
top_read:
             cmp    cs:[in_block], TRUE      ; are we in a block?
             jnz    lp_it
             jmp    read_loop                ; yes
lp_it:
             mov    cs:[_incnt], FALSE
             mov    si, offset cs:[_soh]     ; pointer block begin
             mov    cs:[timer], TEN_SECONDS  ; set up timer
             mov    cs:[nak_cnt], FALSE      ; set to no NAKS
             cmp    cs:[block_num], 1        ; first time?
             jnz    rd_blk                   ; no
             call   send_nak                 ; send the first NAK
             STAT   LEFT_BRACKET             ; show a '['
rd_blk
             call   get_char                 ; any characters?
             jc     rd_blk2                  ; no
             cmp    cs:[_incnt], FALSE       ; first character?
             jnz    nxt_char                 ; no
             cmp    al, SOH                  ; first char an SOH?
             jz     nxt_char                 ; yes. store it
             cmp    al, ABORT                ; Abort xfer?
             jz     abort_it                 ; yep. Abort it!
             cmp    al, EOT                  ; are we done?
             jnz    rd_blk                   ; no, throw char away
             call   send_ack                 ; process EOF
             STAT   RIGHT_BRACKET            ; show a ']'
             STAT   CR                       ; and a clean line
             STAT   LF
             xor    ax, ax
             ret

nxt_char:
             mov    cs:[si], al              ; store it
             inc    cs:[_incnt]              ; bump the char count
             inc    si                       ; and the pointer
rd_blk2:
             cmp    cs:[_incnt], FULLBLKSIZE ; enough bytes
             jge    chkblk                   ; to check?
             call   eat_char                 ; for control-C
             cmp    cs:[timer], FALSE        ; out of time?
             jnz    rd_blk                   ; nope. try again
             STAT   DOT                      ; show a dot
             jmp    bad_blk2
bad_blk:
             STAT   QUESTION                 ; show question mark
bad_blk2:
             call   send_nak                 ; yep. send a NAK
             cmp    word ptr cs:[abort],TRUE ; abort?
             jnz    rd_blk3                  ; no, so reset timer,
                                             ; try again
abort_it:
             call   send_abort               ; send an abort
             call   send_abort               ; twice
             STAT   EXCLAIM                  ; finish with a bang!
             mov    ax, 800ch                ; mark an error
             ret
rd_blk3:
             mov    cs:[timer], TEN_SECONDS  ; reset the timer
             jmp    rd_blk                   ; and try again
             ret
chkblk:
             mov    ax, cs:[block_num]
             push   ax
             dec    al                       ; temporary
             cmp    al, cs:[_blk1]           ; duplicate block?
             jnz    real_blk                 ; no
             STAT   DUP_BLK                  ; yes
             xor    ax,ax
             mov    cs:[_incnt], ax
             jmp    ack_blk
real_blk:
             pop    ax                       ; back up to correct block
             cmp    cs:[_blk1], al           ; is block count ok?
             jnz    bad_blk                  ; no
             not    al
             cmp    cs:[_blk2], al           ; block complement?
             jnz    bad_blk                  ; no
             mov    cx, BLKSIZE
             mov    si, offset _buf
             call   do_chksum
             cmp    cs:[_chksum], al
             jnz    bad_blk                  ; checksum bad
             mov    cs:[in_block], TRUE      ; looks good!
             mov    cs:[_inptr], offset cs:_buf   ; set up pointer
             sub    cs:[_incnt], FULLBLKSIZE - BLKSIZE
read_loop:
             mov    si, cs:[_inptr]          ; reset the pointer
rd_loop:
             mov    al, cs:[si]              ; get the character
             mov    ds:[bx], al              ; and stuff it
             inc    bx                       ; up each pointer
             inc    si
             inc    word ptr es:[di.count]   ; inc the counter
             dec    cs:[_incnt]              ; drop the chars left
             jnz    stf_nxt_char             ; still more!
             inc    cs:[block_num]
             STAT   STAR                     ; send a '*'
ack_blk:
             call   send_ack                 ; send the ack
             mov    cs:[in_block], FALSE     ; out of characters
             jmp    top_read
stf_nxt_char:
             dec    cs:[inrequest]           ; anymore?
             jnz    rd_loop
             xor    ax,ax                    ; no more request,
             ret                             ; no errors
read         endp


;;                  WRITE ROUTINE

write        proc   near
             PUSHALL_AX
             DO_PRINT                      msg_write
             mov    cx, es:[di.count]        ; how many bytes?
             xor    dx, dx                   ; zero our char cnt
             lds    bx, es:[di.address]      ; ds:bx pnts to data
             mov    si, cs:[_outptr]         ; where to stick
                                             ; each char
wr_lp:
             mov    al, ds:[bx]              ; get the character
             mov    cs:[si], al              ; and save in buffer
             inc    bx                       ; move pointers
             inc    cs:[_outcnt]             ; total char count
             inc    dx                       ; current char cnt
             cmp    cs:[_outcnt], BLKSIZE    ; send a block?
             jnz    wr_ok                    ; not yet
             call   send_block               ; send the block
             cmp    cs:[abort_xfer], FALSE   ; should we abort?
             jz     blk_ok                   ; no, block sent okay
             call   send_abort               ; yes,
             call   send_abort               ; send it twice!
             mov    es:[di.count], FALSE     ; mark no chars sent
             mov    ax, ERROR + WRITE_ERROR  ; mark as write error
             ret
blk_ok:
             mov    cs:[_outptr], offset _buf; reset the pointer,
             mov    si, offset _buf          ; the register,
             mov    cs:[_outcnt], FALSE      ; and number of chars
wr_ok:
             inc    si                       ; pnt next char pos
             loop   wr_lp
             mov    es:[di.count], dx        ; how many went okay
             mov    cs:[_outptr], si         ; now save it
             xor    ax, ax                   ; no errors
             POPALL_AX
             ret
write        endp

;;                  DEVICE OPEN ROUTINE

dev_open     proc   near
             PUSHALL_AX
             DO_PRINT                      msg_dev_open
             mov    cs:[cancel_flag], FALSE
             mov    cs:[_inptr], offset cs:_soh
             mov    cs:[_incnt], FALSE
             mov    cs:[_outptr], offset cs:_buf
             mov    cs:[_outcnt], FALSE
             mov    cs:[block_num], 1
             call   set_timer
             mov    dx, cs:[com_port]        ; init the comm port
             mov    al, cs:[init_data]
             mov    ah, 0
             int    14h
eat_loop:
             mov    dx, ds:[com_port]
             mov    ah, 3                    ; get status
             int    14h
             test   ah, 1                    ; any data ready?
             jz     continue                 ; no
             mov    ah, 2
             int    14h                      ; eat the character and
             jmp    eat_loop                 ; try again
continue:
             test   al, 080h                 ; DCD on?
             jz     off_line
             jmp    on_line                  ; yes
off_line:
             mov    dx, offset cs:offline    ; output the message
             mov    ah, 9
             int    21h
             mov    si, offset cs:numbuf
             mov    cx, 19                   ; maximum length

             call   get_num
             cmp    cs:[kb_len], 0           ; only a return?
             jnz    dial_it
             mov    dx, offset cs:await
             mov    ah, 9
             int    21h
             mov    si, offset cs:answerstring
             call   out_string
             mov    cs:[timer], FOREVER
             jmp    offline_lp               ; now wait for carrier
dial_it:
             mov    dx, offset cs:dialing
             mov    ah, 9
             int    21h
             mov    si, offset cs:dialstring
             call   out_string
             mov    si, offset cs:numbuf
             call   out_string
             mov    si, offset cs:return
             call   out_string
             mov    cs:[timer], ONE_MINUTE
             mov    cs:[was_dialed], TRUE    ; mark as a dialed call
                                             ; for ATH later
offline_lp:
             mov    ah, 3                    ; are we on-line?
             mov    dx, cs:[com_port]
             int    14h
             test   al, 080h                 ; DCD on?
             jnz    made_con                 ; yes
             call   eat_char                 ; for control-C
             cmp    cs:[timer], 0            ; out of time?
             jnz    offline_lp               ; nope
abort_call:
             mov    cs:[was_dialed], FALSE
             mov    dx, offset cs:no_con
             mov    ah, 9
             int    21h
             xor    ax,ax
             mov    es:[di.count], ax        ; set count to zero
             mov    ax, ERROR + GEN_FAILURE  ; mark as an error
             POPALL_AX
             ret
made_con:
             mov    dx, offset cs:con
             mov    ah, 9
             int    21h
term_em_lp:
             call   get_char
             jc     get_term_char
             cmp    al, ESCAPE
             jz     term_exit                ; escape. Get out!
             mov    ah, 02h
             mov    dl, al
             int    21h
get_term_char:
             mov    ah, 0bh
             int    21h                      ; any characters?
             or     al, al
             jz     term_em_lp               ; nope
             mov    ah, 1                    ; yep. get the character
             int    21h
             mov    ah, 1
             mov    dx, ds:[com_port]
             int    14h
             cmp    al, ESCAPE
             jz     term_exit                ; escape. Get out!
             mov    ah, 02h
             mov    dl, al
             int    21h
             jmp    get_term_char
term_exit:
             mov    dx, offset cs:xfer       ; status message
             mov    ah, 9
             int    21h

on_line:
             xor    ax, ax
             POPALL_AX
             ret
dev_open     endp

;;                  DEVICE CLOSE ROUTINE

dev_close    proc   near
             DO_PRINT         msg_dev_close
             cmp    cs:[cancel_flag], TRUE   ; were we canceled?
             jnz    no_cancel                ; nope
             mov    ah, 1                    ; send the ABORT
             mov    dx, cs:[com_port]
             mov    al, ABORT
             int    14h
             jmp    no_send                  ; and exit normally
no_cancel:
             cmp    cs:[_outcnt], FALSE      ; any characters left?
             jz     no_send                  ; no
             call   send_block               ; yes. send remainder
             STAT   RIGHT_BRACKET
             STAT   CR
             STAT   LF
             mov    cs:[_outptr], offset _buf; reset the pointer,
             mov    si, offset _buf          ; the register,
             mov    cs:[_outcnt], FALSE      ; and number of chars
             inc    cs:[block_num]           ; just in case....
no_send:
             cmp    cs:[block_num], 1        ; minim 1 block sent?
             jz     no_blk_sent              ; no
             mov    ah, 1                    ; send the EOT
             mov    dx, cs:[com_port]
             mov    al, EOT
             int    14h
no_blk_sent:
             cmp    cs:[was_dialed], TRUE    ; were we the dialer?
             jnz    no_hang
             mov    cs:[timer], ONE_SECOND   ; one sec of silence
sil_lp1:
             call   eat_char                 ; for control-C
             cmp    cs:[timer], FALSE        ; expired?
             jnz    sil_lp1
             mov    si, offset cs:pluses     ; get the modems att
             call   out_string
             mov    cs:[timer], ONE_SECOND   ; one sec of silence
sil_lp2:
             call   eat_char                 ; for control-C
             cmp    cs:[timer], FALSE        ; expired?
             jnz    sil_lp2
             mov    si, offset cs:hangup     ; now hangup
             call   out_string
no_hang:
             mov    cs:[was_dialed], FALSE
             mov    cs:[cancel_flag], FALSE
             call   reset_timer
             ret
dev_close    endp

;;      NON-DESTRUCTIVE READ ROUTINE ◄──┐
;;      INPUT STATUS ROUTINE            │
;;      FLUSH INPUT BUFFER ROUTINE      │   These routines are
;;      OUTPUT STATUS ROUTINE           ├─► located here in the
;;      FLUSH OUTPUT BUFFERS ROUTINE    │   full program listing
;;      READ I/O CONTROL ROUTINE        │
;;      Write I/O CONTROL ROUTINE       │
;;      SEND_BLOCK ROUTINE           ◄──┘
                                    ∙
                                    ∙
                                    ∙
;;           INITIALIZE DRIVER ROUTINE

init         proc   near
             DO_PRINT             msg_init
             mov    ah, 030h                 ; get the DOS version
             int    21h
             cmp    ah, 3                    ; version 3.x?
             jge    okay_dos                 ; yes. At least.
             mov    dx, offset cs:wrong_dos  ; output the message
             mov    ah, 9
             int    21h
endless_loop:
             cli
             jmp    endless_loop             ; very ugly,
                                             ; but effective!
okay_dos:
             mov    dx, offset cs:greetings  ; output the message
             mov    ah, 9
             int    21h
             push   ds
             mov    ds, es:[di.count + 2]    ; get the segment of
                                             ; CONFIG.SYS line
             mov    si, es:[di.count]        ; get the offset of
                                             ; CONFIG.SYS line
             call   output_chars
             pop    ds
             mov    dx, offset cs:end_greetings   ; output the message
             mov    ah, 9
             int    21h
             mov    word ptr es:[di.address], offset init
             mov    word ptr es:[di.address + 2], cs
             xor    ax,ax
             ret
init         endp

wrong_dos    db '??Must run this driver under DOS 3.0 or higher',
                    db CR, LF, ' System Halted! $'
greetings    db CR, LF, LF, 'MDM_DRV being installed...', CR, LF
                    db 'CONFIG.SYS Line is: $'
end_greetings       db CR, LF, LF, LF, '$'

output_chars proc   near
output_loop:
             mov    dl, ds:[si]              ; get the character
             cmp    dl, LF                   ; is it NULL?
             jnz    outit                    ; no
             ret
outit:
             mov    ah, 2                    ; output character
             int    21h
             inc    si
             jmp    output_loop
output_chars endp

driver       endp
code         ends
             end

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

Microsoft C Optimizing Compiler 5.0 Offers Improved Speed and Code Size

Augie Hansen☼

Microsoft(R) Corporation has released the latest version of its professional
C compiler, Microsoft C Optimizing Compiler Version 5.0. The new compiler
produces highly optimized object code that runs about 25-30 percent faster
than code generated by the Version 4.0 C compiler. Improved development
tools, an enhanced version of the CodeView(R) debugger, expanded
documentation, new libraries of routines for BIOS/DOS access, and a full
set of graphics routines all support the new version. The professional
development environment provided by Microsoft C 5.0 is bolstered by the
addition of the highly interactive, integrated development environment
offered by QuickC(TM), which is being packaged with the optimizing compiler.


Optimizing C

A compiler is a type of code translator, specifically a program, or a set
of programs, designed to translate instructions from a readable and
writeable form (source code) into one that can be understood and executed
by a computer (object code). A compiler converts high-level instructions
into machine code once, preserving the results in a program file that the
operating system can load and run without any additional translation.

Assemblers and interpreters are also code translators. An assembler takes
low-level mnemonic statements (like MOV AX,0) and converts them to their
equivalent machine instructions, which are expressed as sequences of
binary numbers, 0s and 1s. The programmer therefore avoids having to
program in 0s and 1s, but the programmer is still "programming the bare
metal" when writing assembler code.

An interpreter, like a compiler, accepts high-level instructions as input,
but instead of performing one translation  to executable machine
instructions, it reads the input and translates it each time a program is
run. Interpreters provide an interactive development environment that
assists the programmer, but at a cost of slower execution speed of the
program.

Microsoft C Optimizing Compiler Version 5.0 combines the QuickC compiler
(see "Programming in C the Fast and Easy Way with Microsoft QuickC,")
with a multipass optimizing C compiler to serve the needs of a wide range
of programmers. QuickC, a fast, in-memory compiler offers an interpreter
like development environment featuring ease of use and instant feedback.
QuickC sacrifices execution speed of the object code to obtain a fast 7000-
lines-per-minute compilation speed on a typical 6-MHz 80286-class machine.
The compiler will take the source developed in the QuickC environment, or
with a favorable text editor, and will optimize it for either speed or code
size.


Optimizing for Code Size and Execution Speed

A translator is free to convert source code into object code in any way it
chooses, but the quality of the resulting code varies considerably from
one translation to another. A simple translator should produce code that
runs and generates the correct results for all inputs, but the executable
code may not be the smallest or the fastest that can be written.

One measure of the quality of code produced by a compiler is its execution
speed; another measure is the size of the object code. Usually a tradeoff
must be made between speed and size to arrive at the best code for a given
operating condition; rarely does code satisy speed and size requirements
simultaneously.

Optimization is the process of improving code over that which a simple
translation produces. Optimizing compilers attempt to produce the best
possible object code resulting in the fastest running or smallest object.
The Microsoft C compiler employs state-of-the-art techniques for
generating code as well as advanced optimization methods to produce object
code that rivals that produced by hand assembly.

One added benefit of Microsoft C is that the XENIX(R) C compiler and the
equivalent MS-DOS version are essentially the same product. This
facilitates the development of programs that are portable from MS-DOS to
XENIX and vice versa. Also, the XENIX CMERGE compiler offers a full cross-
development environment  in which programs targeted for MS-DOS can be
developed and managed under XENIX. The XENIX environment is much better
suited to large development projects because it provides a well-
coordinated set of software engineering tools, multiuser access, and
multitasking features that are not available under MS-DOS.


Standards

The proposed ANSI C standard is still being reviewed and commented on, so
no C compiler vendor can claim full ANSI compatibility. Microsoft is
participating in the standardization effort, and its compilers follow the
proposed standard as closely as possible. In the absence of an official
standard, Microsoft C provides the next best thing──full compatibility with
the UNIX System V C compiler, which, along with its standard libraries, is
the current de facto standard of the industry. The C Programming Language,
Brian Kernighan and Dennis Ritchie (Prentice Hall, 1978), served as the de
facto standard but was out of date shortly after it was printed because
features such as the void function return type (typeless), enum (the
enumeration data type), and structure assignment were soon added to the
language.


Memory Models

The standard memory models for Intel segmented-memory processors are
supported: small, compact, medium, and large. The default memory model is
small. The difference between the models is the maximum allocation of
memory for code and data that each permits (see Figure 1). The maximum
size of any data item under any of these memory models is 64Kb. The
compiler also provides a huge memory model, which is identical to the
large memory model except that data items can exceed 64Kb.

Mixed-model programming permits code and data items to be referenced as
near or far items. The keywords near and far can be applied to variables
and functions according to how addressing is done. A near address is a 16-
bit offset with a default segment. A far address uses both segment and
offset values, 16 bits each, to address memory items. Items addressed by
near pointers are assumed to lie completely within a single 64Kb segment,
so pointer arithmetic is done only on the 16-bit offset component of an
address.

To permit direct access to large data items, such as a large array, the
huge keyword specifies full 32-bit addressing. Individual array elements
are still limited to 64Kb, and while they cannot span segment boundaries,
the array can be larger than 64Kb. Also, casts to long are required on
huge pointer differences and sizeof values. The huge keyword applies only
to data; it does not apply to code items.


Floating-point Support

Microsoft C performs floating-point arithmetic in several ways. If a
numeric data processor (NDP, commonly called a coprocessor) is present,
in-line code, which avoids unnecessary function-call overhead, is
generated for calculations to obtain the greatest possible execution
speed.

In the absence of an NDP, run-time modules produced by Microsoft C
automatically use software emulation routines for math calculations.
Routines that calculate to the same 80-bit accuracy as the NDP are used by
default. Microsoft binary math routines that calculate with 64-bit
accuracy can be selected if reduced accuracy can be reasonably traded for
faster program execution.


Interlanguage Calling

The C language uses a different convention for passing parameters than
such programming languages as FORTRAN and Pascal. FORTRAN and Pascal push
parameters onto the stack in left-to-right order, which is the order in
which they appear in procedure and function declarations. The C language
convention is just the reverse──the leftmost parameter of a function
parameter list is the last one pushed onto the stack.

The different convention results from a C language provision for passing a
variable number of parameters to a function. Since the first parameter is
always the last one pushed, its offset from the start of the stack frame
is at a known address. The first parameter, or a subsequent one, can
contain information about how many other parameters were passed. This is
how printf-family functions are implemented.

The C language convention (the default) requires that the calling function
contain the instructions that tell how to restore the stack to the
conditions that applied before the call. The clean-up code is added after
each and every call to the function. The alternative FORTRAN and Pascal
convention is to place the stack clean-up code in the called procedure or
function. Thus the clean-up code appears only once for each procedure
regardless of how many times the procedure is called.

The net effect is that the FORTRAN and Pascal calling conventions produce
slightly smaller and faster executable code at the expense of some
programming flexibility. In small programs, the difference is negligible,
but as programs grow in size and complexity, the advantages of the fixed-
parameter convention become more apparent.

Microsoft C offers the fortran, pascal, and cdecl extended keywords to
tell the compiler which calling convention to use on a function-by-
function basis in programs that involve interlanguage calling. Also, a
compiler command-line switch, /Gc, can be used to compile an entire module
by using the alternative calling convention.


Installation

An automated procedure greatly simplifies compiler installation. The SETUP
program takes a set of parameters that specify the memory-model support
desired, optional names of directories for commands, headers, and
libraries, and an optional C 4.0 compatibility flag.

Use the SETUP program to ensure that all necessary files are placed in the
correct subdirectories. A frequent problem associated with the
installation of earlier versions of the Microsoft C compiler is a failure
on the part of the programmer to create a SYS directory under include and
to copy in the system header files, such as stat.h. The SETUP program also
has the option of creating a single combined library for each memory model
specified, which results in fewer disk accesses and, therefore, faster
linking times.


Standard C Library

The standard library contains a comprehensive set of system calls and
standard subroutines. Taken together, the roughly 250 standard routines
provide the essential time, input/output, string-handling, memory-
management, process-control, and math operations needed to construct a
wide range of programs.

The system calls for MS-DOS to follow the UNIX/XENIX pattern for buffered
and unbuffered access to the file system and files. Owing to differences
in the operating systems, however, some of the functions behave
differently or require different parameters or numbers of parameters. A
few UNIX/XENIX functions, such as fork, getlogin, and sleep, have no MS-
DOS equivalents. Still, the standard subroutines are, for the most part,
equivalent to their UNIX/XENIX alter egos.


Graphics Library

In addition to standard UNIX System V──compatible run-time libraries,
Microsoft C includes about 50 new graphics and DOS/BIOS functions, which
are contained in separate libraries, to help programmers with tasks that
have previously required custom programming. The GRAPHICS.LIB library file
contains graphics functions that take advantage of highly compliant CGA
and EGA display hardware. Support for selected VGA modes is also included.
Figure 2 is a summary of the graphics routines.

The graphics routines differentiate between visual and active screen
pages, memory regions that hold images that are being displayed (visual
page) and written to (active page). If the pages (memory areas) are
identical, then anything written is seen as it is being drawn. To create
animation effects or "instant" screen updates, use separate active and
visual pages. Write to the active page while the visual page is being
viewed, and then swap the pages.

All graphics writing is relative to a logical coordinate system, which is,
in turn, expressed relative to the screen's physical coordinate system.
The origin of the physical coordinate system is (0, 0) at the upper-left
corner of the screen, while the origin of the logical coordinate system
can be set to any valid physical coordinate. The number of pixels in each
screen dimension can be obtained from the configuration data.

The x-axis increases its values as it moves to the right, but the y-axis
is inverted, with values increasing as it progresses downward. You can
write macros to change signs in order to provide axis translation if it is
needed in an application.

Text is managed on a grid of rows and columns with row 0 and column 0 at
the upper-left corner of the screen. Text can be output in either text or
graphics modes. Each text item is simply a string, so any numbers must be
converted to their string representations before being written.

All images written by functions in the graphics library, whether graphics
or text, are clipped to specified window boundaries (clipping regions).
The graphics library completely controls display attributes, line styles,
and area-fill operations.

The built-in graphics support provided by Microsoft C considerably eases
the burden of graphics programming by encapsulating the most commonly used
graphics operations into a convenient, memory model-independent library of
routines. The routines relieve the programmer of a tremendous amount of
detailed programming work in preparing and controlling graphics images on
the screen.

A sampling of the compiler's graphics functions is used in GDEMO to show
how easy it is to use the functions, and how capable and flexible the
interface is. Although GDEMO is a very simple graphics program, it
demonstrates how programs set a graphics mode, either upon entry or during
normal operation, and then restore the user's original video mode upon
exit.

Examining the GDEMO.C source file reveals that a call to _setvideomode is
all that is necessary to switch to a specific video mode. Modes are
represented by symbolic names defined in the GRAPH.H  header file. In this
program, you set the medium-resolution 16-color graphics mode
(_MRES16COLOR), which requires an enhanced graphics adapter (EGA) and a
suitable monitor. Before the program terminates, restore the video mode
that was in effect before the program started running by sending a call
to _setvideomode with the mode parameter set to _DEFAULTMODE.

The demonstration program is purposely unfettered by error-checking code
to make the graphics calls visible, but the return value of the function
call should be checked before graphics operations are performed to be sure
that the mode-setting operation is successful. A return value of 0 flags
an error, which is usually caused by a program trying to set an
unsupported video mode. A check such as

  if (_setvideomode (_MRES16COLOR) > 0)
 {
 fprintf (stderr, "This program requires an EGA\n") ;
 exit (1) ;
 }

will do the job.

An alternative design approach is to attempt a switch to one of the CGA
modes if the EGA test fails. The program can run, albeit with fewer
colors, on a CGA-compatible video system.

After setting the video mode, it is necessary to gather video
configuration information to discover the screen limits in pixels and
other important information about the video system. The information is
saved in a structured variable that is defined by the struct videoconfig
template in the GRAPH.H header file. In GDEMO, the screen dimensions are
used to obtain the value of the screen center and make that coordinate the
logical origin by the call to _setlogorg.

Drawing lines and rectangles is as easy as writing calls to _rectangle,
_moveto, and _lineto. All points are expressed relative to the previously
defined logical origin. The graphics functions handle all calculations
needed to place points at the correct physical screen locations.

Plotting arbitrary points requires only slightly more work. The _setpixel
function plots points and sets the pixel at the specified logical
coordinate to the current color. The opposite operation, retrieving the
value of a specified pixel, is done by _getpixel.

As noted earlier, the sense of the y-axis needs to be inverted if curves
are to be drawn correctly from the user's point of view. The hardware
notion of screen coordinates is based on the fact that the physical origin
(0, 0) is at the upper-left corner of the screen. The PLOT macro was
written to simplify the plotting of points:

  #define PLOT(x_, y_)
 _setpixel ((x_), -y))

This macro inverts the value of the y parameter and calls _setpixel to
display the point.

In graphics modes, text is actually drawn by routines that plot a pattern
of points. The graphics-mode text origin is at the upper-left corner of
the screen and text-writing functions use the familiar text-mode
conventions of screen rows and columns. Placing text into an image
requires a call to _settextposition to move to the desired row and column,
followed by a call to _outtext with a pointer to a NUL-terminated string
as the sole parameter.

A number of other functions are provided in the graphics library to
control access to video screen pages; set colors, control cursor
visibility, set clipping regions and viewports, and manage coordinate
systems. This highly functional graphics interface is an important
addition to the Microsoft C compiler package.


Library Functions

Another new set of functions in the Microsoft C compiler package provide
operating system and PC hardware interfaces. Figure 3 is a summary of new
MS-DOS and BIOS functions that greatly simplify programming in an MS-DOS
environment. Note that these functions are not portable to XENIX or other
environments; they are intended solely to give full access to the MS-DOS
environment.

The BIOS interface functions offer easy access to basic input/output
interrupts. Besides housekeeping  and  configuration information (system
clock, equipment list, and memory size), the BIOS library functions
include peripheral-access functions for disks, printers, and serial ports.

The MS-DOS interface functions access the operating system through system
calls. Several of the functions in the set were available in earlier
versions of the C compiler: bdos, int86, and its variants. The new MS-DOS
functions provide C language functions that make calls on the Interrupt
21H "umbrella" interrupt. Previously, it was necessary to write the
functions in terms of int86 calls. Now each of the major MS-DOS services
can simply be called by name, such as _dos_getdate to obtain the current
system date.


Heap Debugging

Dynamic storage problems can be the bane of any programmer's existence.
The heap is the unallocated area of memory in which dynamic simple
variables and data structures are allocated, moved, and freed during the
running of a program. It is often difficult to find errors in heap
management, so three new functions have been added to the C library to
assist programmers in testing and debugging dynamic memory.

The _heapchk function runs consistency checks, _heapset fills the heap
with known values, and _heapwalk "walks" through the heap gathering
information about each entry. When called in small data models (small and
medium), the functions map to near-heap versions (_nheapchk, _nheapset,
and _nheapwalk). In large data models (compact and large), they map to
far-heap versions (_fheapchk, _fheapset, _fheapwalk).


Controlling Compilations

Pragmas are implementation-defined preprocessor directives that are
ignored by systems that don't recognize them. A pragma directive, added
to C by the proposed ANSI standard, is introduced by the #pragma token.
Microsoft C uses pragmas to control compilation.

A loop optimization pragma allows local control over loop optimizations.
Local control overrides any global optimization controls established on
the compiler command line. For example, #pragma loop_opt(on) enables loop
optimizations, and #pragma loop_opt(off) disables loop optimizations.
Stack-checking can be turned on and off in a similar fashion by using the
check_stack pragma. Other pragmas offer control of intrinsics, structure
alignment and packing, and segment allocations.


Run-time Library Source

The source code for the entire run-time library is now available as a
separate product. Having the library source code gives programmers a base
for customization to meet special requirements. The source also has many
examples of tightly written C and assembler code to help beginning and
expert programmers.


Enhanced CodeView

CodeView is a full-screen symbolic source-level debugger. Multiple windows
can display source and object code simultaneously, set and observe
watchpoints, set breakpoints, watch CPU register and flag values, and
trace the stack. CodeView provides complete execution control over the
program being debugged, including single-stepping, screen-swapping to
alternately view code and resulting screen displays, and many other
features (see "A Guide to Debugging with CodeView," MSJ, Vol. 2 No. 1).
Since its introduction, the debugger has been improved and enhanced with
new features.

Expanded memory support gives CodeView access to large amounts of memory,
thus very large applications, including those that use overlays. A new
mixed-language debugging capability allows programmers to debug programs
with modules written in Microsoft C, FORTRAN, Macro assembler, Pascal, and
BASIC. The QuickC integrated environment includes a subset of CodeView
debugging features.


Using MAKE

Simple programs, particularly those that consist of only a single source
file, are easily translated into executable programs. You simply use the
compiler driver program, CL.EXE, which takes a source filename and
possibly some optional parameters. It controls the entire preprocessing,
compiling, and linking phases of the translation. CL calls the individual
compiler passes, C1, C2, and C3, to compile the source statements and then
it calls the LINK program to combine the object file with the standard C
library routines.

Programs that involve more than one source file or a mixture of assembler
and C sources and those that require linking with libraries other than the
default LIBC.LIB (for the selected memory model) require a greater project
management effort that is best controlled by the MAKE program
builder/maintainer utility. Figure 4 depicts one method of creating a two-
module graphics demonstration program.

The primary purpose of MAKE is to automate program building and
maintenance tasks. You build a description of how the various elements of
the program are created and put together to form the final product. MAKE
interprets the instructions in the description file and does the least
amount of work possible to keep the product current with its component
parts. MAKE thus speeds up the development by reducing the amount of
typing you have to do and eliminating unnecessary compilation steps.

A makefile is a user-supplied set of instructions that specifies the names
of targets (items to be created) and the sources from which the targets
are to be built; MAKE interprets the relationships among the sources and
targets. The makefile for the graphics demonstration example program, for
instance, is contained in the file GDEMO.MAK. In order to create
GDEMO.EXE for the first time or to update it after changes have been made
to any of the source files or libraries, you simply type MAKE GDEMO.MAK
and MAKE will compile any sources that are newer than their respective
object files.

The temporal relationships among files are the basis of MAKE's operation.
MAKE uses the MS-DOS directory date and time stamps to determine whether
to remake a target file. A given target can have one or more explicit
dependencies expressed in the makefile. If any one or more of the files
upon which a target depends is newer than the target, the target is then
remade.

An implicit dependency, such as that which exists between any object file
(.OBJ) and its source file (.C, for example) can be given in rules
specified in a TOOLS.INI file, which MAKE can find either in the current
directory or in one of the directories specified by the PATH environment
variable. Explicit rules can be placed in the makefile to override
implicit rules.


Sample Project

To illustrate the building and maintenance of a program, let's construct a
graphics demonstration program. GDEMO switches to medium-resolution
graphics mode and draws an x-axis, a y-axis, plots two equations, and
labels them. The program uses a macro to translate plotted y values of the
equations. It then prompts the user and waits for a keypress before
restoring the original video mode and returning to DOS. The program shows
how text and graphics can be easily combined in a screen image.

The C source is split into two files. GDEMO.C (see Figure 5) contains the
main program source, which illustrates the use of several new graphics
library routines. MESSAGE.C (see Figure 6) contains the source for a
function that writes a text string at a specified location and waits for a
continuation signal, which is the user pressing a key on the keyboard.

In creating the program, I used QuickC to develop and test the code.
QuickC automatically created the file GDEMO.MAK (see Figure 7), which
controls the compiling and linking within the QuickC integrated environment.
To use the in-memory MAKE utility, all you have to do is identify the source
files from which the program is built, and QuickC does the rest.

In switching to the full Microsoft C 5.0 environment to get the benefit of
greater optimization for program speed, I edited the makefile to use the
CL compiler driver program instead of QCL and to build in the link-control
statements rather than have them placed in a separate linker-response
file. The revised makefile is the GDEMO.MAK shown in Figure 8.

The graphics screen display produced by GDEMO appears on the first page of
this article. Each of the labels is positioned and written by the Message
function. For the equation labels, the wait parameter to the Message
function call is M_NOWAIT, so the function just draws the text and moves
on. The prompt message at the bottom of the screen is drawn by a call to
Message with wait set to M_WAIT, which tells the function to wait for the
user to press a key.


The Best of Both Worlds

Before Microsoft introduced its C compiler several years ago, I used a
number of other C compilers in a quest to find the best one for MS-DOS
programming tasks. Since the introduction of Microsoft C Version 3.0, many
other C programming products have been introduced into a market that is
hungry for programming products. The fact that so many vendors have
survived in an intensely competitive environment is a testimonial to both
the enormous size of the market and its explosive growth.

My recent book about C programming, Proficient C (Microsoft Press, 1987),
is devoted to programming in an MS-DOS environment and is based on the
Microsoft C Version 4.0 compiler. Microsoft C compilers are noted for the
fast-running code they produce, as well as for their full UNIX-compatible
library support. Significant portions of the book deal with low-level
access to MS-DOS and PC system services, and in spite of the need for
speed and tight coupling to the hardware and operating system, it was not
necessary to write even a single line in assembler.

Having used the new Microsoft C Optimizing Compiler Version 5.0 for many
months during its development and testing phases, I am impressed by its
overall quality, speed, and breadth of coverage. I consider it to be the C
programming tool of choice. Proficient C contains more than 6500 lines of
C source code. After recompiling the entire source using the Version 5.0
compiler, I observed an object module space saving of about 15 percent and
an increase in program execution speed averaging better than 25 percent
compared to the Version 4.0 compiler.

Microsoft C Optimizing Compiler Version 5.0 is a very sophisticated
compiler system. By combining QuickC, a speedy in-memory development
environment, with a new optimizing C compiler, Microsoft has developed a
compiler package that serves the needs of beginner and expert alike. Speed
and code size improvements, great new libraries, and an enhanced CodeView
all combine to produce the best C programming environment available today.


Figure 1:  Available Memory Models

          Maximum     Maximum
Model     Data Size   Code Size

small     64Kb        64Kb
compact   64Kb        available
medium    available   64Kb
large     available   available


Figure 2:  Graphics Library Routines

     Configure

     _displaycursor     Cursor ON/OFF upon graphics exit
    _getvideoconfig     Get current graphics environment
     _setactivepage     Set memory area for writing images
      _setvideomode     Set screen display mode
      _setvisualpag     Set memory area for displaying images

     Set Coordinates

       _getlogcoord     Convert physical to logical coordinates
      _getphyscoord     Convert logical to physical coordinates
        _setcliprgn     Set clipping region
         _setlogorg     Position the logical origin
       _setviewport     Limit output region; set origin

     Set Palette

   _remapallpalette     Assign colors to all pixel values
      _remappalette     Assign colors to selected pixel values
     _selectpalette     Select a predefined palette

     Set Attributes

        _getbkcolor     Obtain current background color
          _getcolor     Obtain current color
       _getfillmask     Obtain current fill mask
      _getlinestyle     Obtain current line style
        _setbkcolor     Set background color
          _setcolor     Set color
       _setfillmask     Set fill mask
      _setlinestyle     Set line style

     Output Text

      _gettextcolor     Obtain current text color
   _gettextposition     Obtain current text output position
           _outtext     Display text at current position
   _settextposition     Set the text position
      _settextcolor     Set the text color
     _settextwindow     Establish a text window
            _wrapon     Enable/disable line wrap

     Output Images

               _arc     Draw an arc
       _clearscreen     Clear screen to background color
           _ellipse     Draw an ellipse
         _floodfill     Fill an area with the current color
_getcurrentposition     Obtain current graphic output position
          _getpixel     Obtain current pixel value
            _lineto     Draw a line
            _moveto     Move graphic output position
               _pie     Draw a pie-slice shape
         _rectangle     Draw a rectangle
          _setpixel     Set a pixel value

     Transfer Images

          _getimage     Store a screen image in memory
         _imagesize     Return image size (in bytes)
          _putimage     Retrieve an image and display it


Figure 3:  BIOS and MS-DOS Library Routines

     BIOS Interface

      _bios_disk    Issue disk requests
 _bios_equiplist    Perform equipment check
    _bios_keybrd    Access keyboard services
   _bios_memsize    Get available memory size
   _bios_printer    Access printer output services
 _bios_serialcom    Access serial comm services
 _bios_timeofday    Access system clock

     DOS Interface

            bdos    Invoke DOS system call
     _chain_intr    Chain interrupt handlers
        _disable    Disable interrupts
   _dos_allocmem    Allocate a block of memory
      _dos_close    Close a file
      _dos_creat    Create a file
   _dos_creatnew    Create a new file
  _dos_findfirst    Find the first occurrence of a file
   _dos_findnext    Find subsequent occurrences of a file
    _dos_freemem    Free a block of memory
    _dos_getdate    Get the system date
_dos_getdiskfree    Get disk drive information
   _dos_getdrive    Obtain ID of current disk drive
_dos_getfileattr    Get file or directory attributes
   _dos_getftime    Get date and time of last file write
    _dos_gettime    Get the system time
    _dos_getvect    Get value of an interrupt vector
       _dos_keep    Install as TSR program
       _dos_open    Open an existing file
       _dos_read    Read a file
   _dos_setblock    Change the size of an existing block
    _dos_setdate    Set the system date
   _dos_setdrive    Set the default disk drive
_dos_setfileattr    Set file or directory attributes
   _dos_setftime    Set file date and time
    _dos_settime    Set the system time
    _dos_setvect    Set an interrupt vector value
      _dos_write    Send output to a file
       dosexterr    Obtain register values
         _enable    Enable interrupts
          FP_OFF    Return offset portion of far pointer
          FP_SEG    Return segment portion of far pointer
        _harderr    Establish a hardware error handler
     _hardresume    Return to DOS after a hardware error
       _hardretn    Return to the application after a
                    hardware error
           int86    Invoke a DOS interrupt
          int86x    Invoke a DOS interrupt with segment
                    register values
          intdos    Invoke general DOS system call
         intdosx    Invoke general DOS system call with
                    segment register values
         segread    Returns values of segment registers


Figure 4:  Software Project Management with MAKE

┌─────────────┐ ┌─────────────┐
│  GDEMO.MAK  │►│             │
└─────────────┘ │  MAKE.EXE   ├──────────────────────┐
                │             │                      │
                └──────┬──────┘                      │
┌─────────────┐ ┌──────▼──────┐ ┌─────────────┐ ┌───▼──────┐
│   GDEMO.C   │►│             │►│  GDEMO.OBJ  │►│          │
└─────────────┘ │             │ └─────────────┘ │          │
                │             │                 │          │
                │   CL.EXE    │                 │          │ ┌─────────────┐
                │             │                 │          │►│  GDEMO.EXE  │
┌─────────────┐ │             │ ┌─────────────┐ │          │ └─────────────┘
│  MESSAGE.C  │►│             │►│ MESSAGE.OBJ │►│          │
└─────────────┘ └──────■──────┘ └─────────────┘ │ LINK.EXE │
                       ■        ┌─────────────┐ │          │
                ┌──────■──────┐ │GRAPHICS.LIB │►│          │ ┌─────────────┐
                │             │ └─────────────┘ │          │►│  GDEMO.MAP  │
  GDEMO         │   C1.EXE    │                 │          │ └─────────────┘
  Graphics      │   C2.EXE    │                 │          │
  Demo          │   C3.EXE    │ ┌─────────────┐ │          │
  Program       │             │ │  ?LIBC.LIB  │►│          │
                └─────────────┘ └────────────┘ └──────────┘
                          The ? is replaced by S,C,M, or L to match
                          the selected memory model.


Figure 5:  GDEMO.C

/********************************************************************
* NAME: gdemo
*
* DESCRIPTION: This is a simple graphics demonstration program. Its
*  purpose it to show how to select a graphics mode, do some simple
*  graphics work, and then return to DOS after restoring the user's
*  previous video environment.
********************************************************************/

#include <graph.h>
#include <math.h>

#define RHEIGHT         160
#define RWIDTH          300
#define MSG_ROW         25
#define MSG_COL         8
#define M_NOWAIT        0
#define M_WAIT          1

/*
 * Macro to plot points with the normal sense of the y axis. The
 * graphics library default is inverted (y increases in value moving
 * down the screen).
 */

#define PLOT(x_, y_)    _setpixel((x_), -(y_))

extern void Message(short, short, char *, short);

int
main()
{
    struct videoconfig config;  /* video configuration data */
    short x_org, y_org;         /* coordinates of the origin */
    short x_ul, y_ul;           /* upper-left corner */
    short x_lr, y_lr;           /* lower-right corner */
    short x, y;

    static char prompt[] = { "Press a key to continue..." };

    /*
     * Set up the medium-resloution graphics screen and set
     * the logical origin to the center of the screen.
     */

    _setvideomode(_MRES16COLOR);
    _getvideoconfig(&config);
    x_org = config.numxpixels / 2 - 1;
    y_org = config.numypixels / 2 - 1;
    _setlogorg(x_org, y_org);

    /*
     * Draw a border rectangle and coordinate system.
     */

    x_ul = -RWIDTH / 2;
    y_ul = RHEIGHT / 2;
    x_lr = RWIDTH / 2;
    y_lr = -RHEIGHT / 2;
    _rectangle(_GBORDER, x_ul, y_ul, x_lr, y_lr);
    _moveto(x_ul, 0);
    _lineto(x_lr, 0);
    _moveto(0, y_ul);
    _lineto(0, y_lr);

    /*
     * Plot some curves.
     */

    for (x = -100; x <= 100; ++x) {
     y = x / 2;
     PLOT(x, y);
    }
    Message(11, 27, "y = x / 2", M_NOWAIT);
    for (x = -20; x <= 20; ++x) {
     y = (x * x / 3) - 75;
     PLOT(x, y);
    }
    Message(22, 3, "y = x**2 / 3 - 75", M_NOWAIT);

    /*
     * Wait for the user's command to continue.
     */
    Message(MSG_ROW, MSG_COL, prompt, M_WAIT);

    /*
     * Restore the original video mode.
     */
    _setvideomode(_DEFAULTMODE);

    return (0);
}


Figure 6:  Message.C

/***************************************************************
* Message
*
* DESCRIPTION:  Display the message text at the specified
*  screen location (row, col).  If wait has a non-zero value,
*  wait for the user to press a key.  When the user complies,
*  grab the character from the keyboard buffer so it won't
*  interfere with the calling program following the return.
***************************************************************/

#include <conio.h>
#include <graph.h>

void
Message(row, col, text, wait)
short row, col; /* text position */
char *text;     /* text pointer */
short wait;     /* wait flag */
          /* (wait != 0 means wait for a keypress) */
{
    int k;      /* key code */

    /*
     * Write the prompt text at the specified location.
     */
    _settextposition(row, col);
    _outtext(text);

    /*
     * If the wait flag is set, wait for a key to be pressed,
     * then remove the code from the keyboard buffer. Handle
     * extended codes by grabbing two bytes if the first is NUL.
     */
    if (wait) {
     while (!kbhit())
         ;
     k = getch();            /* read the character */
     if (k == '\0')
         /* extended code -- get next byte */
         getch();
    }
}


Figure 7:  GDEMO.MAK as Created by QuickC.

#
# Program: Gdemo
#

.c.obj:
     qcl -c  -W2 -Ze -AM $*.c

gdemo.obj:   gdemo.c

message.obj: message.c

Gdemo.exe : gdemo.obj message.obj
     del Gdemo.lnk
     echo gdemo.obj+ >>Gdemo.lnk
     echo message.obj  >>Gdemo.lnk
     echo Gdemo.exe >>Gdemo.lnk
     echo Gdemo.map >>Gdemo.lnk
     link @Gdemo.lnk $(LDFLAGS);


Figure 8:  GDEMO.MAK Edited to work with the Full Optimizing Complier.

# makefile for the GDEMO program

.c.obj:
     cl -c -W2 -Ze -AL $*.c

gdemo.obj:   gdemo.c

message.obj: message.c

gdemo.exe:   gdemo.obj message.obj link gdemo message, gdemo, nul, graphics;

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

Ask Dr. Bob!


Keeping the Faith

Dear Dr. Bob,

Charles Petzold's article, "A Simple Windows Application For Custom Color
Mixing," MSJ Vol. 2 No. 2, must contain an error. I tried running the
program under the latest beta version of Windows and it will not run.
However, it runs fine under an earlier version. What gives?──FH

Dear FH,

The COLORSCR  program uses wndclass.hbrBackground to paint its background
color, which is fine for most applications. However, it is a program in
which different instances of the application will have different color
backgrounds. Since the window class is normally common to all instances of
an application, using the wndclass.hbrBackground is a bit of a problem. In
each instance, COLORSCR "solves" this problem by reregistering the window
class, so that each one will have its own wndclass.hbrBackground.

However, that's not the right way to do it. It works in Windows 1.0x and
the early beta releases of Windows 2.0, but if you try running COLORSCR
under the final Windows 2.0 release, you'll discover that it will only run
once. Subsequent instances refuse to start.

This is because Windows 2.0 no longer lets you reregister. The second
RegisterClass will fail if you try to register the same window class
twice. You would have been right to think that the technique of
registering the class in each instance sounded questionable.

The correct way to have different background colors in different
application instances  is to set the value of wndclass.hbrBackground to
NULL, and process the WM_ERASEBKGND message explicitly in your window
function. This works properly in all versions of Windows.

To ensure that COLORSCR uses the correct background erasing method, make
the changes listed in Figure 1 to the program.

Charles Petzold replies:

Thanks for the correction. As many Windows programmers know, the Windows
technical documentation and the actual workings of Windows are frequently
inconsistent. Often it is necessary for programmers to assume that the
documentation is wrong and to take a more empirical approach to Windows
programming.

Of course, the danger in this is that later revisions to Windows may work
differently, which is what happened in the case of COLORSCR. The program
was originally developed under Windows 1.03 and only briefly tested under
an early version of Windows 2.0.

It is interesting to note that the Windows 2.0 documentation──at least
the most recent copy  I've seen──still indicates that "If two classes
having the same name are registered, the most recently registered class is
recognized; the other is ignored." So it appears as if Windows is still
not working in accordance with the documentation.

This raises a far more serious question, and that is whether a system with
as complex and detailed an API as Windows can be documented adequately
enough to discourage developers from empirical programming. I have not yet
seen any evidence that it can, but I'm keeping the faith.


Languages Interfacing

Dear Dr. Bob,

I have developed a number of routines using Microsoft C 4.0 that I would
like to be able to utilize in the Microsoft QuickBASIC environment. How
can I accomplish this?──GY

Dear GY,

Microsoft QuickBASIC Version 4.0 will let you interface to your C
routines quite easily. There are a number of conventions to observe, but
you can interface between the two using one of two methods.

You can compile your Microsoft QuickBASIC program modules from the MS-DOS
command line using BC (the Microsoft QuickBASIC command line compiler) and
then LINK the resulting object file to any C routine object file(s) or
library. In fact, most of the functions available in the C standard
library can be called. Alternatively, you can compile your C routines,
link them into a Quick library and then load the library into the
Microsoft QuickBASIC environment. The routines would then be available to
your program from within the environment itself.

Without listing all the details, there are a number of requirements to
observe. First, any C routine called through Microsoft QuickBASIC must be
DECLARE'd as either a SUB or FUNCTION (see Figure 2). A FUNCTION procedure
can return a value whereas a SUB procedure cannot.

Second, a common calling convention must be established. For example, the
CDECL keyword in the DECLARE statement would insure that arguments passed
by the calling routine use the standard C convention for passing
arguments, in the reverse order from which they are listed, which is what
the C routine would expect. This is the most common case, and will not
require modification of your C code. It is also possible to change the
calling convention of the called function instead. For example, the called
C routine can be made consistent with BASIC by use of the fortran keyword,
as shown in Figure 3.

Third, a common naming convention must be observed. For example, the C
compiler recognizes 31 characters and does not change upper and lowercase
letters. Further, it always inserts an underscore before each routine
name. The function called_proc in Figure 3 becomes _called_proc. BASIC
recognizes up to 40 characters, drops the BASIC type declaration
characters, converts all letters to uppercase, and allows the use of a
period within variable names.

The CDECL keyword insures that the C naming convention is followed, i.e.
periods are converted to underscores and a leading underscore is added to
the name. Since the linker will ignore case (assuming /NOI is not
specified), Called.Proc will result in _Called_Proc, and the linker will
be able to resolve the external reference. This means that C routines
should not rely on case (e.g. _called_proc and _CALLED_proc) to
distinguish between different functions.

Also note that CDECL does not enable C to recognize more than 31
characters. There are two ways around this limitation. The first is to
simply avoid names longer than 31 characters. The second is to use the
Microsoft QuickBASIC keyword ALIAS, which allows a substitute name to be
passed.

Fourth, a convention must be established for passing parameters (the data
itself). Microsoft compilers pass parameters in one of three ways: by
value, by near (offset), or by far (segment + offset) reference. The
calling and called routines must agree on how a given parameter will be
passed. A variable passed by reference will give a called routine direct
access to that variable, allowing a called routine to change a variable's
value. Any such change would then have to be taken into account by the
calling procedure. Passing by value means the called routine has no actual
access to the variable. The variable's value is known and can be used, but
the variable itself cannot be changed.

As it happens, the default in BASIC is to pass parameters by near
reference. In C, the default (except for arrays) is to pass parameters by
value, unless pointers are used (see Figure 4). The C function declares
its three parameters as near pointers, meaning the parameters are expected
to be passed as references. Since this is the default method in BASIC no
other keywords are necessary.

Figure 5 shows a C function that expects a parameter passed as a far
reference, and a parameter passed as a value. Note the DECLARE statement
in the Microsoft QuickBASIC code. The SEG keyword causes a segmented (far)
reference to be passed. The BYVAL keyword causes a true value to be
passed.

This is different than the standard method of passing a value in BASIC,
which is to use two sets of parens. CALL CALC((Q)) still passes a
reference, but the reference is to a temporarily created variable and not
to the actual variable, therefore preserving the value of the original.
BYVAL actually allows a true value to be passed, rather than a reference
to a temporary variable.

Arrays should always be passed by far reference, using the SEG keyword.
Arrays of dynamic strings, as well as $STATIC arrays are near references,
but all other arrays are far references. Unless you are absolutely
confident that a near reference will work with an array, use the SEG
keyword, which will always work.

Finally, memory models must be taken into account. Microsoft QuickBASIC
uses far code addresses exclusively. This means that any C routines that
are going to be used in a Microsoft QuickBASIC program must be compiled
using the medium, large, or huge models. This is the simplest approach.
Small and compact models can be used if the specific C routines are
declared as far routines.

There are other do's and don'ts, but this covers most of the basics. The
point is that once you've mastered the rules, mixed model programming──not
only between C and BASIC but between several other languages──is relatively
simple.


TILER Correction

Dear Dr. Bob,

The program TILER printed in MSJ Vol 2, No 3 appears to have an error in
it. It seems to me that, contary to the author's assertion, the main message
loop requires the TranslateMessage call. Am I missing something subtle?──AJ

Dear AJ,

No, you aren't missing a thing. TranslateMessage does belong in the main
message loop. Anyone who typed in the program and tried to run it would
discover that it doesn't work quite right. For instance, the keyboard
interface for the Control (System) menu works only partially. The author
was under the mistaken impression that you do not need to call
TranslateMessage if you don't care about WM_CHAR messages. However,
TranslateMessage takes care of a few other things──including part of the
keyboard processing for the Control menu. Every Windows application should
call TranslateMessage in the main message loop. That loop in TILER (see p.
33 MSJ Vol 2, No. 3) should read as shown in Figure 6. By-the-way, the
author admitted to being red in the face when advised of this error.


Close Resemblance

Dear Dr. Bob,

I just received the November issue of National Geographic, and there was a
fascinating article on a family of baboons in Kenya. Well, I was certainly
surprised when I read the article and discovered that one of the baboons is
named──Dr. Bob! Can it really be you? I always knew you were a charming,
intelligent fellow, but my, what big teeth you have! Next time I'm in Kenya,
I'll be sure to stop by and say hello.──GP

Dear GP,

If you look really closely you can even see the lap-top computer, modem,
and telephone that I use to transmit and receive information between Kenya
and the United States.


Correction

The Windows/386 article in the September issue of MSJ states that
Windows/386 will run on the AT&T 6300 computer. This is clearly incorrect.
We meant to state that Windows/386 will support the AT&T(R) 6300 display.
We should also point out that the error was due to the creative work of
MSJ and not the author, Ray Duncan, who most assuredly knows his 386
machines from his 86/286's.


Figure 1:

Add a new static variable:

HBRUSH hbrBkgnd;

In WinMain, change:

    wndclass.style = CS_HREDRAW | CS_VREDRAW;
                                    ∙
                                    ∙
                                    ∙
    wndclass.hbrBackground = CreateSolidBrush(0L);
                                    ∙
                                    ∙
                                    ∙
    if( ! RegisterClass(&wndclass) )
        return FALSE;

to:

    if( ! hPrevInstance )
    {
        wndclass.style = CS_HREDRAW | CS_VREDRAW;
                                    ∙
                                    ∙
                                    ∙
        wndclass.hbrBackground = NULL;
                                    ∙
                                    ∙
                                    ∙
        if( ! RegisterClass(&wndclass) )
            return FALSE;
    }
    hbrBkgnd = CreateSolidBrush(0L);

In WndProc, add this local variable:

    RECT rect;

In both places where it occurs, change:

    DeleteObject( GetClassWord(hWnd,GCW_HBRBACKGROUND) );

to:

    DeleteObject(hbrBkgnd);

Under SB_THUMBPOSITION, change:

    SetClassWord( hWnd, GCW_HBRBACKGROUND,
        CreateSolidBrush( RGB(color[0],color[1],color[2]) ) );

to:

    hbrBkgnd = CreateSolidBrush( RGB(color[0],color[1],color[2]) );

Add one new case to WndProc:

    case WM_ERASEBKGND:
        GetClientRect( hWnd, &rect );
        FillRect( (HDC)wParam, &rect, hbrBkgnd );
        break;


Figure 2:

DECLARE SUB Exchange CDECL (a%, b%)
' There is no return value

DECLARE FUNCTION Power% CDECL (a%)
' Declaring Power% as FUNCTION allows
' a value to be returned by Power%


Figure 3:

int fortran called_proc (int a, char b)
{
/* The function body goes here. Note the
 * fortran keyword, which will pass
 * parameters consistent with the BASIC
 * convention.
 */
}


Figure 4:

DECLARE SUB Exchange CDECL (a%, b%)

A% = 10
B% = 20
C% = 30

PRINT A%; B%; C%
CALL Exchange(A%,B%,C%)
PRINT A%; B%; C%

/* C routine exchange() */

void exchange(int near *x,int near *y, int near *z)
{
int a1;
int a2;

a1 = *x;
a2 = *y;
*x = *z;
*y = a1;
*z = a2;
}

/* A%, B% and C% are passed by near reference
 * to exchange, which manipulates their
 * values using near pointers. The new
 * values become available immediately
 * after the c routine returns control
 */


Figure 5:

DECLARE FUNCTION AddValue% CDECL (SEG a%, BYVAL b%)

D% = 10
E% = 20
F% = D% + E%

PRINT D%; E%; F%
PRINT D%; E%; AddValue%(D%, E%)

' The FUNCTION declaration allows AddValue AddValue to return a value

/* C routine addvalue() */

int addvalue(far *y, int n)
{
int sum;
sum = *y + n;

return(sum);
}

/* The C routine expects a far reference
 * for its first parameter, as indicated
 * by SEG a%, and receives its second
 * parameter by value, as indicated by
 * BYVAL b%.
 */


Figure 6:

     /* Main message processing loop */
    while( GetMessage( &msg, NULL, 0, 0 ) ) {
        TranslateMessage( &msg );
        DispatchMessage( &msg );
    }