home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Programming Tool Box
/
SIMS_2.iso
/
bp_6_93
/
bonus
/
winer
/
chap11.txt
< prev
next >
Wrap
Text File
|
1994-09-03
|
147KB
|
3,347 lines
CHAPTER 11
ACCESSING DOS AND BIOS SERVICES
BASIC is arguably the most capable of all the popular high-level languages
available for the PC. However, one area where all PC languages are weak is
when accessing DOS and BIOS system interrupts. Previous chapters included
subroutines and functions that access DOS interrupt services using CALL
Interrupt, but in most cases with little explanation. This chapter
explains what interrupts are, how they are accessed, and how they return
information to your program.
Only assembly language--the native language of the processor in every
PC--can directly access interrupts. Assembly language programmers use the
Int instruction, which transfers control to an *interrupt service routine*.
An Int instruction is nearly identical to a conventional CALL statement,
except a slightly different mechanism within the computer's hardware is
used to implement it.
BASIC lets you access system interrupts by providing a pair of
assembly language interface routines called Interrupt and InterruptX.
These routines accept the interrupt number and other parameters the
interrupt requires, and they then perform the actual interrupt call.
InterruptX is similar to Interrupt; the only real difference is that it
lets you access two additional CPU registers.
WHAT IS AN INTERRUPT?
=====================
The IBM PC family of personal computers supports two types of interrupts:
hardware and software. A hardware interrupt is invoked by an external
device or event, such as pressing a key on the keyboard. When this
happens, a signal is sent from the keyboard hardware to the PC's
microprocessor telling it to stop what it's currently doing and instead
call one of the routines in the PC's BIOS.
For example, while your PC is currently copying a group of files you
may type DIR simultaneously, to display the results when the copying has
finished. Even though DOS is reading and writing the files, you interrupt
those operations for a few microseconds each time a key is pressed. The
BIOS routine that handles the keyboard interrupt is responsible for placing
the keystrokes into the PC's 15-character keyboard buffer. Then when DOS
has finished copying your files, the DIR command will already be there.
Because there is a direct physical connection between the keyboard
circuitry and the PC's microprocessor, you are able to interrupt whatever
else is happening at the time.
A software interrupt, on the other hand, doesn't really interrupt
anything. Rather, it is a form of CALL command that an assembly language
program may issue. Just like the CALL command in BASIC that transfers
control to a subroutine, a software interrupt is used in an assembly
language program to access DOS and BIOS services. Although assembly
language programs may use a CALL statement to invoke a subroutine, an
interrupt instruction is needed to access the operating system routines.
When a program issues a subroutine call, the address of that
subroutine must be known, so the processor will be able to jump to the code
there. With most programs, subroutine addresses are determined and
assigned by LINK.EXE when it combines the various portions of your program
into a single executable file. But this method can't be used with the DOS
and BIOS routines, because their addresses are not known ahead of time.
For example, if you compile a BASIC program on an IBM PC, it must also be
able to be run on, say, a Tandy 1000 using a different version of DOS. Of
course, it is impossible for LINK to know where the DOS and BIOS routines
are located on the Tandy computer.
To solve this problem and allow a program to call a routine whose
address is not known, a list of addresses is stored in a known place in low
memory. This place is called the *interrupt vector table*. The first
1,024 bytes in every PC contains a table of addresses for all 256 possible
interrupts. Each table entry requires two words (four bytes): one word is
used to hold the routine's segment, and the other holds its address within
that segment. Whenever an assembly language program issues an interrupt
instruction, the PC's processor automatically fetches the segment and
address from this table, and then calls that address. Thus, any program
may access any interrupt routine, without having to know where in memory
the routine actually resides. The first four bytes in the interrupt vector
table hold the address for Interrupt 0, the next four show where Interrupt
1 is, and so forth.
DOS and BIOS services are specified by interrupt number, and most
interrupt routines also expect a *service number*. Nearly all of the DOS
services you will find useful are accessed through Interrupt &H21, with the
desired service number specified in the AH register. In many cases,
information is also returned in the CPU registers. For instance, the DOS
service that returns the current default disk drive is specified by placing
the value &H19 in the AH register. When the interrupt has finished, the
current drive number is returned in the AL register. Registers will be
described in the section that follows. As with the low memory addresses
discussed in Chapter 10, the DOS and BIOS interrupt numbers use Hexadecimal
numbering by convention.
There are also several BIOS interrupts you will find useful, and these
include video interrupt &H10, printer interrupt &H17, Print Screen
interrupt 5, and the two equipment interrupts &H11 and &H12. There are
other BIOS and DOS interrupts, but those are mostly useful when accessed
from assembly language. For example, there is little need to call keyboard
interrupt &H16 to read a key, since INKEY$ already does this. Likewise,
you are unlikely to find disk interrupt &H13 very interesting, although it
is used when performing copy protection and other low-level direct disk
accesses. But unless you know what you are doing, it is possible--even
likely--to trash your hard disk in the process of experimenting with this
disk interrupt.
I won't attempt to provide all of the information you need to access
every possible DOS and BIOS service here. Indeed, a complete discussion
would fill several books. Two excellent books that I recommend are "Peter
Norton's Programmer's Guide to the IBM PC" (1988), and "Advanced MS-DOS",
by Ray Duncan (1988). Both of these books are published by Microsoft
Press, and can be found in most book stores. These books list every DOS
and BIOS interrupt service, and show which registers are used to exchange
information with each interrupt service.
Also, once you have read and understood the information in this
chapter you should go back to some of the examples presented in earlier
chapters. In particular, Chapter 6 shows how to access DOS Interrupt &H21
to read file names, and Chapter 7 includes routines that access Interrupt
&H2F to see if a network is running on the host PC and if so which one.
REGISTERS
=========
Microprocessors in the Intel 8086 family contain a set of built-in integer
variables called *registers*. Each register can hold a single word (two
bytes), which nicely corresponds to the size of a BASIC integer variable.
Because these registers are contained within the microprocessor itself,
they can be accessed by the CPU very quickly--much faster than variables
which are stored in memory.
The 8086 and 8088 microprocessors contain a total of fourteen
registers. [Newer CPUs contain more registers, but they are not accessible
via CALL Interrupt nor are they useful to a BASIC program.] Some of these
registers are intended for a specific use, while others may be used as
general purpose variables. For example, the CS and DS registers contain
the current code and data segments respectively, while the CX register is
often used as a counter in an assembly language FOR/NEXT loop. I'm not
going to pursue a lengthy discussion of microprocessor theory here though,
because it's not really necessary if you simply want to access a few system
interrupts. Rather, I will focus on how to set up and invoke the various
interrupt services, and interpret the results they return. Assembly
language and CPU registers will be discussed more fully in Chapter 12.
Both Interrupt and InterruptX (Interrupt Extended) require a TYPE
variable with components that mirror each of the processor's registers.
Figure 11-1 lists all of the 8086 registers that are accessible from BASIC,
showing which are available with each of the interrupt routines.
InterruptX Interrupt
========== =========
AX AX
BX BX
CX CX
DX DX
BP BP
SI SI
DI DI
Flags Flags
DS
ES
Figure 11-1: The registers accessible from BASIC through Interrupt and
InterruptX.
When you call the either Interrupt routine, the values in a TYPE variable
are copied into the CPU's registers, the interrupt is performed, and then
the results returned in each register are copied back into a TYPE variable
again. All of the CALL Interrupt examples Microsoft shows use two TYPE
variables called InRegs and OutRegs. However, you can also use the same
TYPE variable to both send and receive the register values. In fact, using
a single TYPE variable will save a few bytes of DGROUP memory. Therefore,
the remaining examples that use CALL Interrupt use a single TYPE variable.
One important issue that needs to be addressed before we can proceed
is how the CPU registers are accessed. I stated earlier that there are
fourteen such registers, and each is the same size as an integer variable:
2 bytes. While this is certainly true, there is more to the story. Four
of the registers--AX, BX, CX, and DX--can also be treated as being two
separate one-byte registers.
Each register half uses the designator "H" or "L" to mean High or Low.
For example, the high-byte portion of AX is called AH, and the low-byte
portion of CX is CL. When considered as a composite register, the two
halves form a single integer word. Figure 11-2 shows how the AX register
is constructed, with each half contributing to the total combined value.
│<──────────────────── AX ─────────────────────>│
╔════════════════════════╤════════════════════════╗
║ 1 1 0 1 0 0 0 1 │ 1 1 0 0 1 1 0 1 ║
╚════════════════════════╧════════════════════════╝
│<───────── AH ─────────>│<───────── AL ─────────>│
Figure 11-2: How a single word-sized register may also be treated as two
byte-sized registers.
In an assembly language program it is simple to access each register half
separately. However, BASIC does not offer a byte-sized variable type to
use within the TYPE declaration. Therefore, a slight amount of math is
required to get at each half separately. Although a fixed-length string
with a length of one character could be used, the added overhead BASIC
imposes to access a string as a number reduces the usefulness of that
approach.
Using Hexadecimal notation and multiplication simplifies access to
each register half when it is being assigned, and integer division and
BASIC's AND operator lets you separate the two halves when reading them.
That is, you can assign the value &H12 to the upper byte in AH and the
value &H34 to the lower byte in AL at one time, like this:
Registers.AX = &H1234
In many cases it is necessary to assign only AH, which can be done like
this:
Registers.AX = &H0600
Here, the value 6 is placed into AH, and 0 is assigned to AL. Since many
of the DOS and BIOS services ignore what is in AL, assigning a value of
zero is the simplest and most effective solution. Again, using Hexadecimal
notation lets you clearly define what is in each register half, because the
first two digits represent the upper portion, and the second two represent
the lower byte.
When both the upper and lower bytes are important, you can use
multiplication to assign them. By definition, any byte value in the high
portion of a register is 256 times greater than it would be in the lower
part. Thus, to assign the variable Low% to AL and High% to AH is as simple
as this:
Registers.AX = Low% + (256 * High%)
In practice the parentheses are not really necessary because multiplication
is always performed before addition. But I included them here for clarity.
When an interrupt routine returns information in one of the
combination registers, you may easily isolate the high and low portions as
follows:
Low% = Registers.DX AND 255
High% = Registers.DX \ 256
Some examples you may have seen use MOD to extract the lower byte, and that
will also work:
Low% = Registers.DX MOD 256
Although MOD and AND cause BASIC to generate the same amount of assembly
language code (three bytes), I generally prefer using AND because that
instruction is somewhat faster on the older 8088 processors.
ACCESSING THE BIOS
==================
The simplest BIOS interrupt to call is the Print Screen interrupt,
Interrupt 5. No parameters are required by this interrupt, and no values
are returned when it finishes. But since the Interrupt routine expects the
TYPE variable to be present and copies data to it, you must still dimension
it in your program.
Because Interrupt and InterruptX are external subroutines as opposed
to built-in commands, you will need to load the Quick Library containing
these routines. QuickBASIC comes with the file QB.QLB; BASIC PDS provides
the same routines in a library named QBX.QLB. [And in VB/DOS this file is
called VBDOS.QLB.] You must of course use whichever is appropriate for
your version of BASIC. To start QuickBASIC and load the Quick Library that
contains these routines use the /L switch like this:
qb /l
Normally, the name of a Quick Library must be given after the /L switch.
However, QB and QBX know that /L by itself means to load the default QB.QLB
or QBX.QLB Quick Library.
The following complete program prints a simple pattern on the screen,
and then sends it to the printer designated as LPT1: as if the PrtSc key
had been pressed.
DEFINT A-Z
TYPE RegType
AX AS INTEGER
BX AS INTEGER
CX AS INTEGER
DX AS INTEGER
BP AS INTEGER
SI AS INTEGER
DI AS INTEGER
Flags AS INTEGER
END TYPE
DIM Registers AS RegType
CLS
FOR X% = 1 TO 24
PRINT STRING$(80, X% + 64);
NEXT
CALL Interrupt(5, Registers, Registers)
Although the Registers TYPE definition is shown here, the remaining
examples in this chapter will instead specify the REGTYPE.BI include file
that contains this code. QuickBASIC includes a similar include file called
QB.BI, and BASIC PDS uses the name QBX.BI for the same file. [I created
REGTYPE.BI so all of the programs in this book will run as is with any
version of BASIC. But the BASIC-supplied versions also include DECLARE
statements for the Interrupt routines, where my REGTYPE.BI file does not.
Since all of these programs use the CALL keyword, a declaration is not
strictly necessary.]
THE BIOS VIDEO INTERRUPT
The next example shows how to call BIOS video interrupt &H10 to clear just
a portion of the display screen. It is designed as a combination
demonstration and subprogram, so you can extract just the subprogram and
add it to programs of your own.
DEFINT A-Z
DECLARE SUB ClearScreen (ULRow, ULCol, LRRow, LRCol, Colr)
'$INCLUDE: 'REGTYPE.BI'
DIM SHARED Registers AS RegType
CLS
FG = 7: BG = 1 'set the foreground and background colors
COLOR FG, BG
FOR X% = 1 TO 24
PRINT STRING$(80, X% + 64);
NEXT
Colr = FG + 16 * BG 'use the same colors for clearing
CALL ClearScreen(5, 10, 20, 70, Colr)
SUB ClearScreen (ULRow, ULCol, LRRow, LRCol, Colr) STATIC
Registers.AX = &H600
Registers.BX = Colr * 256
Registers.CX = (ULCol - 1) + (256 * (ULRow - 1))
Registers.DX = (LRCol - 1) + (256 * (LRRow - 1))
CALL Interrupt(&H10, Registers, Registers)
END SUB
There are two important benefits to using the BIOS for a routine such as
this. One is of course the reduced amount of code that is needed, when
compared to manually looping through memory using POKE to clear each
character position. The second is the BIOS is responsible for determining
the type of monitor installed, to select the correct video segment.
The demonstration portion of the program first clears the screen, and
then creates a simple test pattern using a color of white on blue. Just
before the call to ClearScreen, the correct Colr parameter is calculated
based on the same foreground and background specified to BASIC. Where
BASIC accepts separate foreground and background values, the BIOS requires
a single composite color byte.
The simplified formula used in this example will accommodate normal
colors, but does not support adding 16 to the foreground to specify a
flashing color. This next formula shows how to derive a single color byte
while also honoring flashing:
Colr = (FG AND 16) * 8 + ((BG AND 7) * 16) + (FG AND 15)
ClearScreen is then called telling it to clear a rectangular portion of the
screen that lies within the boundary specified by an upper-left corner at
location 5, 10 to the lower-right corner at location 20, 70. The color
value calculated earlier is also passed, so the white on blue color will be
maintained even after the screen is cleared.
Within ClearScreen, four of the CPU's registers are assigned to values
needed by the BIOS video interrupt. The first statement specifies service
6 in AH, which tells the BIOS to scroll the screen. The number of rows to
scroll is then placed into the AL register, which we've set to zero. This
particular BIOS service recognizes zero as a special flag, which tells it
to clear the screen rather than scroll it.
Service 6 also expects the color to use for clearing in the BH
register. As I explained earlier, multiplying by 256 is equivalent to
assigning just the higher portion of an integer, so the statement
Registers.BX = Colr * 256 is equivalent to placing the one byte that is
actually used by the Colr variable into BH.
The next two instructions take the upper left and lower right corner
arguments, and place them into the appropriate registers. In this case,
the upper left column is placed into CL and the upper left row in CH.
Similarly, the lower right column goes into DL and the lower right row into
DH. Even though BASIC considers screen rows and columns to be numbered
beginning at 1, the BIOS routines assume these to be zero-based.
Therefore, 1 is subtracted from the parameters before they are placed into
each component of the Registers TYPE variable. Finally, BASIC's Interrupt
routine is called specifying Interrupt number &H10.
Note that the same BIOS interrupt service can also be used to scroll a
rectangular portion of the screen. Indeed, this is the primary purpose of
service 6. To scroll a portion of the screen up a certain number of lines,
you will place the number of lines into AL:
Registers.AX = NumLines + (6 * 256)
Scrolling the screen downward is also possible, using service 7 like this:
Registers.AX = NumLines + (7 * 256)
Also note that the Registers TYPE variable was dimensioned to be shared.
This allows it to be accessed from all of the subprograms in a single
program. If Registers is dimensioned in many different subprograms and
functions, then a new instance will be created, with each stealing 20 bytes
of DGROUP memory. Beware, however, that this memory savings has the
potential drawback of introducing subtle bugs due to the same variable
being used by different services. Whatever register values remain after
one use of CALL Interrupt will still be present the next time, unless new
values are explicitly assigned. [But that is rarely a problem, since you
will generally assign all of the registers that a given interrupt needs
just before calling that interrupt.]
Although this short example simply clears or scrolls a portion of the
display screen, it provides a foundation for nearly anything else you may
need to do using CALL Interrupt. The DOS interrupt examples that follow
will build on this foundation, and show how to access a wealth of useful
services that are not otherwise possible using BASIC alone.
ACCESSING DOS INTERRUPTS
========================
As with the BIOS video interrupt services, DOS interrupt &H21 expects a
service number to be given in the AH register. Many DOS services require
additional information in other registers as well, including integer values
and the segments and addresses of variables.
The DOS services that accept or return a string (such as a file or
directory name) require the address of the string, to know where it is
located. For example, the DOS service that changes the current directory
is called with AH set to &H3B, and DS:DX holding the address of a string
that contains the name of the directory to change to.
Likewise, to obtain the current directory you would load AH with the
value &H47, and DS:SI with the address of a string that will receive the
current directory's name. It is essential that this string already be
initialized to a sufficient length before calling DOS. Otherwise, the
returned directory name will likely overwrite other existing data. [And if
that data happens to be a BASIC string descriptor or back pointer you will
likely crash the program and possibly even have to reboot the PC.]
When a string is sent as a parameter to a DOS routine, it must be
terminated with a CHR$(0), so DOS can tell where it ends. Likewise, when
DOS returns a string to your program such as the current directory, it
indicates the end with a CHR$(0). Therefore, it is up to your program to
manually append a CHR$(0) to any file or directory names you pass to DOS.
And when receiving a string from DOS, you must use INSTR to locate the
CHR$(0) that marks the end, and keep only what precedes that character.
I will start with some simple examples that access DOS Interrupt &H21,
and proceed to more complex routines that pass and receive string data.
ACCESSING THE DEFAULT DRIVE
The first DOS example shows how to determine the current default drive, and
it is designed as a DEF FN-style function. A function is a natural way to
design a routine that returns information, as opposed to a called
subprogram. Further, using a DEF FN-style function reduces the amount of
code that BASIC generates, and also reduces the code needed each time the
function is invoked.
DEFINT A-Z
'$INCLUDE: 'REGTYPE.BI'
DIM Registers AS RegType
DEF FnGetDrive%
Registers.AX = &H1900
CALL Interrupt(&H21, Registers, Registers)
FnGetDrive% = (Registers.AX AND &HFF) + 65
END DEF
PRINT "The current default drive is "; CHR$(FnGetDrive%)
Here, service number &H19 is assigned to the AH portion of AX prior to
calling Interrupt &H21, and the value that DOS returns in AL indicates the
current drive. For this service DOS uses 0 to indicate drive A, 1 for
drive B, and so forth. Therefore, you use AND with the value &HFF (255) to
keep just the low portion in AX. Once the DOS drive number has been
isolated, the program adds 65 to adjust that to the equivalent ASCII
character value.
Setting a new default drive is just as easy as obtaining the current
drive. Although BASIC PDS provides the CHDRIVE command to set a new drive
as the current default, QuickBASIC does not. The ChDrive subprogram that
follows affords the same functionality to QuickBASIC users, and it accepts
a single letter to indicate which drive is to be made the new current
default.
DEFINT A-Z
DECLARE SUB ChDrive (Drive$)
'$INCLUDE: 'REGTYPE.BI'
DIM SHARED Registers AS RegType
INPUT "Enter the drive to make current: ", NewDrive$
CALL ChDrive(NewDrive$)
SUB ChDrive (Drive$) STATIC
Registers.AX = &HE00
Registers.DX = ASC(UCASE$(Drive$)) - 65
CALL Interrupt(&H21, Registers, Registers)
END SUB
Now that you know how to set and get the current default drive, you can
combine the two and create a function that tells if a given drive letter is
valid. Many DOS services return the success or failure of an operation
using the CPU's Carry flag. However, the service that sets a new drive is
a notable exception. Therefore, to determine if a given drive letter is in
fact valid requires more than simply trying to set the new drive, and then
seeing if an error resulted.
The only way to tell if a request to change the current drive was
accepted is to make another call to get the current drive, thereby seeing
if the original request took effect. The program that follows accepts a
drive letter as a string, and returns True or False (-1 or 0) to indicate
whether or not the drive is valid.
DEFINT A-Z
DECLARE SUB ChDrive (Drive$)
'$INCLUDE: 'REGTYPE.BI'
DIM SHARED Registers AS RegType
DEF FnGetDrive%
Registers.AX = &H1900
CALL Interrupt(&H21, Registers, Registers)
FnGetDrive% = (Registers.AX AND &HFF) + 65
END DEF
DEF FnDriveValid% (TestDrive$)
STATIC Current 'local to this function
Current = FnGetDrive% 'save the current drive
FnDriveValid% = 0 'assume not valid
CALL ChDrive(TestDrive$) 'try to set a new drive
IF ASC(UCASE$(TestDrive$)) = FnGetDrive% THEN
FnDriveValid% = -1 'they match so it's valid
END IF
CALL ChDrive(CHR$(Current)) 'either way restore it
END DEF
INPUT "Enter the drive to test for validity: ", Drive$
IF FnDriveValid%(Drive$) THEN
PRINT Drive$; " is a valid drive."
ELSE
PRINT "Sorry, drive "; Drive$; " is not valid."
END IF
SUB ChDrive (Drive$) STATIC
Registers.AX = &HE00
Registers.DX = ASC(UCASE$(Drive$)) - 65
CALL Interrupt(&H21, Registers, Registers)
END SUB
The strategy used here is to first save the current default drive, and then
set a new drive on a trial basis. If the current drive is the one that was
just set, then the specified drive was indeed valid. In either case, the
original drive must be restored.
DETERMINING IF A FILE EXISTS
Both of the DOS services we have considered so far use integer arguments to
indicate the new drive, or which drive is the current default. The next
example shows how to pass a BASIC string to a DOS service, which is
somewhat more complicated. The situation is made worse by the far strings
feature available in BASIC PDS. Therefore, be sure to observe the comment
that shows how to replace SSEG with VARSEG for use with QuickBASIC.
Chapter 6 showed an admittedly clunky way to determine if a file is
present. The example given there attempted to open the specified file for
random access, and then used LOF to see if the file had a length of zero.
The problem with that method--besides requiring a lot of unnecessary DOS
activity--is that it reports a file with a perfectly legal length of zero
as not being present, and then deletes it!
The FnFileExist function that follows is intended for use with BASIC
PDS, and comments show how to change it for use with QuickBASIC. Please
understand that PDS doesn't really need a File Exist function, since DIR$
can be used for that purpose. The statement IF LEN(DIR$(FileSpec$)) THEN
will quickly tell if a file is present. However, the point is to show how
strings are passed to DOS, and for that purpose this example serves quite
nicely.
DEFINT A-Z
'$INCLUDE: 'REGTYPE.BI'
DIM Registers AS RegType
TYPE DTA 'used by DOS services
Reserved AS STRING * 21 'reserved for use by DOS
Attribute AS STRING * 1 'the file's attribute
FileTime AS STRING * 2 'the file's time
FileDate AS STRING * 2 'the file's date
FileSize AS LONG 'the file's size
FileName AS STRING * 13 'the file's name
END TYPE
DIM DTAData AS DTA
DEF FnFileExist% (Spec$)
FnFileExist% = -1 'assume the file exists
Registers.DX = VARPTR(DTAData) 'set a new DOS DTA
Registers.DS = VARSEG(DTAData)
Registers.AX = &H1A00
CALL InterruptX(&H21, Registers, Registers)
Spec$ = Spec$ + CHR$(0) 'DOS needs an ASCIIZ string
Registers.AX = &H4E00 'find file name service
Registers.CX = 39 'attribute for any file
Registers.DX = SADD(Spec$) 'show where the spec is
Registers.DS = SSEG(Spec$) 'use this with BASIC PDS
'Registers.DS = VARSEG(Spec$) 'use this with QuickBASIC
CALL InterruptX(&H21, Registers, Registers)
IF Registers.Flags AND 1 THEN FnFileExist% = 0
END DEF
INPUT "Enter a file name or specification: ", FileSpec$
IF FnFileExist%(FileSpec$) THEN
PRINT FileSpec$; " does exist"
ELSE
PRINT "Sorry, no files match "; FileSpec$
END IF
FnFileExist calls upon the DOS Find First service that searches a directory
and attempts to locate the first file that matches a given specification
template. Therefore, besides being able to see if ACCOUNTS.DAT or
F:\UTILS\NU.EXE exist, you can also use the DOS wild cards. For example,
given C:\QB45\*.BAS, FnFileExist will report if any files with a .BAS
extension are in the \QB45 directory of drive C.
As part of its directory searching mechanism, DOS requires a block of
memory known as a Disk Transfer Area, or DTA for short. If a matching file
name is found, DOS stores important information about the file there, where
your program can read it. As you can see by examining the DTAType
structure, this includes the file's name and extension, the date and time
it was last written, to, its current size, and attribute. The 21-byte
string at the beginning identified as Reserved holds sector numbers and
other information, and is used by DOS for subsequent searches. This
function doesn't use any of the information in the DTA; however, it must
still be defined for use by DOS.
You will notice that FnFileExist uses the InterruptX routine rather
than Interrupt, and this is to provide support for use with BASIC PDS far
strings. Two of the CPU's registers are used to hold the DS and ES data
segment registers. When Interrupt is called, it simply leaves whatever is
currently in DS and ES and then calls the interrupt. InterruptX, on the
other hand, loads DS and ES from those components of the Registers TYPE
variable, and those are the values the interrupt itself receives. Were
FnFileExist limited to working with QuickBASIC [where all strings are in
the DS segment], Interrupt would be sufficient and the added complication
of using either VARSEG or SSEG could be avoided.
Note that InterruptX can also be told to use the current value of DS
for both DS and ES, when the calling program doesn't need or want to change
them. This is specified by placing a value of -1 into either or both
portions of the Registers TYPE variable. For example, the statement
Registers.DS = -1 tells InterruptX not to assign DS before performing the
interrupt. Otherwise, if Registers.DS were not assigned, DS would receive
the value 0 which is incorrect for DOS services that receive a variable's
address. In a similar manner, Registers.ES = -1 tells InterruptX to set ES
to the current value of DS.
THE CARRY FLAG
The last item to note in this function is how the Carry flag is tested. As
I mentioned earlier, many DOS services indicate the success or failure of
an operation by either clearing or setting the CPU's Carry flag. This flag
is held in one bit in the Flags register, and its primary purpose is to
assist multi-word arithmetic in assembly language programs. But because
the 80x86 provides single instructions that easily set and test this flag,
the designers of DOS decided to use it as an error indicator.
The Carry flag is stored in the lowest bit of the Flags register, and
can therefore be tested using the AND instruction with a value of 1. If
that bit is set, the result of the AND test will be one; otherwise it will
be zero. Thus, the statement IF Registers.Flags AND 1 THEN will be true if
the Carry flag is set, which indicates an error. In the case of DOS' Find
First function this is not really an error in the strictest sense. But
there is no need here to distinguish between, say, an invalid path name and
the lack of any matching files. Either a match was found or it wasn't.
IMPROVING ON INTERRUPT
======================
Recall that Chapter 8 introduced the DOSInt routine which serves as a
small-code replacement for BASIC's InterruptX routine. Although the
reduction in code size gained by using DOSInt versus Interrupt or
InterruptX is not dramatic, it can save several hundred bytes in a program
that calls it many times. DOSInt is also somewhat easier to set up and
use, because it requires only a single Registers argument.
Of course, DOSInt is meant only for use with DOS Interrupt &H21, and
it will not work with any other DOS or BIOS interrupt services. Because of
the savings that DOSInt affords, the remaining DOS examples in this chapter
will use DOSInt instead of Interrupt or InterruptX. Like InterruptX,
DOSInt lets you access the DS and ES registers, and it also recognizes an
incoming value of -1 to specify the current contents of DS.
OBTAINING THE CURRENT DIRECTORY
Where FnFileExist shows how to pass a BASIC string to a DOS interrupt
service, the FnGetDir function following shows how to receive a string from
DOS. Again, BASIC PDS users have the CURDIR$ function which reports the
current directory, but most QuickBASIC programmers will find this function
invaluable.
DEFINT A-Z
'$INCLUDE: 'REGTYPE.BI'
DIM Registers AS RegType
DEF FnGetDir$ (Drive$)
STATIC Temp$, Drive, Zero 'local variables
IF LEN(Drive$) THEN 'did they pass a drive?
Drive = ASC(UCASE$(Drive$)) - 64
ELSE
Drive = 0
END IF
Temp$ = SPACE$(65) 'DOS stores the name here
Registers.AX = &H4700 'get directory service
Registers.DX = Drive 'the drive goes in DL
Registers.SI = SADD(Temp$) 'show DOS where Temp$ is
Registers.DS = SSEG(Temp$) 'use this with BASIC PDS
'Registers.DS = -1 'use this with QuickBASIC
CALL DOSInt(Registers) 'call DOS
IF Registers.Flags AND 1 THEN 'must be an invalid drive
FnGetDir$ = ""
ELSE
Zero = INSTR(Temp$, CHR$(0)) 'find the zero byte
FnGetDir$ = "\" + LEFT$(Temp$, Zero)
END IF
END DEF
PRINT "Which drive? ";
DO
Drive$ = INKEY$
LOOP UNTIL LEN(Drive$)
PRINT
Cur$ = FnGetDir$(Drive$)
IF LEN(Cur$) THEN
PRINT "The current directory is ";
PRINT Drive$; ":"; FnGetDir$(Drive$)
ELSE
PRINT "Invalid drive"
END IF
PRINT "The current directory for the default drive is ";
PRINT FnGetDir$("")
The variables Temp$, Drive, and Zero are declared as STATIC to prevent them
from conflicting with variables of the same name in your program. Of
course, you could convert this to a formal FUNCTION procedure if you
prefer, which considers variables local by default. Converting to a formal
function is also needed if you plan to access it from multiple source
modules.
Unlike the DOS Get Drive and Set Drive services, service &H47 uses a
value of one to indicate drive A, 2 for drive B, and so forth. To request
the current directory on the default drive you must use a value of zero.
An explicit test for this is made at the beginning of the function. Later,
this value is assigned to Registers.DX where DOS expects it. Note that it
is really DL that will hold the specified drive number. But assigning DX
from Drive as shown does this, and also clears the high (DH) portion in the
process. Since the contents of DH are ignored by this DOS service, no harm
is done and the extra code that would be needed to assign only DL can be
avoided.
As I mentioned earlier, it is essential that you set aside space to
hold the returned directory name. Since the longest path name that DOS can
accommodate is 65 characters, Temp$ is assigned to that length. Then, the
segment and address where Temp$ is stored are passed to DOS in the DS and
SI registers. Note that DOS is not very consistent in its use of
registers. Where the service that finds the first matching file name uses
DS:DX to point to the file specification, this service uses DS:SI to point
to the string.
Like the FnFileExist function, you must change the statement that
assigns Registers.DS if you plan to use this one with QuickBASIC. The
BASIC PDS version of that statement is left active rather than the
QuickBASIC version, so QuickBASIC will highlight that line as an error to
remind you. Although FnFileExist uses VARSEG for the DS value when used
with QuickBASIC, FnGetDir uses -1. Both methods work, and I used -1 here
just to show that in context.
After DOSInt is called to load Temp$ with the current directory name,
the Carry Flag is tested to see if an error occurred. The only error that
is possible here is "Invalid drive", in which case FnGetDir$ is assigned a
null value as a flag to indicate that. Otherwise, INSTR is used to locate
the CHR$(0) zero byte that DOS assigned to mark the end of the name.
This error testing can be left out to save code if you prefer. You
could also validate the drive using the FnDriveValid function, either by
adding the code within FnGetDir, or separately prior to invoking it.
READING FILE AND DIRECTORY NAMES
One important service that many programs need and which BASIC has never
provided is the ability to read directory names from disk. Any word
processor worth its salt will let you view a list of files that match, say,
a *.DOC extension, and then select the one you want to edit. With the
introduction of BASIC PDS Microsoft added the DIR$ function, which lets you
read file names. However, there is no way to specify file attributes
(hidden, read-only, and so forth), and also no way to read directory names.
To add insult to injury, the PDS manuals do not show clearly how to read a
list of file names, and store them into a string array.
The program that follows counts the number of files or directories
that match a given specification, and then dimensions and loads a string
array with their names.
DEFINT A-Z
DECLARE SUB LoadNames (FileSpec$, Array$(), Attribute%)
'$INCLUDE: 'REGTYPE.BI'
TYPE DTA 'used by find first/next
Reserved AS STRING * 21 'reserved for use by DOS
Attribute AS STRING * 1 'the file's attribute
FileTime AS STRING * 2 'the file's time
FileDate AS STRING * 2 'the file's date
FileSize AS LONG 'the file's size
FileName AS STRING * 13 'the file's name
END TYPE
DIM SHARED DTAData AS DTA 'shared so LoadNames can
DIM SHARED Registers AS RegType ' access them too
DEF FnFileCount% (Spec$, Attribute)
STATIC Count 'make this private
Registers.DX = VARPTR(DTAData) 'set new DTA address
Registers.DS = -1 'the DTA is in DGROUP
Registers.AX = &H1A00 'specify service 1Ah
CALL DOSInt(Registers) 'DOS set DTA service
Count = 0 'clear the counter
Spec$ = Spec$ + CHR$(0) 'make an ASCIIZ string
IF Attribute AND 16 THEN 'find directory names?
DirFlag = -1 'yes
ELSE
DirFlag = 0 'no
END IF
Registers.DX = SADD(Spec$) 'the file spec address
Registers.DS = SSEG(Spec$) 'this is for BASIC PDS
'Registers.DS = -1 'this is for QuickBASIC
Registers.CX = Attribute 'assign the attribute
Registers.AX = &H4E00 'find first matching name
DO
CALL DOSInt(Registers) 'see if there's a match
IF Registers.Flags AND 1 THEN EXIT DO 'no more
IF DirFlag THEN
IF ASC(DTAData.Attribute) AND 16 THEN
IF LEFT$(DTAData.FileName, 1) <> "." THEN
Count = Count + 1 'increment the counter
END IF
END IF
ELSE
Count = Count + 1 'they want regular files
END IF
Registers.AX = &H4F00 'find next name
LOOP
FnFileCount% = Count 'assign the function
END DEF
REDIM Names$(1 TO 1) 'create a dynamic array
Attribute = 19 'matches directories only
Attribute = 39 'matches all files
INPUT "Enter a file specification: ", Spec$
CALL LoadNames(Spec$, Names$(), Attribute)
FOR X = LEN(Spec$) TO 1 STEP -1 'isolate the drive/path
Temp = ASC(MID$(Spec$, X, 1))
IF Temp = 58 OR Temp = 92 THEN '":" or "\"
Path$ = LEFT$(Spec$, X) 'keep what precedes that
EXIT FOR 'and we're all done
END IF
NEXT
FOR X = 1 TO UBOUND(Names$) 'print the names
PRINT Path$; Names$(X)
NEXT
PRINT
PRINT UBOUND(Names$); "matching file(s)"
END
SUB LoadNames (FileSpec$, Array$(), Attribute) STATIC
Spec$ = FileSpec$ + CHR$(0) 'make an ASCIIZ string
NumFiles = FnFileCount%(Spec$, Attribute) 'count names
IF NumFiles = 0 THEN EXIT SUB 'exit if none
REDIM Array$(1 TO NumFiles) 'dimension the array
IF Attribute AND 16 THEN 'find directory names?
DirFlag = -1 'yes
ELSE
DirFlag = 0 'no
END IF
'---- The following code isn't strictly necessary
' because we know that FnFileCount already set the
' DTA address.
'Registers.DX = VARPTR(DTAData) 'set new DTA address
'Registers.DS = -1 'the DTA in DGROUP
'Registers.AX = &H1A00 'specify service 1Ah
'CALL DOSInt(Registers) 'DOS set DTA service
Registers.DX = SADD(Spec$) 'the file spec address
Registers.DS = SSEG(Spec$) 'this is for BASIC PDS
'Registers.DS = -1 'this is for QuickBASIC
Registers.CX = Attribute 'assign the attribute
Registers.AX = &H4E00 'find first matching name
Count = 0 'clear the counter
DO
CALL DOSInt(Registers) 'see if there's a match
IF Registers.Flags AND 1 THEN EXIT DO 'no more
Valid = 0
IF DirFlag THEN 'directories?
IF ASC(DTAData.Attribute) AND 16 THEN
IF LEFT$(DTAData.FileName, 1) <> "." THEN
Valid = -1 'this name is valid
END IF
END IF
ELSE
Valid = -1 'they want regular files
END IF
IF Valid THEN 'process the file if it
Count = Count + 1 ' passed all the tests
Zero = INSTR(DTAData.FileName, CHR$(0))
Array$(Count) = LEFT$(DTAData.FileName, Zero - 1)
END IF
Registers.AX = &H4F00 'find next matching name
LOOP
END SUB
These routines call upon the DOS Find First and Find Next services, which
performs the actual searching and loading of the names. Before the names
can be loaded into an array, you need some way to know how many files there
are. Therefore, the FnFileCount function makes repeated calls to DOS to
find another file, until there are no more.
The general strategy is to request service &H4E to find the first
matching file. If a file is found then the Carry Flag is returned clear;
otherwise it is set and the function returns with a count of zero. If a
file is found Registers.AX is assigned a value of &H4F, and this tells DOS
to resume searching based on the same file specification as before. Where
the FnFileExist function merely needed to check for the presence of a file
using the Find First service, this one continues in a DO loop until no more
matching files are found.
Understand that these DOS services accept either a partial file
specification such as "*.BAS" or "D:\PATHNAME\*.*", or a single file name
such as "CONFIG.SYS" or "C:\AUTOEXEC.BAT".
File Attributes
The DOS Find services also accept--and require--a file attribute indicating
the type of files that are being sought. The method of specifying and
isolating files and their attributes is convoluted and confusing to be
sure. Figure 11-3 lists each of the six file attributes, and shows which
corresponds to each bit in the attribute byte.
7 6 5 4 3 2 1 0 <── Bits
128 64 32 16 8 4 2 1 <── Numeric Values
═══ ═══ ═══ ═══ ═══ ═══ ═══ ═══
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ └───── Read-Only
│ │ │ │ │ │ └───────── Hidden
│ │ │ │ │ └───────────── System
│ │ │ │ └───────────────── Volume Label
│ │ │ └───────────────────── Subdirectory
│ │ └───────────────────────── Archive
└───┴───────────────────────────── Unused
Figure 11-3: The makeup of the bits in the attribute byte, and the
individual decimal value of each.
In most cases, the attribute bits are cumulative. For example, if you
specify that you want to locate files marked as read-only, you will also
get files that are not. But if you leave that bit clear, then read-only
files will not be included. The same logic is used for reading directory
names. If the directory bit is set then you will read directories, and
also regular files whose directory bit is not set. This requires that you
perform additional qualifications when the file name is read into the DTA.
To make matters even worse, there is an exception to this rule whereby an
attribute of zero will still read file names whose archive bit is set.
Before considering how to qualify the names as they are read, you must
first understand what attributes are and how to specify them to begin with.
Every file has an attribute, which is set by DOS to Archive at the time it
is created. The archive bit is used solely to tell if the file has been
backed up using the DOS BACKUP utility. When BACKUP copies the file to a
floppy disk, it clears the Archive bit in the file's directory entry. Then
if the file is written to again later, DOS sets that bit. This way, BACKUP
can tell which files need to be backed up, and which ones haven't changed
since the last backup was performed. Most modern commercial backup
utilities also manipulate the archive bit, for the same reason that DOS'
BACKUP does.
The hidden bit tells the DOS DIR command not to display that file's
name. Although it won't display in a directory listing, a hidden file may
be opened, read from, and written to. The system bit is similar in that it
also tells DIR not to display the file. The IO.SYS and MSDOS.SYS files
that come with MS-DOS are hidden system files, so to read their names you
must set those bits in the search attribute. Note that IBM's version of
DOS uses the names IBMBIO.COM and IBMDOS.COM respectively for the same
files.
The label bit identifies a file as the disk's volume label, which
isn't really a file at all. Every disk is allowed to have one volume label
entry in its root directory, which lets an application identify the disk.
This feature is not particularly important with hard disks, but when
floppy-only systems were the norm this let programs ensure that the correct
data diskette was installed in the drive. Even though a volume label is
stored in the disk's directory like a regular file name, no sectors are
allocated to it. Note that a bug in DOS 2.x versions causes a search for a
volume label to fail. The only work-around is to use the more complex DOS
1.x Find First/Next services that are still supported in later versions for
compatibility with older programs.
Finally, the subdirectory attribute bit identifies a file as a
directory. From DOS' perspective a subdirectory *is* a file, with fixed-
length records that hold the names, attributes, and other information for
the files it contains. Notice that the "." and ".." directory entries that
appear when you type DIR are in fact present in that directory.
Every directory except the root contains these entries, and they also
have a directory attribute. The single dot refers to the current
directory, and the double dots to the parent directory one level above. I
mention this because these "dot" entries are reported by the Find First and
Find Next services, and in many cases you will want to filter them out.
To specify a file attribute you must determine the correct value,
based on the individual bits to be included in the search. As I stated
earlier, setting the attribute to zero includes all normal files, and
exclude any marked as read-only, hidden, system, or subdirectory.
Therefore, to include all files but not subdirectories you will use an
attribute value of 39. This value is derived by adding up the bit values
for each desired attribute as shown in Figure 11-3.
When you add all of the values for each bit of interest, the answer is
32 (archive) + 4 (system) + 2 (hidden) + 1 (read-only) = 39. In a similar
fashion, you will use 16 to read directory names, but hidden or read-only
directories will not be included unless you also add 2 + 1 = 3, resulting
in a final value of 19.
Although you can specify attribute bits in nearly any combination, DOS
returns all of the names that match any of the bits. Therefore, you must
further qualify the files by examining the attribute DOS returns in the DTA
TYPE variable. A typical search for directory names will ask to include
all three attribute bits (directory, hidden, and read-only), but the
qualification test merely tests if the directory bit is set. The following
excerpt shows this in context.
Registers.CX = 19
CALL DOSInt(Registers)
IF ASC(DTAData.Attribute) AND 16 THEN 'it is a directory
Even if the directory was in fact hidden or read-only, the test for the
directory bit will succeed regardless of any other bits that may be set.
Unfortunately, the reverse is not true. If the directory is not hidden or
read-only, then testing for those bits will fail. Both the FnFileCount
function and the LoadNames subprogram include an explicit test for
directory searches, and contain additional logic to check for this case.
You could also add similar logic to the FnFileExist function, or
create a separate version perhaps called FnDirExist that adds a test for
the directory bit and also filters out the "dot" entries.
REDIM PRESERVE
One glaring shortcoming you have probably already noticed is the enormous
amount of code that is duplicated in both the FnFileCount and LoadNames
routines. In fact, the two are almost identical, except that LoadNames
also assigns elements in the array. Worse, having to count all of the
names before they can be read greatly increases the amount of time needed
to process a directory when there are many files. Until you know how many
files are present, there's no way to known how large to dimension the
string array.
One solution is to create an array with, say, 500 elements, and hope
that the actual number of files does not exceed that. But if there are
only a few files this wastes a lot of memory, and when there are more than
500, then, well, you're still out of luck. In fact, this is one of the few
features that C offers but QuickBASIC does not. C programs can allocate
memory that will be treated as an array, and then repeatedly request more
memory for that same array as it is needed.
Fortunately, BASIC PDS version 7.1 includes the PRESERVE option to the
REDIM statement. This allows you to increase (or decrease) the size of an
array, but without destroying its current contents. Thus, REDIM PRESERVE
is ideal for applications like this that require an array's size to be
altered. The next, much shorter program uses REDIM PRESERVE to advantage,
and avoids the extra step that counts how many files match the search
specification. Of course, this program requires BASIC PDS.
DEFINT A-Z
DECLARE SUB LoadNames (FileSpec$, Array$(), Attribute%)
'$INCLUDE: 'REGTYPE.BI'
TYPE DTA 'used by find first/next
Reserved AS STRING * 21 'reserved for use by DOS
Attribute AS STRING * 1 'the file's attribute
FileTime AS STRING * 2 'the file's time
FileDate AS STRING * 2 'the file's date
FileSize AS LONG 'the file's size
FileName AS STRING * 13 'the file's name
END TYPE
DIM SHARED DTAData AS DTA 'shared so LoadNames can
DIM SHARED Registers AS RegType ' access them too
REDIM Names$(1 TO 1) 'create a dynamic array
Attribute = 19 'matches directories only
Attribute = 39 'matches all files
Spec$ = "*.*" 'so does this
CALL LoadNames(Spec$, Names$(), Attribute)
IF Names$(1) = "" THEN 'check for no files
PRINT "No matching files"
ELSE
FOR X = 1 TO UBOUND(Names$) 'print the names
PRINT Path$; Names$(X)
NEXT
END IF
END
SUB LoadNames (FileSpec$, Array$(), Attribute) STATIC
Spec$ = FileSpec$ + CHR$(0) 'make an ASCIIZ string
Count = 0 'clear the counter
Registers.DX = VARPTR(DTAData) 'set new DTA address
Registers.DS = -1 'the DTA is in DGROUP
Registers.AX = &H1A00 'specify service 1Ah
CALL DOSInt(Registers) 'DOS set DTA service
IF Attribute AND 16 THEN 'find directory names?
DirFlag = -1 'yes
ELSE
DirFlag = 0 'no
END IF
Registers.DX = SADD(Spec$) 'the file spec address
Registers.DS = SSEG(Spec$) 'this is for BASIC PDS
Registers.CX = Attribute 'assign the attribute
Registers.AX = &H4E00 'find first matching name
DO
CALL DOSInt(Registers) 'see if there's a match
IF Registers.Flags AND 1 THEN EXIT DO 'no more
Valid = 0 'invalid until qualified
IF DirFlag THEN 'find directories?
IF ASC(DTAData.Attribute) AND 16 THEN 'yes, is it?
IF LEFT$(DTAData.FileName, 1) <> "." THEN
Valid = -1 'this name is valid
END IF
END IF
ELSE
Valid = -1 'they want regular files
END IF
IF Valid THEN 'process the file if it
Count = Count + 1 ' passed all the tests
REDIM PRESERVE Array$(1 TO Count) 'expand the array
Zero = INSTR(DTAData.FileName, CHR$(0)) 'find zero
Array$(Count) = LEFT$(DTAData.FileName, Zero - 1)
END IF
Registers.AX = &H4F00 'find next matching name
LOOP
END SUB
MANAGING FILES
Chapter 6 explained in great detail how files are opened, closed, read, and
written using BASIC. I mentioned there that BASIC imposes a number of
arbitrary limitations on what you can and cannot do with files. Indeed,
DOS allows almost any action except writing to a file that has been opened
for input. As you can imagine, CALL Interrupt--or in this case the DOSInt
replacement routine--can be used to circumvent BASIC and access your files
directly.
Although BASIC expects you to state how the file will be accessed with
the various OPEN options, to DOS all files are considered as being opened
for binary access. There is no equivalent DOS service for BASIC's INPUT #
or PRINT # commands. Therefore, it is up to you to write subroutines that
look for a terminating carriage return and optional line feed when reading
sequential text. Likewise, it is up to you to manually append a carriage
return and line feed to the end of each line of text written to disk.
Frankly, sequential file access is often best left to BASIC, since a
lot of time-consuming tests are needed when reading sequential data. You
could, however, use the BufIn function shown in Chapter 6, or similar logic
of your own devising. There are many types of file access that can be
performed using direct DOS calls, and I will show those that are the most
useful and appropriate here.
The program that will follow shortly is a combination demonstration,
and suite of twelve subprograms and functions that perform most of the
services necessary for manipulating files. Subprograms are provided to
replace BASIC's OPEN, CLOSE, GET, and PUT statements, as well as LOCK and
UNLOCK, SEEK, and KILL.
There are also replacement functions for LOC and LOF, as well as two
additional subprograms that have no BASIC equivalent. All of the routines
use the DOSInt interface routine, and avoid using BASIC's file handling
statements. The demonstration is comprised of a series of code blocks that
exercise each routine showing how it is used. Comments at the start of
each block explain what is being demonstrated.
One reason to go behind BASIC's back this way is to avoid its many
restrictions. For example, BASIC will not let you read from a file that
has been opened for output, even though DOS considers this to be perfectly
legal. Another is to avoid the need for ON ERROR. As you learned in
Chapter 3, ON ERROR can make a program run more slowly, and also increase
its size. By going directly to DOS you can avoid the burden of ON ERROR,
which is otherwise needed to prevent your program from terminating if an
error occurs. These replacement routines avoid errors such as those caused
by attempting to open a file that does not exist, or trying to lock a
network file that has already been locked by someone else.
As with some of the other programs in this book that combine a
demonstration and subroutines, you should make a copy of the file, and then
delete all of the code in the main portion of the program. The only lines
that must not be deleted are the DEFINT, DECLARE, and INCLUDE statements,
and also the two DIM SHARED statements. Then, you can load the resultant
module into the BASIC editor along with your own main application.
'DOS.BAS, demonstrates the direct DOS access routines
DEFINT A-Z
DECLARE FUNCTION DOSError% ()
DECLARE FUNCTION ErrMessage$ (ErrNumber)
DECLARE FUNCTION LocFile& (Handle)
DECLARE FUNCTION LofFile& (Handle)
DECLARE FUNCTION PeekWord% (BYVAL Segment, BYVAL Address)
DECLARE SUB ClipFile (Handle, NewLength&)
DECLARE SUB CloseFile (Handle)
DECLARE SUB FlushFile (Handle)
DECLARE SUB KillFile (FileName$)
DECLARE SUB LockFile (Handle, Location&, NumBytes&, Action)
DECLARE SUB OpenFile (FileName$, OpenMethod, Handle)
DECLARE SUB ReadFile (Handle, Segment, Address, NumBytes)
DECLARE SUB SeekFile (Handle, Location&, SeekMethod)
DECLARE SUB WriteFile (Handle, Segment, Address, NumBytes)
'$INCLUDE: 'REGTYPE.BI'
DIM SHARED Registers AS RegType 'so all can access it
DIM SHARED ErrCode 'ditto for the ErrCode
CRLF$ = CHR$(13) + CHR$(10) 'define this once now
COLOR 15, 1 'this makes the DOS
CLS 'messages high-intensity
COLOR 7, 1
'---- Open the test file we will use.
FileName$ = "C:\MYFILE.DAT" 'specify the file name
OpenMethod = 2 'read/write non-shared
CALL OpenFile(FileName$, OpenMethod, Handle)
GOSUB HandleErr
PRINT FileName$; " successfully opened, handle:"; Handle
'---- Write a test message string to the file.
Msg$ = "This is a test message." + CRLF$
Segment = SSEG(Msg$) 'use this with BASIC PDS
'Segment = VARSEG(Msg$) 'use this with QuickBASIC
Address = SADD(Msg$)
NumBytes = LEN(Msg$)
CALL WriteFile(Handle, Segment, Address, NumBytes)
GOSUB HandleErr
PRINT "The test message was successfully written."
'---- Show how to write a numeric value.
IntData = 1234
Segment = VARSEG(IntData)
Address = VARPTR(IntData)
NumBytes = 2
CALL WriteFile(Handle, Segment, Address, NumBytes)
GOSUB HandleErr
PRINT "The integer variable was successfully written."
'---- See how large the file is now.
Length& = LofFile&(Handle)
GOSUB HandleErr
PRINT "The file is now"; Length&; "bytes long."
'---- Seek back to the beginning of the file.
Location& = 1 'specify file offset 1
SeekMethod = 0 'relative to beginning
CALL SeekFile(Handle, Location&, SeekMethod)
GOSUB HandleErr
PRINT "We successfully seeked back to the beginning."
'---- Ensure that the Seek worked by seeing where we are.
CurSeek& = LocFile&(Handle)
GOSUB HandleErr
PRINT "The DOS file pointer is now at location"; CurSeek&
'---- Read the test message back in again.
Buffer$ = SPACE$(23) 'the length of Msg$
Segment = SSEG(Buffer$) 'use this with BASIC PDS
'Segment = VARSEG(Buffer$) 'use this with QuickBASIC
Address = SADD(Buffer$)
NumBytes = LEN(Buffer$)
CALL ReadFile(Handle, Segment, Address, NumBytes)
GOSUB HandleErr
PRINT "Here is the test message: "; Buffer$
'---- Skip over the CRLF by reading it as an integer.
Address = VARPTR(Temp) 'read the CRLF into Temp
Segment = VARSEG(Temp)
NumBytes = 2
CALL ReadFile(Handle, Segment, Address, NumBytes)
GOSUB HandleErr
'---- Read the integer written earlier, also into Temp.
Address = VARPTR(Temp)
Segment = VARSEG(Temp)
NumBytes = 2
CALL ReadFile(Handle, Segment, Address, NumBytes)
GOSUB HandleErr
PRINT "The integer value just read is:"; Temp
'---- Append a new string at the end of the file.
Msg$ = "This is appended to the end of the file." + CRLF$
Segment = SSEG(Msg$) 'use this with BASIC PDS
'Segment = VARSEG(Msg$) 'use this with QuickBASIC
Address = SADD(Msg$)
NumBytes = LEN(Msg$)
CALL WriteFile(Handle, Segment, Address, NumBytes)
GOSUB HandleErr
PRINT "The appended message has been written, ";
PRINT "but it's still in the DOS file buffer."
'---- Flush the file's DOS buffer to disk.
CALL FlushFile(Handle)
GOSUB HandleErr
PRINT "Now the buffer has been flushed to disk. ";
PRINT "Here's the file contents:"
SHELL "TYPE " + FileName$
'---- Display the current length of the file again.
PRINT "Before calling ClipFile the file is now";
Length& = LofFile&(Handle)
GOSUB HandleErr
PRINT Length&; "bytes long."
'---- Clip the file to be 2 bytes shorter.
NewLength& = LofFile&(Handle) - 2
CALL ClipFile(Handle, NewLength&)
PRINT "The file has been clipped successfully. ";
'---- Prove that the clipping worked successfully.
Length& = LofFile&(Handle)
GOSUB HandleErr
PRINT "It is now"; Length&; "bytes long."
'---- Close the file.
CALL CloseFile(Handle)
GOSUB HandleErr
PRINT "The file was successfully closed."
'---- Open the file again, this time for shared access.
OpenMethod = 66 'full sharing, read/write
CALL OpenFile(FileName$, OpenMethod, Handle)
GOSUB HandleErr
PRINT FileName$; " successfully opened in shared mode";
PRINT ", handle:"; Handle
'---- Lock bytes 50 through 59.
Start& = 50
Length& = 10
Action = 0 'specify locking
CALL LockFile(Handle, Start&, Length&, Action)
GOSUB HandleErr
PRINT "File bytes 50 through 59 are successfully locked."
'---- Prove that it is locked by asking DOS to copy it.
PRINT "DOS (another process) fails to access the file:"
SHELL "COPY " + FileName$ + " NUL"
'---- Unlock the same range of bytes (mandatory).
Start& = 50
Length& = 10
Action = 1 'specify unlocking
CALL LockFile(Handle, Start&, Length&, Action)
GOSUB HandleErr
PRINT "File bytes 50 through 59 successfully unlocked."
'---- Prove the unlocking worked by having DOS copy it.
PRINT "Once unlocked DOS can access the file:";
SHELL "COPY " + FileName$ + " NUL"
CloseIt:
'---- Close the file
CALL CloseFile(Handle)
GOSUB HandleErr
PRINT "The file was successfully closed, ";
'---- Kill the file to be polite
CALL KillFile(FileName$)
GOSUB HandleErr
PRINT "and then successfully deleted."
END
'=======================================
' Error handler
'=======================================
HandleErr:
TempErr = DOSError% 'call DOSError% just once
IF TempErr = 0 THEN RETURN 'return if no errors
PRINT ErrMessage$(TempErr) 'else print the message
IF TempErr = 1 THEN 'we failed trying to lock
COLOR 7 + 16
PRINT "SHARE must be installed to continue."
COLOR 7
RETURN CloseIt
ELSE 'otherwise end
END
END IF
SUB ClipFile (Handle, Length&) STATIC
'-- Use SeekFile to seek there, and then call WriteFile
' specifying zero bytes to truncate it at that point.
' Length& + 1 is needed because we need to seek just
' PAST the point where the file is to be truncated.
CALL SeekFile(Handle, Length& + 1, Zero)
IF ErrCode THEN EXIT SUB 'exit if an error occurred
CALL WriteFile(Handle, Dummy, Dummy, Zero)
END SUB
SUB CloseFile (Handle) STATIC
ErrCode = 0 'assume no errors
Registers.AX = &H3E00 'close file service
Registers.BX = Handle 'using this handle
CALL DOSInt(Registers)
IF Registers.Flags AND 1 THEN ErrCode = Registers.AX
END SUB
FUNCTION DOSError%
DOSError% = ErrCode 'simply return the error
END FUNCTION
FUNCTION ErrMessage$ (ErrNumber) STATIC
SELECT CASE ErrNumber
CASE 2
ErrMessage$ = "File not found"
CASE 3
ErrMessage$ = "Path not found"
CASE 4
ErrMessage$ = "Too many files"
CASE 5
ErrMessage$ = "Access denied"
CASE 6
ErrMessage$ = "Invalid handle"
CASE 61
ErrMessage$ = "Disk full"
CASE ELSE
ErrMessage$ = "Undefined error: " + STR$(ErrNumber)
END SELECT
END FUNCTION
SUB FlushFile (Handle) STATIC
ErrCode = 0 'assume no errors
Registers.AX = &H4500 'create duplicate handle
Registers.BX = Handle 'based on this handle
CALL DOSInt(Registers)
IF Registers.Flags AND 1 THEN 'an error, assign it
ErrCode = Registers.AX
ELSE 'no error, so closing the
TempHandle = Registers.AX 'dupe flushes the data
CALL CloseFile(TempHandle)
END IF
END SUB
SUB KillFile (FileName$) STATIC
ErrCode = 0 'assume no errors
LocalName$ = FileName$ + CHR$(0) 'make an ASCIIZ string
Registers.AX = &H4100 'delete file service
Registers.DX = SADD(LocalName$) 'using this handle
Registers.DS = SSEG(LocalName$) 'use this with PDS
'Registers.DS = -1 'use this with QB
CALL DOSInt(Registers)
IF Registers.Flags AND 1 THEN ErrCode = Registers.AX
END SUB
FUNCTION LocFile& (Handle) STATIC
ErrCode = 0 'assume no errors
Registers.AX = &H4201 'seek to where we are now
Registers.BX = Handle 'using this handle
Registers.CX = 0 'move zero bytes from here
Registers.DX = 0
CALL DOSInt(Registers)
IF Registers.Flags AND 1 THEN 'an error occurred
ErrCode = Registers.AX
ELSE 'adjust to one-based
LocFile& = (Registers.AX + (65536 * Registers.DX)) + 1
END IF
END FUNCTION
SUB LockFile (Handle, Location&, NumBytes&, Action) STATIC
ErrCode = 0 'assume no errors
LocalLoc& = Location& - 1 'adjust to zero-based
Registers.AX = Action + (256 * &H5C) 'lock/unlock
Registers.BX = Handle
Registers.CX = PeekWord%(VARSEG(LocalLoc&), VARPTR(LocalLoc&) + 2)
Registers.DX = PeekWord%(VARSEG(LocalLoc&), VARPTR(LocalLoc&))
Registers.SI = PeekWord%(VARSEG(NumBytes&), VARPTR(NumBytes&) + 2)
Registers.DI = PeekWord%(VARSEG(NumBytes&), VARPTR(NumBytes&))
CALL DOSInt(Registers)
IF Registers.Flags AND 1 THEN ErrCode = Registers.AX
END SUB
FUNCTION LofFile& (Handle)
'---- first get and save the current file location
CurLoc& = LocFile&(Handle) 'LocFile also clears ErrCode
IF ErrCode THEN EXIT FUNCTION
Registers.AX = &H4202 'seek to the end of the file
Registers.BX = Handle 'using this handle
Registers.CX = 0 'move zero bytes from there
Registers.DX = 0
CALL DOSInt(Registers)
IF Registers.Flags AND 1 THEN 'an error occurred
ErrCode = Registers.AX
EXIT FUNCTION
ELSE 'assign where we are
LofFile& = Registers.AX + (65536 * Registers.DX)
END IF
Registers.AX = &H4200 'seek to where we were before
Registers.BX = Handle 'using this handle
Registers.CX = PeekWord%(VARSEG(CurLoc&), VARPTR(CurLoc&) + 2)
Registers.DX = PeekWord%(VARSEG(CurLoc&), VARPTR(CurLoc&))
CALL DOSInt(Registers)
IF Registers.Flags AND 1 THEN ErrCode = Registers.AX
END FUNCTION
SUB OpenFile (FileName$, Method, Handle) STATIC
ErrCode = 0 'assume no errors
Registers.AX = Method + (256 * &H3D) 'open file service
LocalName$ = FileName$ + CHR$(0) 'make an ASCIIZ string
DO
Registers.DX = SADD(LocalName$) 'point to the name
Registers.DS = SSEG(LocalName$) 'use this with PDS
'Registers.DS = -1 'use this w/QuickBASIC
CALL DOSInt(Registers) 'call DOS
IF (Registers.Flags AND 1) = 0 THEN 'no errors
Handle = Registers.AX 'assign the handle
EXIT SUB 'and we're all done
END IF
IF Registers.AX = 2 THEN 'File not found error
Registers.AX = &H3C00 'so create it!
ELSE
ErrCode = Registers.AX 'read the code from AX
EXIT SUB
END IF
LOOP
END SUB
SUB ReadFile (Handle, Segment, Address, NumBytes) STATIC
ErrCode = 0 'assume no errors
Registers.AX = &H3F00 'read from file service
Registers.BX = Handle 'using this handle
Registers.CX = NumBytes 'and this many bytes
Registers.DX = Address 'read to this address
Registers.DS = Segment 'and this segment
CALL DOSInt(Registers)
IF Registers.Flags AND 1 THEN ErrCode = Registers.AX
END SUB
SUB SeekFile (Handle, Location&, Method) STATIC
ErrCode = 0 'assume no errors
LocalLoc& = Location& - 1 'adjust to zero-based
Registers.AX = Method + (256 * &H42)
Registers.BX = Handle
Registers.CX = PeekWord%(VARSEG(LocalLoc&), VARPTR(LocalLoc&) + 2)
Registers.DX = PeekWord%(VARSEG(LocalLoc&), VARPTR(LocalLoc&))
CALL DOSInt(Registers)
IF Registers.Flags AND 1 THEN ErrCode = Registers.AX
END SUB
SUB WriteFile (Handle, Segment, Address, NumBytes) STATIC
ErrCode = 0 'assume no errors
Registers.AX = &H4000
Registers.BX = Handle
Registers.CX = NumBytes
Registers.DX = Address
Registers.DS = Segment
CALL DOSInt(Registers)
IF Registers.Flags AND 1 THEN
ErrCode = Registers.AX
ELSEIF Registers.AX <> Registers.CX THEN
ErrCode = 61
END IF
END SUB
This program begins by dimensioning two variables as SHARED throughout the
entire module. By establishing the Registers TYPE variable as SHARED, all
of the routines can use the same portion of DGROUP memory. If a separate
DIM statement were used within each procedure, that many copies of this 20-
byte variable would reside in memory at once. The CRLF$ variable does not
need to be shared, because it is used only by the demonstration portion of
the program.
Before I describe each of these routines and how they are used, it is
important to explain how DOS uses file handles. BASIC is unique among
languages in that it allows you to make up an arbitrary file number that is
used to access the files. With most languages and operating systems--and
DOS is no exception--it is the operating system that assigns a number which
your program must remember. Therefore, when you call the OpenFile routine
to open a file, the Handle parameter is returned to you and you will use
that number for subsequent file operations.
Another important point is how errors are handled by these routines.
Since you do not use ON ERROR to trap those situations another method is
needed. Each routine clears or sets a global SHARED variable named
ErrCode, which indicates its success or failure. After each call to one of
these routines you will then check this variable, to see if it was
successful. For the most efficiency, this program invokes a central error
checking GOSUB routine that performs the actual testing. If an error
occurs this routine prints an appropriate message using the ErrMessage$
function, and then ends. The DOSError function is provided to allow access
to ErrCode from other modules.
In practice, it is not strictly necessary to add an explicit test
after each subroutine call. For example, if you know the file has been
opened successfully and you are sure the disk drive has sufficient space,
then it is probably safe to assume that subsequent file writes will be
okay. However, if you do call a routine that causes an error and don't
check for that error, the next successful call to another routine will
clear ErrCode and you will have no way to know about the earlier error.
Opening a File
The demonstration begins by first assigning a file name and open method,
and then calling OpenFile to open the file. The open method lets you
indicate the file access mode (reading, writing, or both), and also if the
file will be accessed on a network. This parameter is bit-coded, and each
bit has a parallel equivalent in BASIC's ACCESS READ, WRITE, SHARED, LOCK
READ, and LOCK WRITE options. Figure 11-4 shows how these bits are
organized.
7 6 5 4 3 2 1 0 <── Bits
n/a 64 32 16 n/a 4 2 1 <── Numeric Values
═══ ═══ ═══ ═══ ═══ ═══ ═══ ═══
│ │ │ │ │ │ │ │
│ │ │ │ │ └───┴───┴───── Access Mode
│ │ │ │ └───────────────── Reserved
│ └───┴───┴───────────────────── Sharing Mode
└───────────────────────────────── Inheritance
Figure 11-4: The organization of the bits that establish how a file is to
be opened.
As with the file attribute bits shown earlier in Figure 11-3, you also need
to set bits individually here to fully control the various file permission
privileges. The access mode bits are valid with DOS versions 2.0 or later,
and are equivalent to BASIC's ACCESS arguments. The sharing mode bits
require DOS 3.0 or later, and also require SHARE.EXE to be installed. Note
that some network software does not explicitly require SHARE, and provides
the same functionality as part of its normal operation.
The three lower bits control the file access, using the following
binary code: 000 establishes read-only access, 001 allows writing only, and
010 allows both reading and writing. The term access as used here means
what actions *your* program can perform, and has nothing to do with network
or file sharing privileges.
File sharing privileges are controlled by the three bits in the upper
nybble (half-byte), and these determine what actions may be performed by
other programs while your file is open. Regardless of what sharing (or
locking) options you choose, your program always has full permission to
access the file. The share bits are organized as follows: 000 means
sharing is disabled, and this is what you must specify if you are not
running on a network or when DOS 2.x is installed. A code of 001 denies
other programs access to either read from or write to the file, 010 allows
other programs to read but not write, and 011 allows writing but not
reading. A code of 100 indicates full sharing, which lets other programs
read and write, as long as that part of the file is not locked explicitly.
Again, these codes are presented as binary values, and it is up to you
to determine the correct value based on the settings of the individual
bits. This is not as hard as it may sound at first, because you simply add
up the bit values shown in the table. For example, to open a file for non-
network read/write access under any version of DOS you use 000 + 010 = 2,
which is the value used in the first OPEN example. To open a file for
reading and writing and also allow other applications to access it fully
you instead use 100 + 010 = 64 + 2 = 66. This is shown in the second OPEN
statement. Figure 11-5 lists a few of the possible bit combinations, with
the equivalent BASIC OPEN options.
BASIC OPEN Statement Bits Value
================================= ======== =====
OPEN FOR BINARY 00000010 2
OPEN FOR BINARY ACCESS READ 00000000 0
OPEN FOR BINARY ACCESS WRITE 00000001 1
OPEN FOR BINARY ACCESS READ WRITE 00000010 2
OPEN FOR BINARY ACCESS READ SHARED 01000000 64
OPEN FOR BINARY LOCK READ 00110010 50
OPEN FOR BINARY LOCK WRITE 00100010 34
Figure 11-5: Bit equivalents for some of BASIC's OPEN options.
Reading and Writing
Once the file has been opened successfully, the next step is to show how to
write a string variable in the same way BASIC does when you use PRINT #.
The WriteFile and ReadFile routines each expect four arguments: the DOS
file handle, the segment and address to save from or read into, and the
number of bytes. These are the same parameters that DOS expects, and you
can see by examining the subprograms that they merely pass this information
on to DOS.
Just before the first call to WriteFile, Msg$ is assigned a short test
string, and a carriage return and line feed are appended to it manually.
Remember, when you use BASIC's PRINT # command it is BASIC that adds these
bytes for you. When dealing with DOS directly it is up to you to append
these characters. Of course, you would omit these to mimic appending a
semicolon at the end of a BASIC print line:
PRINT #1, Msg$;
SSEG then determines where the string data segment is, and SADD reports its
address within that segment. The QuickBASIC version is shown as a comment,
and it uses VARSEG instead. The number of bytes is obtained using LEN, and
DOS accepts any value up to 65535. It is imperative that you never pass a
value of zero for the number of bytes, or DOS will truncate the file at the
current seek location. I will discuss this in more detail later on, in the
section entitled *Beyond BASIC's File Handling*.
The next example that writes an integer variable to the file is
similar, except it uses a fixed length of 2. BASIC will not let you pass
different types of data to one subprogram or function, which is why these
read and write routines are designed to accept a segment and address.
ReadFile is not called until later in the demonstration; however, it
is nearly identical to WriteFile. Because you must tell ReadFile how many
bytes are to be read, you should establish some type of system. One good
one is the method used by Lotus and described in Chapter 6. For programs
that do not need such a heavy-handed approach or that write only strings,
you could use a simpler technique. For example, each string could be
preceded by an integer length word, and that word would be read prior to
reading each string. The short code fragment that follows shows how this
might work.
Segment = VARSEG(Length) 'Length is what gets read first
Address = VARPTR(Length)
CALL ReadFile(Handle, Segment, Address, 2)
Work$ = SPACE$(Length) 'make a string that long
Segment = SSEG(Work$) 'then read Length bytes into the string
Address = SADD(Work$)
CALL ReadFile(Handle, Segment, Address, Length)
Setting and Reading the DOS Seek Location
The LocFile and LofFile functions are similar to their BASIC LOC and LOF
counterparts, except that LocFile is really equivalent to the SEEK
function. Chapter 6 described the difference between the LOC and SEEK
functions, and came to the inescapable conclusion that LOC is not nearly as
useful as SEEK in most situations.
The SeekFile subprogram, on the other hand, is equivalent to the
statement form of BASIC's SEEK, and offers an interesting twist as an
enhancement. Where BASIC's SEEK statement expects an offset from the
beginning of the file, DOS provides additional seek methods. One lets you
seek relative to where you are now in the file, and the other is relative
to the end of the file. Therefore, I have included a SeekMethod parameter
with my version of SeekFile, letting you enjoy the same flexibility.
If SeekMethod is set to zero, DOS behaves the same as BASIC does and
bases the new seek location from the beginning of the file. If SeekMethod
is instead assigned to 1, the new offset into the file will be based on the
current location. Note that you may use both positive *and* negative seek
values, to move forward and backwards respectively. Finally, using a
SeekMethod value of 2 tells DOS to consider the new location as being
relative to the end of the file.
For this method you may also use either a positive or negative value,
to go beyond the end of the file or some offset before the end. While
there is nothing inherently wrong with seeking past the end of a file, if
any data is written at that point DOS will make that the new file length.
And as explained in Chapter 6, the portion of the file that lies between
the previous end of the file and the current end will hold whatever junk
happened to be in the sectors that were just assigned to extend the length.
One slight complication arises if you are dealing with fixed-length
record data: you must calculate the appropriate file offset manually. The
short one-line DEF FN function below shows how to do this.
DEF FNSeekLoc&(RecNumber, RecLen) = ((RecNumber - 1) * CLNG(RecLen)) + 1
Locking a File
The LockFile subprogram serves the same purpose as BASIC's LOCK and UNLOCK
statements. Because the code to lock and unlock a file are identical
except for a single instruction, it seemed reasonable to combine the two
services into one routine. LockFile expects four arguments: a handle, a
starting offset, the number of bytes, and an action code. The starting
offset and number of bytes use long integer values, to accommodate large
files.
Because DOS's Lock and Unlock services require you to specify the
range of bytes to be locked, additional effort may be needed on your part.
For example, if you are manipulating fixed-length records it is up to you
to translate record numbers and record ranges to an equivalent binary
offset and number of bytes. Fortunately, these values are very easy to
determine using the following formulas:
Location& = (RecNumber - 1) * CLNG(RecLength)
NumBytes& = RecLength * CLNG(NumRecords)
Note how CLNG is necessary to prevent BASIC from creating an overflow error
if the result of the multiplications exceeds 32767.
LockFile can also be used with normal BASIC file handling statements,
if you merely want to avoid an error from attempting to lock a file that is
already locked by another process. This requires you to use BASIC's
FILEATTR function to obtain the equivalent DOS handle, thus:
Handle = FILEATTR(FileNumber, 2)
Here, FileNumber is the BASIC file number that was specified when the file
was first opened. For example, if you used this:
OPEN FileName$ FOR RANDOM SHARED AS #4 LEN = RecLength
then the correct value for FileNumber will be 4.
Beyond BASIC's File Handling
Aside from SeekFile's ability to use the end of a file or the current seek
location as a base point, the routines presented so far merely mimic the
same capabilities BASIC already provides. Two notable exceptions, however,
are ClipFile and FlushFile.
The ClipFile subprogram lets you set a new length for a file, and that
length may be either longer or shorter than the current length. ClipFile
takes advantage of a little-known DOS feature that sets a new length for a
file when you tell it to write zero bytes. This technique was used in the
DBPACK.BAS program from Chapter 7, and it let that program remove deleted
records from the end of a dBASE file.
ClipFile begins by calling SeekFile to move the DOS file pointer just
past the new length specified. If no error occurred it then calls
WriteFile to write zero bytes at that point, thus establishing the new
length. Notice the way the undefined variable Zero is used rather than a
literal constant 0. As you already learned in Chapter 2, when a constant
is passed to a subprogram or function, BASIC creates code to store a copy
of the constant in DGROUP, and then passes the address of that copy.
Although the variable Zero also requires two bytes of DGROUP memory for
storage, the code to explicitly place the value there is avoided. Since an
unassigned variable is always zero this method can be used with confidence.
FlushFile also provides an important service that BASIC does not.
When data is written to disk using either BASIC or DOS via direct interrupt
calls, the last portion that was written is not necessarily on the physical
disk. DOS buffers all file writes to minimize the number of disk accesses
needed, thereby improving the speed of those writes. BASIC performs
additional buffering as well, which further improves your program's
performance. However, this creates a potential problem because a power
outage or other disaster will cause any data in the file buffer to be lost.
FlushFile calls upon another little-known DOS service called Duplicate
Handle. When this service is called with the handle of a file that is
already open, DOS creates a duplicate handle for the same file. This
service is not that useful in and of itself, except for one important
exception: When the duplicate handle is subsequently closed, DOS also
writes the original file's contents to disk and updates the directory entry
to reflect the current length. This is exactly what FlushFile does to
flush the file buffer to disk.
Error Messages
The ErrMessage$ function is designed to display an appropriate message if
an error occurs while using these routines. DOS has fewer error codes than
BASIC, and it also uses a completely different numbering system. The
ErrMessage$ function returns an error message that is equivalent to BASIC's
where possible, but based on the DOS error return codes.
Potential Problems
Although this collection of file handling routines offers many improvements
over using equivalent BASIC statements, there is one important issue I have
not addressed here: handling critical errors. A critical error is caused
by attempting to access a floppy disk drive with the drive door open, or no
disk in place. At the DOS command line critical errors result in the
infamous "Abort, Retry, Fail" message.
Handling critical errors requires pure assembly language, and is a
fairly complex undertaking. Therefore, I have purposely omitted that
functionality from these routines. However, add-on library products such
as QuickPak Professional and P.D.Q. from Crescent Software are written in
assembly language, and include critical error handling.
There is another potential problem you must be aware of when using
these routines. When you open a file using BASIC's OPEN statement, and
then restart the program before the file has been closed, BASIC closes the
file before running your program again. This is done automatically and
without your knowing about it.
If you call OpenFile to open a file and then restart the program, the
original file remains open. This causes no harm by itself--your program
will simply receive the next available handle when it calls OpenFile. But
at some point you will surely exhaust the available handles. The problem
is that you will not be able to save your program, because the BASIC editor
needs a handle when writing your source code to disk.
The solution is to press F6 to go to the Immediate window, and then
type the following line:
FOR X% = 5 TO 20: CALL CloseFile(X%): NEXT
This closes all of the files your program opened, thus freeing them for use
by the BASIC editor. It is essential that you never close DOS handles zero
through four, because they are in use by the PC. Since DOS uses these
handles itself to print to the screen and read keyboard input, closing
those handles will effectively lock up your PC. [Also, it is okay to close
handles 5 through 20, even if your program hasn't opened that many. That
is, asking DOS to close a file handle that was never opened does no harm.]
ACCESSING THE MOUSE
===================
All of the DOS and BIOS system services we have looked at so far rely on
either the Interrupt routine that comes with BASIC, or the simplified
DOSInt replacement. In a similar fashion, accessing the mouse driver also
requires you to call interrupts. All of the mouse services are invoked
using Interrupt &H33, and like DOS and the BIOS they require you to load
the processor's registers to pass information, and then read them again
afterward to obtain the results.
In this section I will present several useful subroutines that show
how to access the mouse interrupt. The first portion discusses the various
utility routines, and shows how they are used. Following that, I will
explain how the routines actually work and interface with the mouse driver.
MOUSE SERVICES
The important mouse services provided here are those that turn the mouse
cursor on and off, position it on the screen and control its color, and let
you determine which buttons are being pressed and where the cursor is
presently located. Other routines show how to restrict the range of the
mouse cursor's travel, and show how to define new, custom cursor shapes.
To reduce the size of your programs I have written a short assembly
language subroutine called MouseInt. This is similar to the DOSInt routine
introduced in Chapter 6, except it is intended for use with the mouse
interrupt &H33.
;MOUSEINT.ASM
.Model Medium, Basic
MouseRegs Struc
RegAX DW ?
RegBX DW ?
RegCX DW ?
RegDX DW ?
Segmnt DW ?
MouseRegs Ends
.Code
MouseInt Proc Uses SI DS ES, MRegs:Word
Mov SI,MRegs ;get the address of MouseRegs
Mov AX,[SI+RegAX] ;load each register in turn
Mov BX,[SI+RegBX]
Mov CX,[SI+RegCX]
Mov DX,[SI+RegDX]
Mov SI,[SI+Segmnt] ;see what the segment is
Or SI,SI ;is it zero?
Jz @F ;yes, skip ahead and use default
Cmp SI,-1 ;is it -1?
Je @F ;yes, skip ahead
Mov DS,SI ;no, use the segment specified
@@:
Push DS ;either way, assign ES=DS
Pop ES
Int 33h ;call the mouse driver
Push SS ;regain access to MouseRegs
Pop DS
Mov SI,MRegs ;access MouseRegs again
Mov [SI+RegAX],AX ;save each register in turn
Mov [SI+RegBX],BX
Mov [SI+RegCX],CX
Mov [SI+RegDX],DX
Ret ;return to BASIC
MouseInt Endp
End
Like DOSInt, this routine also uses a TYPE variable to define the various
CPU registers that are needed by the mouse driver. However, fewer
registers are needed simplifying the TYPE structure. You should define
this TYPE variable as follows:
TYPE MouseType
AX AS INTEGER
BX AS INTEGER
CX AS INTEGER
DX AS INTEGER
Segment AS INTEGER
END TYPE
DIM MouseRegs AS MouseTYPE
Since the mouse driver uses only these few registers, you can save a few
bytes of DGROUP memory by using this subset TYPE instead of the full
Registers TYPE that DOSInt requires. Notice the last component called
Segment. Unlike the Mouse routine that Microsoft sells as an add-on
library, MouseInt lets you specify a segment for passing far data to the
mouse interrupt handler. For most mouse services you can leave the segment
set to zero or -1. Either value tells MouseInt to use BASIC's default data
segment. But some services that accept the address of incoming data also
need to know the data's segment.
In the Microsoft version you have no choice but to use static data and
near memory arrays. Obviously, this precludes being able to use BASIC PDS
far strings with that interface routine. You would instead have to create
a single fixed-length string or TYPE variable, just to force the data to
reside in near memory. When calling MouseInt with a value other than zero
or -1 for the segment, MouseInt loads both DS and ES with that value.
As with the collection of DOS file access routines, the following
subprograms and functions can be added as a module to your program. Again,
you should first make a copy of the source file that is included on the
accompanying floppy disk, and then delete the demonstration portion of the
program. This way, you can also run the original demonstration, and trace
through it to test each of the mouse services. Of course, be sure to leave
the commands that dimension the MouseRegs and MousePresent variables as
being shared, and also the relevant DECLARE and DEFINT statements.
'MOUSE.BAS, demonstrates the various mouse services
DEFINT A-Z
'---- assembly language functions and subroutines
DECLARE FUNCTION PeekWord% (BYVAL Segment, BYVAL Address)
DECLARE SUB MouseInt (MouseRegs AS ANY)
'---- BASIC functions and subprograms
DECLARE FUNCTION Bin2Hex% (Binary$)
DECLARE FUNCTION MouseThere% ()
DECLARE FUNCTION WaitButton% ()
DECLARE SUB CursorShape (HotX, HotY, Shape())
DECLARE SUB HideCursor ()
DECLARE SUB MouseTrap (ULRow, ULCol, LRRow, LRCol)
DECLARE SUB MoveCursor (X, Y)
DECLARE SUB ReadCursor (X, Y, Buttons)
DECLARE SUB ShowCursor ()
DECLARE SUB TextCursor (FG, BG)
DECLARE SUB Prompt (Message$) 'used for this demo only
TYPE MouseType 'similar to DOS RegType
AX AS INTEGER
BX AS INTEGER
CX AS INTEGER
DX AS INTEGER
Segment AS INTEGER
END TYPE
DIM SHARED MouseRegs AS MouseType
DIM SHARED MousePresent
REDIM Cursor(1 TO 32)
IF NOT MouseThere% THEN 'ensure a mouse is present
PRINT "No mouse is installed" ' and initialize it if so
END
END IF
CLS
DEF SEG = 0 'see what type of monitor
IF PEEK(&H463) <> &HB4 THEN 'if it's color
ColorMon = -1 'remember that for later
SCREEN 12 'this requires a VGA
LINE (0, 0)-(639, 460), 1, BF 'paint a blue background
END IF
DIM Choice$(1 TO 5) 'display some choices
LOCATE 1, 1 'for something to point at
FOR X = 1 TO 5
READ Choice$(X)
PRINT Choice$(X);
LOCATE , X * 12
NEXT
DATA "Choice 1", "Choice 2", "Choice 3"
DATA "Choice 4", "Choice 5"
IF NOT ColorMon THEN 'if it's not color
CALL TextCursor(-2, -2) 'select a text cursor
END IF
CALL ShowCursor
CALL Prompt("Point the cursor at a choice, and press _
a button.")
DO 'wait for a button press
CALL ReadCursor(X, Y, Button)
LOOP UNTIL Button
IF Button AND 4 THEN Button = 3 'for three-button mice
CALL Prompt("You pressed button" + STR$(Button) + _
" and the cursor was at location" + STR$(X) + "," + _
STR$(Y) + " - press a button.")
IF ColorMon THEN 'if it is a color monitor
RESTORE Arrow ' load a custom arrow
GOSUB DefineCursor
END IF
Dummy = WaitButton%
IF ColorMon THEN 'the hardware can do it
RESTORE CrossHairs 'set a cross-hairs cursor
GOSUB DefineCursor
CALL Prompt("Now the cursor is a cross-hairs, press _
a button.")
Dummy% = WaitButton%
END IF
IF ColorMon THEN 'now set an hour glass
RESTORE HourGlass
GOSUB DefineCursor
END IF
CALL Prompt("Now notice how the cursor range is _
restricted. Press a button to end.")
CALL MouseTrap(50, 50, 100, 100)
Dummy = WaitButton%
IF ColorMon THEN 'restore to 640 x 350
CALL MouseTrap(0, 0, 349, 639)
ELSE 'use CGA bounds for mono!
CALL MouseTrap(0, 0, 199, 639)
END IF
Dummy = MouseThere% 'reset the mouse driver
CALL HideCursor 'and turn off the cursor
SCREEN 0 'revert to text mode
END
DefineCursor:
FOR X = 1 TO 32 'read 32 words of data
READ Dat$ 'read the data
Cursor(X) = Bin2Hex%(Dat$) 'convert to integer
NEXT
CALL CursorShape(Zero, Zero, Cursor())
RETURN
Arrow:
NOTES:
'The first group of binary data is the screen mask.
'The second group of binary data is the cursor mask.
'The cursor color is black where both masks are 0.
'The cursor color is XORed where both masks are 1.
'The color is clear where the screen mask is 1 and the
' cursor mask is 0.
'The color is white where the screen mask is 0 and the
' cursor mask is 1.
'
'Mouse cursor designs by Phil Cramer.
'--- this is the screen mask
DATA "1110011111111111"
DATA "1110001111111111"
DATA "1110000111111111"
DATA "1110000011111111"
DATA "1110000001111111"
DATA "1110000000111111"
DATA "1110000000011111"
DATA "1110000000001111"
DATA "1110000000000111"
DATA "1110000000000011"
DATA "1110000000000001"
DATA "1110000000011111"
DATA "1110001000011111"
DATA "1111111100001111"
DATA "1111111100001111"
DATA "1111111110001111"
'---- this is the cursor mask
DATA "0001100000000000"
DATA "0001010000000000"
DATA "0001001000000000"
DATA "0001000100000000"
DATA "0001000010000000"
DATA "0001000001000000"
DATA "0001000000100000"
DATA "0001000000010000"
DATA "0001000000001000"
DATA "0001000000000100"
DATA "0001000000111110"
DATA "0001001100100000"
DATA "0001110100100000"
DATA "0000000010010000"
DATA "0000000010010000"
DATA "0000000001110000"
CrossHairs:
DATA "1111111101111111"
DATA "1111111101111111"
DATA "1111111101111111"
DATA "1111000000000111"
DATA "1111011101110111"
DATA "1111011101110111"
DATA "1111011111110111"
DATA "1000000111000000"
DATA "1111011111110111"
DATA "1111011101110111"
DATA "1111011101110111"
DATA "1111000000000111"
DATA "1111111101111111"
DATA "1111111101111111"
DATA "1111111101111111"
DATA "1111111111111111"
DATA "0000000010000000"
DATA "0000000010000000"
DATA "0000000010000000"
DATA "0000111111111000"
DATA "0000100010001000"
DATA "0000100010001000"
DATA "0000100000001000"
DATA "0111111000111111"
DATA "0000100000001000"
DATA "0000100010001000"
DATA "0000100010001000"
DATA "0000111111111000"
DATA "0000000010000000"
DATA "0000000010000000"
DATA "0000000010000000"
DATA "0000000000000000"
HourGlass:
DATA "1100000000000111"
DATA "1100000000000111"
DATA "1100000000000111"
DATA "1110000000001111"
DATA "1110000000001111"
DATA "1111000000011111"
DATA "1111100000111111"
DATA "1111110001111111"
DATA "1111110001111111"
DATA "1111100000111111"
DATA "1111000000011111"
DATA "1110000000001111"
DATA "1110000000001111"
DATA "1100000000000111"
DATA "1100000000000111"
DATA "1100000000000111"
DATA "0000000000000000"
DATA "0001111111110000"
DATA "0000000000000000"
DATA "0000111111100000"
DATA "0000100110100000"
DATA "0000010001000000"
DATA "0000001010000000"
DATA "0000000100000000"
DATA "0000000100000000"
DATA "0000001010000000"
DATA "0000011111000000"
DATA "0000110001100000"
DATA "0000100000100000"
DATA "0000000000000000"
DATA "0001111111110000"
DATA "0000000000000000"
FUNCTION Bin2Hex% (Binary$) STATIC 'binary to integer
Temp& = 0
Count = 0
FOR X = LEN(Binary$) TO 1 STEP -1
IF MID$(Binary$, X, 1) = "1" THEN
Temp& = Temp& + 2 ^ Count
END IF
Count = Count + 1
NEXT
IF Temp& > 32767 THEN Temp& = Temp& - 65536
Bin2Hex% = Temp&
END FUNCTION
SUB CursorShape (HotX, HotY, Shape()) STATIC
IF NOT MousePresent THEN EXIT SUB
MouseRegs.AX = 9
MouseRegs.BX = HotX
MouseRegs.CX = HotY
MouseRegs.DX = VARPTR(Shape(1))
MouseRegs.Segment = VARSEG(Shape(1))
CALL MouseInt(MouseRegs)
END SUB
SUB HideCursor STATIC 'turns off the mouse cursor
IF NOT MousePresent THEN EXIT SUB
MouseRegs.AX = 2
CALL MouseInt(MouseRegs)
END SUB
FUNCTION MouseThere% STATIC 'reports if a mouse is present
MouseThere% = 0 'assume there is no mouse
IF PeekWord%(Zero, (4 * &H33) + 2) = 0 THEN 'segment = 0
EXIT FUNCTION ' means there's no mouse
END IF
MouseRegs.AX = 0
CALL MouseInt(MouseRegs)
MouseThere% = MouseRegs.AX
IF MouseRegs.AX THEN MousePresent = -1
END FUNCTION
SUB MouseTrap (ULRow, ULColumn, LRRow, LRColumn) STATIC
IF NOT MousePresent THEN EXIT SUB
MouseRegs.AX = 7 'restrict horizontal movement
MouseRegs.CX = ULColumn
MouseRegs.DX = LRColumn
CALL MouseInt(MouseRegs)
MouseRegs.AX = 8 'restrict vertical movement
MouseRegs.CX = ULRow
MouseRegs.DX = LRRow
CALL MouseInt(MouseRegs)
END SUB
SUB MoveCursor (X, Y) STATIC 'positions the mouse cursor
IF NOT MousePresent THEN EXIT SUB
MouseRegs.AX = 4
MouseRegs.CX = X
MouseRegs.DX = Y
CALL MouseInt(MouseRegs)
END SUB
SUB Prompt (Message$) STATIC 'prints prompt message
V = CSRLIN 'save current cursor position
H = POS(0)
LOCATE 30, 1 'use 25 for EGA SCREEN 9
CALL HideCursor 'this is very important!
PRINT LEFT$(Message$, 79); TAB(80);
CALL ShowCursor 'and so is this
LOCATE V, H 'restore the cursor
END SUB
SUB ReadCursor (X, Y, Buttons) 'returns cursor and button
' information
IF NOT MousePresent THEN EXIT SUB
MouseRegs.AX = 3
CALL MouseInt(MouseRegs)
Buttons = MouseRegs.BX AND 7
X = MouseRegs.CX
Y = MouseRegs.DX
END SUB
SUB ShowCursor STATIC 'turns on the mouse cursor
IF NOT MousePresent THEN EXIT SUB
MouseRegs.AX = 1
CALL MouseInt(MouseRegs)
END SUB
SUB TextCursor (FG, BG) STATIC
IF NOT MousePresent THEN EXIT SUB
MouseRegs.AX = 10
MouseRegs.BX = 0
MouseRegs.CX = &HFF
MouseRegs.DX = 0
IF FG = -1 THEN 'maintain FG as the cursor moves?
MouseRegs.CX = MouseRegs.CX OR &HF00
ELSEIF FG = -2 THEN 'invert FG as the cursor moves?
MouseRegs.CX = MouseRegs.CX OR &H700
MouseRegs.DX = &H700
ELSE 'use the specified color
MouseRegs.DX = 256 * (FG AND &HFF)
END IF
IF BG = -1 THEN 'maintain BG as the cursor moves?
MouseRegs.CX = MouseRegs.CX OR &HF000
ELSEIF BG = -2 THEN 'invert BG as the cursor moves?
MouseRegs.CX = MouseRegs.CX OR &H7000
MouseRegs.DX = MouseRegs.DX OR &H7000
ELSE 'use the specified color
Temp = (BG AND 7) * 16 * 256
MouseRegs.DX = MouseRegs.DX OR Temp
END IF
CALL MouseInt(MouseRegs)
END SUB
FUNCTION WaitButton% STATIC 'waits for a button press
IF NOT MousePresent THEN EXIT FUNCTION
X! = TIMER 'pause to allow releasing
WHILE X! + .2 > TIMER ' the button
WEND
DO 'wait for a button press
CALL ReadCursor(X, Y, Button)
LOOP UNTIL Button
IF Button AND 4 THEN Button = 3 'for three-button mice
WaitButton% = Button 'assign the function
END FUNCTION
This program begins by declaring all of the support functions, and then
defines and dimensions the MouseRegs TYPE variable. The integer array is
used to hold the custom graphics cursor shape information, which the
CursorShape routine requires. The remainder of the program illustrates how
to use the various mouse routines in your own programs.
(2) Determining if a Mouse is Present
The first function is MouseThere, which serves two important purposes:
The first is to determine if a mouse is present. The second purpose of
MouseThere is to initialize the mouse driver to its default parameters.
This lets you be sure that the mouse color, shape, and other parameters are
in a known state. Resetting the mouse is strongly recommended because some
programs do not bother to reset the mouse when they are finished.
Although there is a mouse service to determine if the driver is
installed, you must also perform an additional test to prevent problems
with early computers running DOS version 2. The problem arises because
these computers leave the mouse interrupt (&H33) undefined if no mouse is
present, and calling this interrupt is likely to make the PC crash.
As you already know, the interrupt vector table in low memory holds
the segment and address for every interrupt service routine that is present
in the PC. But who puts those addresses into the interrupt vector table?
All of the BIOS interrupt addresses are assigned by the BIOS as part of the
power-up code in your PC's ROM. Likewise, DOS installs the addresses it
needs while it is being loaded from disk.
The BIOS in modern computers assigns every interrupt vector to a valid
address, even those that it (the BIOS) does not use. The code pointed to
by the unused interrupts is an assembly language Iret (Interrupt Return)
instruction. So if no other routine is servicing that interrupt, calling
it merely returns with no change to the register contents. But early
computers and early versions of DOS ignored Interrupt &H33, and left the
values in that vector address set to zero. [Calling the "code" at address
zero is guaranteed to fail, since address zero holds other addresses and
not executable code.] Therefore, to safely detect the presence of a mouse
requires first looking in low memory, to ensure that the interrupt address
there is valid.
It is important to understand that you *must* use MouseThere once at
the start of your program, before any of the other mouse routines will
work. All of the mouse routines check the global variable MousePresent
before calling MouseInt, and do nothing if it is zero. This safety
mechanism lets you freely call the various mouse services without regard to
whether or not a mouse is installed, to avoid the DOS 2 problem described
earlier. Thus, the same program statements can accommodate a mouse if one
is present or not, without requiring many separate IF tests.
For example, you will probably want to write programs that use a mouse
if one is present, but don't require it. If you had to have a separate
block of code for each case, your program would be much larger and slower
than necessary. Therefore, you can simply call these mouse routines
whether or not a mouse is present. The code fragment that follows shows a
simple example of this in context.
PRINT "Press a key or mouse button to continue: ";
DO
Temp$ = INKEY$
CALL ReadCursor(X, Y, Buttons)
LOOP UNTIL LEN(INKEY$) OR Buttons
PRINT "Thank you."
If MouseThere determined that no mouse was present when it was called
earlier, then ReadCursor will do nothing and return no values. Of course,
you will have to check for mouse events and act on them, but these can be
handled within the same blocks of code that also handle keyboard input.
Once the program knows that a mouse is in fact present, it checks to
see if the display adapter is color or monochrome. A color monitor
supports more mouse options such as changing the shape of the mouse cursor.
In this case the program assumes that you have a VGA adapter. If you have
only an EGA, simply change the SCREEN 12 statement to SCREEN 9. You will
also have to change the LOCATE command in the Prompt subprogram to use line
25 instead of line 30. Although the cursor shape can be altered with CGA
and Hercules adapters, those are not accommodate here.
Once the screen display mode is set, a filled box is drawn covering
the entire screen, to create an attractive blue background. You should be
aware that the drivers included come with many older, inexpensive clone
mouse devices do not support the EGA and VGA display modes. This is not a
limitation with the mouse hardware; rather, the problem lies in the driver
software. Fortunately, the MOUSE.COM and MOUSE.SYS drivers that Microsoft
includes with BASIC work with most brands of mouse. Furthermore, you are
allowed to distribute those drivers with your own programs, as long as you
include an appropriate copyright notice. See the license agreement that
came with your version of BASIC for more information on displaying the
Microsoft copyright.
CONTROLLING THE TEXT CURSOR
After reading and displaying a list of sample choices that serve as a menu,
the program again checks to see which type of adapter is present. If it is
monochrome, then a custom text cursor is defined using the TextCursor
routine. This routine is appropriate for both monochrome and color
adapters, and offers several useful options that let you control fully how
the foreground and background colors will appear. Also, an initial call to
TextCursor is needed with some non-Microsoft mouse drivers to ensure that
the cursor is displayed after calling ShowCursor.
TextCursor expects two parameters to control the cursor's foreground
and background colors. If a positive value is given for either parameter,
then that is the color the mouse cursor assumes as it travels around the
screen. For example, if you use a color combination of 0, 4 the character
under the mouse cursor will be shown in black on a red background. It is
important to understand that the normal mouse cursor color is actually the
character's background color. The foreground indicates what color the text
is to become as the cursor passes over it.
Using a value of -1 for either parameter tells the mouse driver to
leave that portion of the color alone when the cursor is positioned over a
character. If you use a color combination of 7, -1 the text under the
mouse cursor will be shown in white and the background will be unchanged.
Of course, if both the foreground and background are set to -1, the cursor
will never be visible.
A value of -2 causes that color portion to be inverted using an XOR
process as the cursor moves around the screen. That is, white becomes
black, green turns to magenta, and blue is translated to brown. Although a
value of -2 for the background guarantees that the cursor is always
visible, it can also be distracting to see the mouse cursor color change
constantly when the screen itself uses many colors. If you want to
experiment with the various TextColor options, add remarking apostrophes to
deactivate the three statements after the line IF PEEK(&H463) <> &HB4 THEN
near the beginning of the program.
The ShowCursor subprogram simply tells the mouse drive to make the
mouse cursor visible, in much the same way LOCATE , , 1 option does with
the normal screen cursor. The companion routine HideCursor turns the mouse
cursor off again. These are very simple routines that do not require much
explanation; however, please understand that until you turn the cursor on
explicitly it remains hidden. As a rule, you also want to ensure that the
cursor is turned off before you end your program and return to DOS.
There is one irritating quirk about how the mouse driver keeps track
of whether the mouse cursor is currently visible or not. When you use the
statement LOCATE , , 0 to turn off the regular text cursor, the BIOS
remembers that it is off. And if you subsequently use the same statement
again the request is ignored. The mouse driver, on the other hand,
remembers how many times you called HideCursor and requires a corresponding
number of calls to ShowCursor before it becomes visible. However, the
reverse is not true. If you turn on the cursor, say, five times in a row,
only one call to HideCursor is needed to turn it off.
READING THE MOUSE BUTTONS AND CURSOR POSITION
The next mouse routine is called ReadCursor, and it calls the service that
returns both the current mouse cursor position and also which buttons are
currently pressed. Notice that the X and Y values returned assume graphics
pixel coordinates even when the display screen is in text mode! Therefore,
when a monochrome display adapter is being used, the values returned range
from 0 to 639 horizontally (X), and 0 through 199 vertically (Y). These
are the same values you would receive when in CGA black and white screen
mode 2. When in graphics mode, the X and Y values are based on the current
SCREEN setting. For example, in EGA screen mode 9, the returned value for
X ranges from 0 through 639, and Y is between 0 and 349.
When your program is in text mode (SCREEN 0), the current X and Y
cursor location is based on the upper-left corner of the mouse cursor box.
Therefore, the actual horizontal range (X) is usually returned between 0
and 632 to account for a box width of 8 pixels. The vertical location (Y)
ranges from 0 to 192 for the same reason: If the bottom of the cursor is at
the bottom of the screen, then the top is eight pixels higher. In graphics
mode you are allowed to establish any portion of the mouse cursor as being
the *hot spot*, and this is discussed below in the section "Changing the
Mouse Cursor Shape".
The buttons are returned bit coded--the lowest bit is set if button 1
is pressed, and the next bit is set when the second button is pressed. If
a mouse has three buttons, the third bit may also be set to indicate that.
Isolating which bit or combination of bits is set is done using the AND
logic operator. If Button AND 1 is non-zero then the first button is
pressed. Similarly, Button AND 2 means the second button is being pressed.
However, testing for button 3 requires a value of 4, since that is the
value of the third bit. The program fragment that follows shows this in
context, and you can press one or more buttons at a time.
DO
PRINT "Press Ctrl-Break to end."
CALL ReadCursor(X, Y, Button)
LOCATE 10, 1
IF Button AND 1 THEN
PRINT "BUTTON 1"
ELSE
PRINT " "
END IF
LOCATE 10, 11
IF Button AND 2 THEN
PRINT "BUTTON 2"
ELSE
PRINT " "
END IF
LOCATE 10, 21
IF Button AND 4 THEN
PRINT "BUTTON 3"
ELSE
PRINT " "
END IF
LOOP
Besides the ReadCursor routine which returns the cursor position and button
status, I have also included a related function called WaitButton. If your
program will be waiting for a button and needs to know which button was
pressed, WaitButton does this using fewer bytes of compiler-generated code.
Since there are no passed parameters only five bytes are needed to call
WaitButton, compared to 17 needed to call ReadCursor. WaitButton simply
waits in an empty loop until a button is pressed, and then reports which
button it was.
CHANGING THE MOUSE CURSOR SHAPE
The CursorShape routine lets you change the size and shape of the mouse
cursor when the display is in graphics mode. The mouse driver routine that
is called requires the address of a block of memory 32 words long that
holds the new shape and color information. The data in this memory block
is organized into two sections. The first 16 words hold what is called the
*screen mask*, and the second 16 words hold the *cursor mask*.
The bits in these masks interact to change the way the foreground and
background colors on the screen change as the cursor passes over them. The
method used by the mouse driver to control the cursor shape and colors is
very complex, and the examples and discussions in Microsoft's documentation
do little to assist the programmer. Therefore, I have provided a simple
mechanism that lets you draw the cursor shape using a series of BASIC DATA
statements.
Using this method it is easy to control each individual pixel in the
mouse cursor, and determine if it is white, black, or transparent. When
the bits in both the screen and cursor masks are both zero, the cursor will
be black. And when the bits in both masks are set to 1, the color is XORed
(reversed) at that pixel position. If a screen mask bit is 1 and its
corresponding bit in the cursor mask is 0, the cursor is transparent.
Reversing this to make the screen mask 0 and the cursor mask 1 makes the
cursor white at that position. Thus, you can create nearly any shape for
the mouse cursor, and a wide variety of interesting color effects.
If your needs are modest or to minimize the number of DATA statements,
you can define only the cursor mask and use -1 for the first 16 elements in
the array by changing that portion of the program like this:
DefineCursor:
FOR X = 1 TO 32 'read 32 words of data
IF X < 17 THEN 'set first 16 elements = -1
Cursor(X) = -1
ELSE 'and for the second 16
READ Dat$ ' read the data and then
Cursor(X) = Bin2Hex%(Dat$) ' convert to an integer
END IF
NEXT
DATA "1100000000000000" 'use only 16 DATA items
DATA "1110000000000000" ' in this section
.
.
The other two parameters required by CursorShape are the X and Y cursor hot
spots. When you call ReadCursor to return the current mouse cursor
location and button information, the X and Y position returned identifies a
single pixel on the screen. Which pixel within the mouse cursor that is
reported is the cursor hot spot. When you use an arrow cursor shape, the
hot spot is typically the tip of the arrow. This is located in the upper
left corner of the cursor box and is identified as location 0, 0. However,
you can also make any other portion of the cursor the hot spot. For
simplicity, the GOSUB routine at the DefineCursor label always uses 0, 0.
However, the cross hairs cursor really should use the values 8, 8 to set
the hot spot at the center of the block.
CONTROLLING THE MOUSE CURSOR POSITION AND RANGE
The MoveCursor routine lets you set a new position for the mouse cursor,
and it too expects pixel values even when the screen is in text mode.
Although MoveCursor is not demonstrated in this program, it is included in
the interest of completeness.
The final mouse subprogram included lets you restrict the range of
mouse cursor travel, and it is called--appropriately enough--MouseTrap.
You pass the upper-left and lower-right boundaries to MouseTrap, and it in
turns passes those values on to the mouse driver. Internally, the mouse
driver lets you restrict the range for horizontal and vertical motion
independently. But for simplicity this routines requires both sets of
values at one time.
Like the services that ReadCursor and MoveCursor call, these services
also expect the cursor bounds to be given as pixels even when in text mode.
Also, notice that the mouse driver always forces the cursor into the
restricted region for you. That is, if the cursor is in the upper-left
corner and you call MouseTrap forcing it to stay inside the bottom half of
the screen, it will be moved to the top of that region.
Be aware that MouseTrap is also required if you plan to use the 43- or
50-line EGA and VGA text modes. By default, the mouse driver assumes that
a text screen has only 25 lines, and will not normally let the mouse cursor
be placed below that line. If you have used WIDTH , 50 to put the screen
into the 50-line mode, the mouse cursor will not be allowed below line 25.
Therefore, you must use MouseTrap to increase the allowable cursor region
beyond the default range. Also be aware that using values larger than the
current screen dimensions let the mouse disappear off the bottom of the
screen, or wrap around past the right edge and reappear on the left side.
ACCESSING THE MOUSE DRIVER
All of the mouse routines considered so far are comprised of a simplified
interface to the mouse driver through the MouseInt routine. MouseInt lets
you access any service supported by the mouse driver, including those that
I have not described here. Similar to the various DOS and BIOS services,
the mouse driver expects a service number in the AX register. The other
registers contain the various expected parameters and returned information,
and they vary from service to service.
There are no errors returned by the mouse driver, so no mechanism is
needed to handle errors. For example, if you tell the mouse driver to
position the cursor off the top edge of the screen, it simply ignores you.
Unfortunately, discussing every possible mouse service goes beyond
what I could ever hope to include in a book about BASIC. If you want to
learn more about the services that are available to you, I recommend
purchasing a good technical reference such as the Microsoft Mouse
Programmer's Reference. Other mouse manufacturers also publish their own
technical manuals, and make them available to the public for a small
charge. Thankfully, all of the mouse services are consistent across
brands, although some brands include more features than defined by
Microsoft. Unless you write programs only for your own use, you should
avoid relying on services that are specific to a single manufacturer.
ACCESSING EXPANDED MEMORY
=========================
The last set of routines I will present show how you can use interrupts to
access an expanded memory (EMS) driver. Expanded memory has been available
for many years, and it provides a way to exceed the normal 640K RAM barrier
imposed by the 8088 microprocessors. Newer computers that use an 80286 or
later processors can use what is called Extended Memory (XMS), and this
type of memory will eventually become the standard way for all computers in
the future to access more than 1MB of memory. Unfortunately, accessing the
extended memory beyond 1MB on an 80286-based PC is complicated by a design
deficiency in that CPU chip. Many people are confused about the difference
between Expanded and Extended memory, so perhaps a brief explanation is in
order.
Extended memory is a single contiguous block that starts at address
zero and extends through the highest address available, based on the amount
of memory that is present in a PC. Expanded memory, on the other hand, is
more complex, and uses a technique called *bank switching*. With bank
switching, a large amount of memory (up to 16 megabytes) is made available
to the CPU in 16K blocks. Each of these blocks is called a page, and only
four of them can be accessed at one time. Thus, the term bank switching is
appropriate because various banks of far memory are switched in and out of
a near memory address space.
The EMS standard requires a 64K contiguous area of near memory within
the 1MB addressable range to be reserved for use by the EMS driver as a
*page frame*. On my own PC the 64k address range from &HE000:0000 through
&HE000:FFFF is not used for any other purpose, and is therefore available
for use by an EMS driver. At any given time, the four 16K blocks of memory
within this segment can be connected to memory that lies outside of the 1MB
normal address range.
Hardware plug-in EMS boards such as the Intel Above Board contain
their expanded memory on the board itself. EMS emulator software instead
converts the Extended memory on computers so equipped to be accessible
through the 64K segment within the EMS page frame. This is achieved
through hardware switches that allow any area of memory to be remapped to
any other range of addresses. In either case, however, Expanded memory is
made available to an application one page at a time as near memory.
Each of the four 16K near memory pages in the EMS page frame are
called *physical pages*, because they reside in physical memory that can be
accessed directly by the CPU. However, many pages of far EMS memory are
available--up to four at a time--and these are called *logical pages*.
This is shown graphically in Figure 11-6.
│
│
│
│
│
──────┼────────────┤
/ │ Page 73 │
1MB boundary --> ┌────────────┐ / ──────┼────────────┤
│ ROM BIOS │ / / │ Page 72 │
┌─>╞════════════╡/ / ──────┼────────────┤
│ │ Page 3 │ / / │ │
│ ├────────────┤/ / │ │
Physical │ │ Page 2 │ / │ │
Pages │ ├────────────┼──────────────┼────────────┤
│ │ Page 1 │ │ Page 45 │
│ ├────────────┼──────────────┼────────────┤
│ │ Page 0 │ \ │ │
└─>├────────────┤\ \────────┼────────────┤
│ DISPLAY │ \ │ Page 38 │
│ MEMORY │ \─────────┼────────────┤
640K boundary --> ╞════════════╡ │ │
│ │ │ │
│ Normal │ │ EMS │
│ DOS │ │ Logical
│ Memory Pages │
│ │
│ │
│ └────────────┘
│
Address 0 --> └────────────┘
Figure 11-6: How EMS logical pages in far memory are mapped onto physical
pages in conventional memory.
Here, physical page 0 is connected to logical page 38 in expanded memory,
physical page 1 to logical page 45, and so forth. Whenever a program wants
to access a particular logical page in expanded memory, it calls the EMS
driver telling it to map that page to one of the four physical pages in the
page frame segment. Then, the EMS logical page can be accessed at the near
memory address within the page frame.
For simplicity, all of the routines provided here to handle Expanded
memory use physical page 0 only. Since these routines merely copy array
data back and forth between conventional and Expanded memory, the data can
be copied in blocks of 16K and there is no need to have to map multiple
pages simultaneously. Therefore, these routines always map physical page 0
to whichever logical page needs to be accessed, and then copy the data in
that page only.
EMS SERVICES
As with the DOS services accessed through Interrupt &H21, the EMS driver
also uses handles to identify which data you are working with. When memory
is allocated using EMS Interrupt &H67, you tell the driver how many 16K
pages you are requesting, and if there is sufficient memory available it
returns a handle. It should come as no surprise to learn that these
parameters are passed using the CPU registers. Also like DOS and the BIOS,
the EMS driver expects a service number in the AH Register. For example,
the service that requests memory is specified with AH set to &H43.
To minimize the amount of code that is added to your programs, I have
created a short assembly language subroutine called EMSInt that replaces
the Interrupt routine included with BASIC. As with DOSInt and MouseInt,
this routine lets you pass only the parameters that are actually needed, to
reduce the amount of compiler-generated code. EMSInt needs access only to
the AX, BX, CX, and DX registers, so these are the only components in the
EMSType TYPE structure shown below.
TYPE EMSType
AX AS INTEGER
BX AS INTEGER
CX AS INTEGER
DX AS INTEGER
END TYPE
Unlike BASIC's Interrupt routine that has to deal with three parameters and
code to generate any interrupt number, EMSInt itself is relatively simple:
;EMSINT.ASM
.Model Medium, Basic
EMSRegs Struc
RegAX DW ?
RegBX DW ?
RegCX DW ?
RegDX DW ?
EMSRegs Ends
.Code
EMSInt Proc Uses SI, ERegs:Word
Mov SI,ERegs ;get the address of EMSRegs
Mov AX,[SI+RegAX] ;load each register in turn
Mov BX,[SI+RegBX]
Mov CX,[SI+RegCX]
Mov DX,[SI+RegDX]
Int 67h ;call the EMS driver
Mov SI,ERegs ;access EMSRegs again
Mov [SI+RegAX],AX ;save each register in turn
Mov [SI+RegBX],BX
Mov [SI+RegCX],CX
Mov [SI+RegDX],DX
Ret ;return to BASIC
EMSInt Endp
End
If you plan to use the mouse and EMS routines in the same program, you
could use the MouseRegs variable for both and ignore the Segment portion
when call EMSInt.
The program that follows combines a demonstration portion and a
collection of subprograms and functions. Notice that like the various
mouse services, you *must* query EMSThere to ensure that an EMS driver is
loaded before any of the other routines can be used.
'EMS.BAS, demonstrates the EMS memory services
DEFINT A-Z
DECLARE FUNCTION Compare% (BYVAL Seg1, BYVAL Adr1, BYVAL Seg2, _
BYVAL Adr2, NumBytes)
DECLARE FUNCTION EMSErrMessage$ (ErrNumber)
DECLARE FUNCTION EMSError% ()
DECLARE FUNCTION EMSFree& ()
DECLARE FUNCTION EMSThere% ()
DECLARE FUNCTION PeekWord% (BYVAL Segment, BYVAL Address)
DECLARE SUB EMSInt (EMSRegs AS ANY)
DECLARE SUB EMSStore (Segment, Address, ElSize, NumEls, Handle)
DECLARE SUB EMSRetrieve (Segment, Address, ElSize, NumEls, Handle)
DECLARE SUB MemCopy (BYVAL FromSeg, BYVAL FromAdr, BYVAL ToSeg, _
BYVAL ToAdr, NumBytes)
TYPE EMSType 'similar to DOS Registers
AX AS INTEGER
BX AS INTEGER
CX AS INTEGER
DX AS INTEGER
END TYPE
DIM SHARED EMSRegs AS EMSType
DIM SHARED ErrCode
DIM SHARED PageFrame
CLS
IF NOT EMSThere% THEN 'ensure EMS is present
PRINT "No EMS is installed"
END
END IF
PRINT "This computer has"; EMSFree&;
PRINT "kilobytes of EMS available"
REDIM Array#(1 TO 20000)
FOR X = 1 TO 20000
Array#(X) = X
NEXT
CALL EMSStore(VARSEG(Array#(1)), VARPTR(Array#(1)), 8, 20000, Handle)
IF EMSError% THEN
PRINT EMSErrMessage$(EMSError%)
END
END IF
REDIM Array#(1 TO 20000)
CALL EMSRetrieve(VARSEG(Array#(1)), VARPTR(Array#(1)), 8, 20000, Handle)
IF EMSError% THEN
PRINT EMSErrMessage$(EMSError%)
END
END IF
FOR X = 1 TO 20000 'prove it worked
IF Array#(X) <> X THEN PRINT ".";
NEXT
END
FUNCTION EMSErrMessage$ (ErrNumber) STATIC
SELECT CASE ErrNumber
CASE 128
EMSErrMessage$ = "Internal error"
CASE 129
EMSErrMessage$ = "Hardware malfunction"
CASE 131
EMSErrMessage$ = "Invalid handle"
CASE 133
EMSErrMessage$ = "No handles available"
CASE 135, 136
EMSErrMessage$ = "No pages available"
CASE ELSE
IF PageFrame THEN
EMSErrMessage$ = "Undefined error: " + STR$(ErrNumber)
ELSE
EMSErrMessage$ = "EMS not loaded"
END IF
END SELECT
END FUNCTION
FUNCTION EMSError% STATIC
Temp& = ErrCode
IF Temp& < 0 THEN Temp& = Temp& + 65536
EMSError% = Temp& \ 256
END FUNCTION
FUNCTION EMSFree& STATIC
EMSFree& = 0 'assume failure
IF PageFrame = 0 THEN EXIT FUNCTION
EMSRegs.AX = &H4200
CALL EMSInt(EMSRegs)
ErrCode = EMSRegs.AX 'save possible error from AH
IF ErrCode = 0 THEN EMSFree& = EMSRegs.BX * 16
END FUNCTION
SUB EMSRetrieve (Segment, Address, ElSize, NumEls, Handle) STATIC
IF PageFrame = 0 THEN EXIT SUB
LocalSeg& = Segment 'use copies we can change
LocalAdr& = Address
BytesNeeded& = NumEls * CLNG(ElSize)
PagesNeeded = BytesNeeded& \ 16384
Remainder = BytesNeeded& MOD 16384
IF Remainder THEN PagesNeeded = PagesNeeded + 1
NumBytes = 16384 'assume we're copying a
' complete page
ThisPage = 0 'start copying to page 0
FOR X = 1 TO PagesNeeded 'copy the data
IF X = PagesNeeded THEN 'watch out for last page
IF Remainder THEN NumBytes = Remainder
END IF
IF LocalAdr& > 32767 THEN 'handle segment boundaries
LocalAdr& = LocalAdr& - &H8000&
LocalSeg& = LocalSeg& + &H800
IF LocalSeg& > 32767 THEN
LocalSeg& = LocalSeg& - 65536
END IF
END IF
EMSRegs.AX = &H4400 'map physical page 0 to the
EMSRegs.BX = ThisPage ' current logical page
EMSRegs.DX = Handle ' for the given handle
CALL EMSInt(EMSRegs) 'then copy the data there
ErrCode = EMSRegs.AX 'save possible error from AH
IF ErrCode THEN EXIT SUB
CALL MemCopy(PageFrame, Zero, CINT(LocalSeg&), CINT(LocalAdr&), _
NumBytes)
ThisPage = ThisPage + 1
LocalAdr& = LocalAdr& + NumBytes
NEXT
EMSRegs.AX = &H4500 'release memory service
EMSRegs.DX = Handle
CALL EMSInt(EMSRegs)
ErrCode = EMSRegs.AX 'save possible error
END SUB
SUB EMSStore (Segment, Address, ElSize, NumEls, Handle) STATIC
IF PageFrame = 0 THEN EXIT SUB
LocalSeg& = Segment 'use copies we can change
LocalAdr& = Address
BytesNeeded& = NumEls * CLNG(ElSize)
PagesNeeded = BytesNeeded& \ 16384
Remainder = BytesNeeded& MOD 16384
IF Remainder THEN PagesNeeded = PagesNeeded + 1
EMSRegs.AX = &H4300 'allocate memory service
EMSRegs.BX = PagesNeeded
CALL EMSInt(EMSRegs)
ErrCode = EMSRegs.AX 'save possible error from AH
IF ErrCode THEN EXIT SUB
Handle = EMSRegs.DX 'save the handle returned
NumBytes = 16384 'assume we're copying a
' complete page
ThisPage = 0 'start copying to page 0
FOR X = 1 TO PagesNeeded 'copy the data
IF X = PagesNeeded THEN 'watch out for last page
IF Remainder THEN NumBytes = Remainder
END IF
IF LocalAdr& > 32767 THEN 'handle segment boundaries
LocalAdr& = LocalAdr& - &H8000&
LocalSeg& = LocalSeg& + &H800
IF LocalSeg& > 32767 THEN
LocalSeg& = LocalSeg& - 65536
END IF
END IF
EMSRegs.AX = &H4400 'map physical page 0 to the
EMSRegs.BX = ThisPage ' current logical page
EMSRegs.DX = Handle ' for the given handle
CALL EMSInt(EMSRegs) 'then copy the data there
ErrCode = EMSRegs.AX 'save possible error from AH
IF ErrCode THEN EXIT SUB
CALL MemCopy(CINT(LocalSeg&), CINT(LocalAdr&), PageFrame, Zero, _
NumBytes)
ThisPage = ThisPage + 1
LocalAdr& = LocalAdr& + NumBytes
NEXT
END SUB
FUNCTION EMSThere% STATIC
EMSThere% = 0 'assume the worst
DIM DevName AS STRING * 8
DevName = "EMMXXXX0" 'search for this below
'---- Try to find the string "EMMXXXX0" at offset 10 in the EMS handler.
' If it's not there then EMS cannot possibly be installed.
Int67Seg = PeekWord%(0, (&H67 * 4) + 2)
IF NOT Compare%(Int67Seg, 10, VARSEG(DevName$), VARPTR(DevName$), 8) THEN
EXIT FUNCTION
END IF
EMSRegs.AX = &H4100 'get Page Frame Segment service
CALL EMSInt(EMSRegs)
ErrCode = EMSRegs.AX 'save possible error from AH
IF ErrCode = 0 THEN
EMSThere% = -1
PageFrame = EMSRegs.BX
END IF
END FUNCTION
EMS.BAS begins by declaring all of the subprograms and functions that it
uses, as well as the EMSType structure. The three shared variables are
used by the various procedures, and should not be removed when you delete
the demo portion to create a reusable module.
DETERMINING IF EMS IS PRESENT
The first function used is EMSThere, which reports if an EMS driver is
loaded and operative. EMSThere begins by assuming that an EMS driver is
not loaded, and assigns a function output value of 0. Then it attempts to
find the device name "EMMXXXX0" in the header portion of the EMS device
driver. Like the MouseThere function that checked the interrupt vector
table for a non-zero segment value, this preliminary check is also needed
to prevent a system lockup on older computers running DOS version 2.
To search for this string EMSThere uses PeekWord to retrieve the
segment for Interrupt &H67, and then looks at the eight bytes at offset 10
within that segment. If the Compare function finds the unique identifying
string, it knows that the driver is loaded and it is safe to invoke
Interrupt &H67. Service &H41 returns either -1 in AX if the driver is
active, or 0 if it is not. This service also returns the page frame
segment the driver is using in near memory, and EMSThere saves this value
in the shared variable PageFrame for access by the other routines.
DETERMINING AVAILABLE EMS MEMORY
The second function, EMSFree, returns the number of 16K EMS pages that are
available to your program. The remainder of the demonstration simply
dimensions a 20,000 element double precision array, and then saves it to
expanded memory. Because this array exceeds 64K, you must start BASIC with
the /ah command line switch. Otherwise you will receive a "Subscript out
of range" error message.
EMSFree uses function &H42 to ask the EMS driver for the number of
free pages, and the driver returns the page count in BX. Although it is
not shown here, service &H42 also returns the total number of pages in the
DX register. Therefore, you could easily create a TotalPages function from
a copy of EMSFree by changing the line that assigns the function output to
instead be IF ErrCode = 0 THEN TotalPages& = EMSRegs.DX * 16.
STORING AND RETRIEVING DATA
The actual storing and retrieving of data to and from Expanded memory is
fairly complicated, because of the need to map different logical pages to
physical page zero. Although Figure 11-6 shows a single group of logical
pages, the EMS driver really maintains a separate series of logical pages
for each active handle.
EMSStore and EMSRetrieve store and retrieve data in Expanded memory
respectively, and both of these subprograms are designed to accommodate
huge arrays larger than 64k. Therefore, additional work is needed to
calculate new segment values as each 16K portion has been processed.
As with all of the EMS procedures shown here, EMSStore begins by
verifying that EMSThere has already been invoked, and that a valid page
frame segment has been obtained. The next step is to make long integer
copies of the incoming segment and address parameters. Because of the
segment arithmetic that is performed later in the routine, long integers
are needed to allow values greater than 32,767 to be compared. Equally
important, a routine should never alter incoming parameters unless they
also return information or such changes are expected.
Next, EMSStore determines the total number of bytes of EMS storage
that are needed, and from that calculates the total number of 16K pages.
Because the EMS driver allocates entire pages only, an odd number of bytes
requires an entire additional page. BASIC's MOD function is used for this,
and if the result is non-zero, the TotalPages variable is incremented.
Once the number of pages is known, service &H43 is called to allocate
the Expanded memory. The remainder of the procedure walks through the
array data in 16K increments, mapping physical page zero to the next
logical page in sequence. Note the code that tests the current address to
see if it is within 32K of spanning a segment boundary. In that case, the
address is dropped by 32K, and the segment is increased by an equivalent
amount. Because each new segment starts 16 bytes higher than the previous
one, 32K \ 16 is added to LocalSeg& rather than a full 32K.
After the array is stored in EMS, it is redimensioned in the
demonstration and then retrieved using the EMSRetrieve subprogram.
EMSRetrieve is nearly identical to EMSStore, except it copies from EMS to
the array, and releases memory when it is finished rather than claim it at
the beginning. The final step in the demonstration is to examine the value
in each element, to prove that the array was restored correctly.
DETECTING EMS ERRORS
The EMSError function retrieves the current value of ErrCode, and
manipulates it into a form useable by your programs. EMS errors are
returned in the AH register, which requires dividing by 256 to derive a
single byte value. But since EMS error numbers start at 128, the value
returned in AX appears negative to BASIC programs which treat all integers
as being signed. This is why a long integer is used initially and then
converted to a positive value, before dividing to produce the final result.
The EMSErrMessage function can be used to display an appropriate
message if an error is detected. The incoming error code is filtered
through a series of CASE statements, based on the error values defined by
the EMS specification.
SUGGESTED ENHANCEMENTS
The routines presented herein provide a limited set of services for
accessing Expanded memory. However, there are several improvements you can
make, and a few other uses that I have not shown. If you are using BASIC
PDS [or VB/DOS], one useful enhancement you can add is to change the
subprograms and functions to receive their parameters by value using the
BYVAL option. In fact, this can also be done with the DOS and mouse
routines, to minimize the amount of code the BASIC compiler adds to your
final executable program.
Although this demonstration shows storing array data only, you can
also use these routines to store and retrieve text and graphics screens.
This is much quicker than saving them to disk, as was shown in Chapter 6.
For example, to save a 25 line by 80 column color text screen in Expanded
memory you would use the appropriate segment and address like this:
CALL EMSStore(&HB800, 0, 1, 4000, Handle)
CALL EMSRetrieve(&HB800, 0, 1, 4000, Handle)
Just as you can cause problems by failing to close DOS handles during the
development of a program, the same problem can happen with an EMS driver.
Unfortunately, it is not as easy to know which handle numbers are still
open if you have not kept track of them yourself manually. DOS issues its
handles using a sensible series of sequential numbers. This is not
necessarily the case with EMS handles. The EMM386.EXE driver provided by
Microsoft does issue sequential handles, starting with handle 1. But many
drivers use other starting values, some work from high numbers backwards,
and yet others use a handle number sequence that is not in order.
Finally, to learn about all of the possible EMS services you need a
good reference. Although the primary services are shown here, there are
several others you may find useful. For example, service &H46 lets you
retrieve the EMS version number, and service &H4C lets you see how many
pages are currently allocated for a given handle. The EMS driver version
can be valuable, because newer drivers offer more features which you may
want to take advantage of. Ray Duncan's book "Advanced MS-DOS" mentioned
earlier is one good source, and it lists each EMS service and the possible
errors that can be returned.
SUMMARY
=======
In this chapter you learned how BASIC--and indeed, all languages--use
interrupts to communicate with the operating system. You learned what
interrupts are and how to access them, and how the CPU registers are used
to communicate information between your program and the interrupt handler
being invoked. You also learned how some of the two-byte registers can be
treated as two one-byte registers, which requires multiplying and dividing
to access those portions individually.
A number of complete programs were presented showing how to access the
BIOS, DOS, the mouse driver, and Expanded memory. In the section on BIOS
interrupts, examples were given that showed how to simulate pressing the
PrtSc key, and also how to call the video service that clears or scrolls
only a portion of the display screen.
The DOS examples included a complete set of subroutines to replace
BASIC's file handling statements. One advantage gained by bypassing BASIC
is to read and write large amounts of data at one time. Another is to
avoid the need for ON ERROR in certain programming situations. Although
calling the DOS services directly can be beneficial in many cases, it also
requires more work on your part. However, some services cannot be accessed
using BASIC alone, such as reading file and directory names, or determining
a file's attribute. Where BASIC employs string descriptors to know how
long a string is, DOS instead uses a CHR$(0) zero byte to mark the end.
The mouse and Expanded memory discussions described how those
interrupt services are accessed, and provided practical advice and warnings
where appropriate. Although a large number of interrupt routines were
described, there is a practical limit to how much information can be
provided here. In particular, you will need a separate reference manual
that describes the details of each interrupt service routine in depth.
In the next and final chapter you will learn how to program in
assembly language, and how to add assembly language routines to programs
you write using BASIC. Assembly language is unlike any high-level
language, and it provides the ultimate means to exploit fully all of the
resources in a PC.