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 proce