                        Lowlevel.txt

        Description of Lowlevel uses and syntax.

        The syntax of the Lowlevel.bin program requires you to call it
with one (or more, as we'll see later) arguments which are dBASE
character strings.  The first argument must start with the interrupt
number followed by a comma and then the registers needed to send or
receive information to or from DOS, with their values where information
is being sent.  All values should be in ASCII hexadecimal.  Since the
first argument will also receive the values of any registers returned by
DOS, and since dBASE does not permit a .bin routine to increase the
length of an argument, there are two restrictions:

        1)  If you are using the CALL command, as opposed to the call()
function, to call lowlevel.bin, the first argument must be passed as a
variable, not a string literal. Otherwise, there will be nothing to hold
the results returned and dBASE won't be able to access them.  This won't
matter for those few services, such as service 0 of interrupt 10h to
change the video mode, that don't return any meaningful results in
registers.

        2)  The first argument must be long enough to hold the registers
returned.  You can pass just the names of registers, such as AH,AL, in
the argument, but Lowlevel.bin will add an equal sign after each and two
or four characters of data as appropriate, returning perhaps
AH=00,AL=04.  Pad the argument at the call with enough spaces, in this
case 6, that the expansion won't overflow the length of the argument, or
the extra characters returned will be truncated by dBASE and lost.

        The syntax of lowlevel.bin to return the DOS version is:

        Arg="21,AX=3000"
        CALL Lowlevel WITH Arg

        On return, "Arg" will look something like this:

        Arg="NC,AX=0004"

        The "NC" means "no carry"--usually, but not always, an
indication that the call to DOS or the BIOS has succeeded.  If "CY"
leads the argument on return, it means the carry flag is set, which as
documented under the various DOS and BIOS calls usually means an error
has occurred.  If an error has occurred, AX normally contains the error
number.

        In the example, there is no carry, so as specified for this
function call, AL, the lower part of AX, holds the major version number,
04.  AH should hold the minor number, which I expected would be 1 for my
Version 4.01, but for some reason it holds 0 on my system.  This is not
the fault of Lowlevel.bin but of something in this version of DOS; the
same result occurs without Lowlevel.bin.  Tested with DOS Version 3.30,
Lowlevel returns the full version as expected.

        It would have been equally acceptable to call with this
argument:

        Arg="21,AH=30,AL   "

        The values of AH and AL will be returned separately, which you
may find more meaningful in this situation since they do represent
separate items.  Do remember to include the extra spaces, or an equal
sign and a couple of zeroes, after the AL so the value returned for it
won't be chopped off by dBASE and lost.

        If DOS version 2.0 or later is in use, registers BX and CX may
also hold useful information:

        Arg="21,AX=3000,BX,CX"+space(10)
        CALL Lowlevel with Arg

        In this case, on return "Arg" will also give the values in the
BX and CX registers.  You can include other registers in the argument,
even all of them.  Of course, the values returned may not mean much.  If
you need the full flags register on return, include it as "FX"; any
values you attempt to pass in it will be ignored.  It is even possible
to include the same register twice in the argument either explicitly or
by including both CX and CH.  No problem should arise except that
attempting to pass two different values to the interrupt in the same
register will obviously fail; the values of the one later in the
argument will overwrite the earlier ones.

        As you no doubt realize by now, the syntax of calling Lowlevel
is fairly obscure.  The reason is that it must be completely general,
but dBASE does not allow null characters in strings or numbers.  Passing
binary values as ASCII strings is clumsy and requires code on both
ends--but it always works.
        
        While the parsing routine in lowlevel.bin is fairly forgiving of
extra spaces, lower case and missing equal signs or numbers,  I
recommend that you include equal signs and the full number of digits for
each register (use 0's if the value doesn't matter), because it
simplifies calculating where in the returned string the values you are
interested in will be.  You can if you prefer extract values from the
returned string with something like:

                substr( Arg1, at( "BX", Arg1 ) + 3, 4 )

        This will locate the BX value in the returned string without
your having to calculate its position in the string.

        I recommend that you generally do not attempt to call
Lowlevel.bin directly.  The syntax will drive you crazy, and it gets
worse than in the examples given so far.  Instead, build yourself a
user-defined function for each call you need, so the tricky parts can be
coded once and forgotten.  You may find the functions dec2hex() and
hex2dec(), included below and in Lowfuncs.prg, helpful. Here's a
function for our DOS-version call:

        FUNCTION dosver
        PRIVATE Arg
        Arg="21,AX=3000"
        CALL Lowlevel WITH Arg
        IF left( Arg, 2 )="CY"
          RETURN -hex2dec( right( Arg, 2 ) )
        ELSE
          RETURN hex2dec( right( Arg, 2 ) ) ;
            + hex2dec( substr( Arg, 7, 2 ) ) / 100
        ENDIF

        Once the function is compiled, and made available either in the
program file or the active procedure file, the simple command:

        ? dosver()

        will print the version number, or negative of the error number,
with no further need to struggle with the locations or coding of items
in the Arg string.  Or, the version number may be stored to a variable
where the application programs can consult it.

        Okay, enough for the first lesson.  Let's create a file.  There
are DOS functions 5bh and 6ch that are even fancier, but for now let's
use function 3ch to create a file if it does not exist, truncate it to
zero length if it does exist, and open it in either case, returning a
DOS handle.  You'll have to look at one of the sources given above, or a
similar text, to verify that this is what DOS function 3ch does.

        Obviously, we have to give Lowlevel.bin not only the values it
needs to know we want to create a file, but also the path and name of
the file. (If only a name is given, the current directory is used).
Here it is:

        Arg1 = "21,AX=3C00,CX=0000,DS:DX=2"
        Arg2 = "\dbase\myfile.txt"
        CALL Lowlevel WITH Arg1, Arg2

        The difference from our previous syntax is the "DS:DX=2" bit,
plus the presence of a second argument. "DS:DX=2" tells Lowlevel to put
the address of argument 2 into the DS:DX registers when calling INT 21h.
This technique can be used in reverse in the few cases when DOS
functions ask for an address then write data to it. In such a case the
contents of the argument won't matter, but its length at the call must
be enough to hold the data returned, and it must be passed as a
variable, not a character literal.

        In the example above, the call to DOS will be made with DS:DX
holding the address of the filespec in Arg2, with AX as usual holding
the function number 3c00h (AH=3c would do as well here, since the
contents of AL don't matter) and CX holding 0, meaning the file to be
opened should have no special attributes such as read-only, hidden or
system.  If all goes well, on return Arg1 might be:

        NC,AX=000C,CX=????,DS:DX=?

        The NC indicates success and 000Ch or 12 decimal is the handle
of the opened file (it might be a different number).  Arg will actually
contain digits on return in place of the question marks shown above, but
the values are undefined by DOS and should be ignored.

        Here's the function to tame it:

        FUNCTION fcreate
        PARAMETERS Filespec
        PRIVATE Arg1
        Arg1="21,AX=3C00,CX=0000,DS:DX=2"
        CALL Lowlevel WITH Arg1,Filespec
        RETURN hex2dec( substr( Arg1, 7, 4 ) ) ;
          * iif( left( Arg1, 2 ) = "CY", -1, 1 )

        Handle = fcreate( "\dBASE\myfile.txt" ) will return the handle
assigned to the file, or the negative of the error number.

        Fopen(), using DOS function 3dh which opens an existing file but
will not create one that does not exist, is of course similar.  CX is
not used but AL is, to hold the access code.  We'll pass the access code
as a number in the variable Mode, defined as 0=read only, 1=write only,
2=both.  As with fcreate(), the handle is returned, or a negative in
case of error.

        FUNCTION fopen
        PARAMETERS Filespec, Mode
        PRIVATE Arg1
        Arg1 = "21,AX=3D0" + str( Mode, 1 ) + ",DS:DX=2"
        CALL Lowlevel WITH Arg1, Filespec
        RETURN hextodec( substr( Arg1, 7, 4 ) ) ;
          * iif( left( Arg1, 2 ) = "CY", -1, 1 )

        Once the file is open, we might want to move or read the
position of the file pointer, using DOS function 42h.  Here "Method" is
0 to start at the top of the file, 1 from the current location, and 2
from the end, and Bytes is the number of bytes to move. Calling with
Method 1 or 2 and 0 Bytes will return the current location of the
pointer or the length of the file, respectively.  If using Method 1 or 2
Bytes may be negative, but not if using Method 0.  The need for two
types of conversions, into a signed or unsigned value, accounts for most
of the complexity:

        FUNCTION fileptr
        PARAMETERS Handle, Method, Bytes
        PRIVATE Arg1, C, D, V
        Arg1 = "21,AX=3D0" + str( Method, 1 ) + "," ;
           + "BX=" + dec2hex( Handle ) + ","
        V = abs( Bytes )
        IF Method = 0
          C = dec2hex( int( V / 2^16 ) )
          D = dec2hex( mod( V, 2^16 ) )
        ELSE
          IF Bytes < 0
            V = 2^32 - V
          ENDIF
          C = dec2hex( int( V / 2^8 ) )
          D = dec2hex( mod( V / 2^8 ) )
        ENDIF
        Arg1 = Arg1 + "CX=" + C + ",DX=" + D
        CALL Lowlevel WITH Arg1, Filespec
        IF left( Arg1, 2 ) = "CY"
          RETURN -hex2dec( substr( Arg1, 7, 4 ) )
        ELSE
          RETURN hex2dec( substr( Arg1, 31, 4 ) + ;
            substr( Arg1, 7, 4 ) )   && value in DX:AX
        ENDIF

        In the case of the fileptr() operation it is
possible, although a mistake, to set the pointer to a
negative location.  As written, the function above
will return an absurdly large number if the pointer
has been set negative, with negatives reserved for an error
on the call.

        The DOS service to close the file when you have finished is 3eh,
requiring a handle in BX.  We'll use the whole AX register instead of AH
so we'll have access to any error code returned in AL:

        FUNCTION fclose
        PARAMETERS Handle
        PRIVATE Arg1
        Arg1 = "21,AX=3E00,BX=" + dectohex( Handle )
        CALL Lowlevel WITH Arg1
        RETURN hex2dec( substr( Arg1, 7, 4 ) ) ;
            * iif( left( Arg1, 2 )= "CY", -1, 1 )

                   Peek, Poke and Buffer

        There are DOS and BIOS calls that pass data containing nulls, or
containing more than 254 bytes, such as the calls to read from and write
to files. In either case, passing the data in dBASE character-string
arguments may be awkward or impossible.  What's needed is a way to
reserve an area of memory, perhaps fill it, pass its address to
Lowlevel, and extract any data placed in it by DOS or the BIOS.

        Buffer.bin is 32,000 bytes long, the maximum for a dBASE .bin
file.  When called, it simply returns its own starting address to dBASE,
in ASCII hexadecimal segment:offset form as "XXXX:XXXX."  Using the CALL
command, a nine-byte character variable must be passed as argument to
hold the address returned.  The 32,000 bytes (7D00h) starting at the
address returned may be then used to hold data being passed to or from
DOS or the BIOS using Lowlevel.bin.  Save the address returned for later
use, because Buffer.bin cannot be called again without releasing it and
reloading--its code may have been overwritten by data.  If your app
cannot guarantee that Buffer.bin will not be called again without
reloading, add 100h to the address returned and use only the 7C00h bytes
starting there for data.  This will assure that the code will be left
intact and prevent a crash if it is called again.

        Poke.bin is straightforward.  It should be called with an
address as a first argument, in the usual ASCII hexadecimal "XXXX:XXXX"
form, a string of bytes to put there as the second argument, and the
number of repetitions if not 1 in the optional third argument--this
option is intended mainly to allow storing several nulls with a single
call.  To store miscellaneous data that contains some nulls, you'll
probably have to call Poke several times, once for each string of data
up to a null and again for each group of nulls itself.  In the second
argument, the bytes to poke into memory should be represented as binary
chr() values, chr(1) for 01h, chr(128) for 80h, etc.  The values
returned by its companion program, Peek.bin, are similarly binary and
can be turned back into their numeric equivalents by
asc( substr( Arg ), x, 1 ), etc.

        Finally, Where.bin can be used with Lowlevel if a buffer larger
than 32,000 bytes is required.  As indicated in the notes in the
Where.asm source code, dBASE stores an array as one 56-byte variable
with the name of the array, a byte indicating type "A" for array, and a
doubleword pointer to the storage of the array elements.  The elements
are stored as a contiguous block of 56-byte entries, one for each
element.  This in effect reserves an entire block of memory which may be
almost a full segment of 64K bytes for the array elements, and we can
ignore the intended purpose and use it however we wish.  The only trick
is to RELEASE the array when finished, as referring to an "array
element" which has been overwritten with other data will almost
certainly cause problems.  Simply declare an array of 1170 elements,
find its address with Where.bin, poke data there as needed, call
Lowlevel.bin with the address and eventually peek the returned data back
out.  It's more efficient to use Buffer.bin if we don't need more than
32,000 bytes in the buffer, of course.

        Where.bin looks through memory for a dBASE variable entry with
the name given.  To be precise, it looks through memory starting at
lowmem, set by default at segment 5000h.  Where.bin looks for any
occurrence of the name given (in upper case) and assumes that a find
represents the variable's entry in the runtime symbol table.  This is a
table of 17-byte entries--eleven bytes null-filled, as always, for the
name, a doubleword ID starting with 1Bh since the system variables use
the lower numbers, and two bytes unknown/reserved.

        Where.bin then looks for a 56-byte memory entry of the same name
and ID with an acceptable type, A, C, D, F, L, or N.  Any find is
reported as the correct entry; if nothing is found the search for a
RTSYM table entry giving the ID, then for a matching 56-byte entry, is
repeated from the location of the last supposed RTSYM entry.  The use of
a variable with an odd and long name, eight or more (up to ten)
characters will tend to reduce the number of false matches and speed up
the search.

        On a find, Where.bin reports the address of the 56-byte entry
for D, F, L or N types--maybe you can use it to decipher the data.  For
A or C types, it returns the pointer to the actual data, which in case
of an array is the base of the (56 bytes)*elements reserved area of
memory.

        When using Where to find an array used as a buffer in memory,
that's what we want.  However, if you intend to use the Where.bin
program as a general-purpose finder of variables, you may wish to have
it always return the address of the start of the variable.  You can then
add 12 to that address and use Peek.bin to find the data pointer for
type A and C variables.  To do so without reassembly, see the notes to
the Where.bin listing.

        There are many opportunities for error in the syntax of these
programs.  For example, if you declare an array "Buffer" and then
command

        Addr = call ( "Where", Buffer )

        you've made two mistakes. First, "Buffer" must be in quotes or
it causes a dBASE error--the name of an array without any subscript is
not a legitimate dBASE IV variable; if it were we'd have another problem
since it is the name "Buffer", not its contents, in which we are
interested.  Second, since the address requires 9 characters but
"Buffer" must receive them and has only 6, it has to be padded.  The
correct syntax is:

        Addr = call( "Where", "Buffer   ") or

        Addr = "Buffer   "
        CALL Where WITH Addr

        Note also that the syntax of Lowlevel accepts a colon to
separate register names only when referring to the address of an
additional argument.  Colons cannot be used when passing an address
itself.  To pass the address 6000:0002 to Lowlevel for the DS:DX
registers, we have to use:

        DS=6000,DX=0002

        since DS:DX=6000:0002 or anything similar will cause a syntax
error.

        In the process of using Where or Peek, the first argument will
be overwritten by the result returned. This can be annoying if the
argument was an address now forgotten and overwritten by the contents.
Putting these commands into user-defined functions is likely to reduce
the frustration index considerably.

