Home of the original IBM PC emulator for browsers.
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 (¤t_time);
target_time = current_time + 1;
while (current_time < target_time)
{
time (¤t_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 ' ®='
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