The QBNews Page 27 Volume 3, Number 1 March 29, 1992 ---------------------------------------------------------------------- Q B A S I C C o r n e r ---------------------------------------------------------------------- Extending the QBasic Interpreter by Brent Ashley With the widespread acceptance of MS-DOS 5.0, many people are discovering and rediscovering the power of modern, structured BASIC. This is evidenced especially in the various BBS network programming echoes, where very often a reply is now prefaced with: "Well, if by QBasic you mean the one that came with DOS 5.0 then I'm afraid that can't be done; but if you mean the QuickBASIC compiler, well then you simply..." QBasic (the interpreter) differs from QB (the QuickBASIC compiler) in a few ways, but mostly in its extensibility, or more to the point, lack of it. Keen QBasic programmers soon run up against the language's limitations, mostly as regards system-level programming. While the obvious solution would be to buy QuickBASIC for its access to DOS and BIOS interrupts and its easy extensibility via code libraries, in many situations this is impractical. Perhaps your company MIS department disallows language purchases by "common users" (I hear bells ringing in heads all over the continent as you read this!), or your personal budget doesn't allow for it yet. It would be best to use a company-sanctioned tool, or one you already have, if only it provided the functionality you needed. One advanced feature Microsoft "left in" the QBasic interpreter is the CALL ABSOLUTE statement. This statement allows you to call a machine-language routine at a known place in memory, and to pass it a list of arguments. At first glance it doesn't seem to be of much use, but with a little imagination, you can use this feature to extend the use of QBasic far into QB's realm. I have written three assembly-language routines which, together with their BASIC support routines, add the following to the QBasic arsenal: o DOS and BIOS Interrupt calls o Almost instant memory block copies o Fast colour single-line box drawing You will find these routines with ASM source, along with a demo program containing many sample QBasic support routines, in QBASIC.ZIP. Meanwhile, I'll explain here the methods I use to load and call binary routines and some of the further-reaching implications. Keep in mind that all of these routines work just as well with the QuickBASIC compiler, as long as the CALL ABSOLUTE routine from QB.LIB is available. I'll try to encapsulate the essence of loading and calling a BIN file from QBasic in a short example. This example program uses the MEMCOPY.BIN file to quickly copy a block of memory so screen saves can be done without PCOPY, which isn't supported by monochrome adapters. The QBNews Page 28 Volume 3, Number 1 March 29, 1992 The source is followed by a discussion of its innards. -------------- ASM Source --------------------------------------- ; MemCopy.ASM - by Brent Ashley ; Copies blocks of memory quickly ; - to be used primarily for screen saves. ; .MODEL medium, BASIC .CODE MemCopy PROC USES si di ds es, \ FromSeg:PTR WORD, FromOfs:PTR WORD, \ ToSeg:PTR WORD, ToOfs:PTR WORD, \ Count:PTR WORD ; load ds:si with source, es:di with destination mov bx,FromOfs mov si,[bx] mov bx,ToSeg mov es,[bx] mov bx,ToOfs mov di,[bx] mov bx,Count mov cx,[bx] ; ds last (used to access data) mov bx,FromSeg mov ds,[bx] ; do the copy cld rep movsb ret MemCopy ENDP END --------------- QBasic code -------------------------------------- DEFINT A-Z DECLARE FUNCTION LoadBin$ (BinFileName$) CLS FOR i = 1 TO 24 ' fill screen with letters PRINT STRING$(80, 64 + i); NEXT ScrnSave 1 ' save screen SLEEP 1 ' wait a second for user to see screen CLS PRINT "That screenful of letters has been erased." PRINT : PRINT "Press a key to restore screen..." DO: LOOP UNTIL LEN(INKEY$) ' wait for key ScrnSave 0 ' restore screen END SUB ScrnSave (SaveRestore) STATIC ' save/restore a text screen ' save = nonzero, restore = 0 STATIC InitDone, ScrnBuf, VidSeg ' ensures local scope IF NOT InitDone THEN The QBNews Page 29 Volume 3, Number 1 March 29, 1992 REDIM ScrnBuf(1 TO 2000) ' 4000 bytes (80x25x2) DEF SEG = 0 IF PEEK(&H463) = &HB4 THEN ' determine mon/colour VidSeg = &HB000 ' mono ELSE VidSeg = &HB800 ' colour END IF DEF SEG InitDone = -1 END IF IF SaveRestore THEN ' save BlockCopy VidSeg, 0, VARSEG(ScrnBuf(1)), VARPTR(ScrnBuf(1)), 4000 ELSE ' restore BlockCopy VARSEG(ScrnBuf(1)), VARPTR(ScrnBuf(1)), VidSeg, 0, 4000 END IF END SUB SUB BlockCopy (FromSeg, FromOfs, ToSeg, ToOfs, Count) ' copy a block of memory STATIC MemCopy$ ' ensure local scope IF NOT LEN(MemCopy$) THEN MemCopy$ = LoadBin("MemCopy.BIN") DEF SEG = VARSEG(MemCopy$) ' point to routine's segment CALL Absolute(FromSeg, FromOfs, ToSeg, ToOfs, Count, SADD(MemCopy$)) END SUB FUNCTION LoadBin$ (BinFileName$) ' Loads a binary file as a string STATIC FileNum, Buf$ FileNum = FREEFILE OPEN BinFileName$ FOR BINARY AS FileNum IF LOF(FileNum) = 0 THEN ' file didn't exist CLOSE FileNum KILL BinFileName$ CLS : PRINT "Can't find "; BinFileName$; " - aborting." END END IF Buf$ = SPACE$(LOF(FileNum)) ' size buffer GET FileNum, , Buf$ ' load BIN routine CLOSE #FileNum LoadBin$ = Buf$ END FUNCTION --------------------- end of example -------------------------------- The Assembly routine was written with Microsoft's QuickAssembler. I used some of its advanced segment directives and high-level language interface capabilities, but knowledgeable (read:masochistic) programmers could generate similar .BIN files with DEBUG as long as they were familiar with accessing stack parameters and cleaning up the stack on return. All it does is to load DS:SI and ES:DI with the source and destination and then copy the number of bytes specified by Count, which is in the range 0 to 65535. The routine is assembled and then converted to binary image with EXE2BIN. The resulting file is called MEMCOPY.BIN. The QBNews Page 30 Volume 3, Number 1 March 29, 1992 The QBasic code consists of a short main module which fills the screen, saves it, erases it, and restores it, using our routine. While only the ScrnSave procedure is called by the demo, ScrnSave relies on BlockCopy, which in turn relies on LoadBIN$. ScrnSave saves and restores the screen by reserving a block of memory to use as a buffer. This allows fast screen saves on monochrome adapters, which do not have extra video pages as do colour adapters. The first time the routine is called, it dimensions the buffer array and determines the adapter type and therefore the address of the screen page. Then, and on any subsequent calls, it copies the the screen contents between the buffer and screen memory, depending on the value of SaveRestore. The BlockCopy routine performs the copy requested by ScrnSave. On first pass, it loads the ASM routine from its .BIN file on disk into the string MemCopy$, which has been declared as STATIC. The STATIC declaration causes it to retain its value between calls ensures it isn't affected by any module-level variable of the same name which may have been declared with the SHARED attribute. It then uses the CALL ABSOLUTE statement to call the BIN routine. This is done by setting BASIC's current DEF SEG to the routine's segment and then passing the address of the routine to run, along with the parameters to pass. It is interesting to note that CALL ABSOLUTE takes a variable number of parameters, depending on the requirements of your routine. Note that SADD is used to determine the address of the routine, since it is stored in a string variable. LoadBin$ is a general-purpose function to load these .BIN files from disk into a string. It assumes the file is in the current directory or that the path is specified in BinFileName$. The file is loaded into a string which is then returned by the function, to be called with CALL ABSOLUTE by your QBasic-level support routine. Using the methods demonstrated in this example, it is possible to extend QBasic's power as far as your assembly skills allow. It is even possible to write support routines which would allow the creation and use of BIN libraries. I had contemplated doing this but decided that interest wouldn't be high enough due to the availability of the QuickBASIC compiler, and the fact that multiple BIN files in a "library" directory would be simple enough to implement under the scheme outlined above. There are some details and implications of the BIN routine methods described above which aren't readily apparent. Firstly, in exploring the CALL ABSOLUTE assembly interface, one will soon discover that FUNCTIONs are not readily supported. The simple solution to this will serve to exemplify an important aspect of QBasic/BIN programming - the QBasic "front-end" routine. We met the front-end routine in our example program, in the guise of the BlockCopy routine. This routine served to insulate our main The QBNews Page 31 Volume 3, Number 1 March 29, 1992 program from the details of loading and calling the BIN file. It was called with only the parameters relevant to the task at hand, and in turn called the ABSOLUTE routine with the additional BIN-file specific parameters. Since the front-end file takes care of some of the dirty details of BIN file calls in easy-to-code-and-maintain QBasic, we can simplify the assembly routine, often the hardest to write and most difficult to maintain part of the hybrid program. Take the example of a routine which uses the BIOS to print a character. In QBasic, we would like to pass to the routine the character, its positional row and column on the screen, and the foreground and background colours in which to print the character. Our calls to this routine might look like this: BIOSPrint Char$, Row%, Col%, Fore%, Back% Anyone who has written assembly routines for BASIC will attest to the hassles involved in accessing BASIC's dynamic strings. It would be nice to write the routine to receive the ASCII value of the character instead as an integer. Furthermore, the BIOS routines use 0-based screen positioning, as opposed to the 1-based positioning used in QBasic. To insulate the user from confusion, the routine should adjust QBasic row and column values to BIOS values, and even build them into a single integer to be loaded into a register for the BIOS call. Lastly, the BIOS routine will want a single integer colour attribute, meaning you will have to program the conversion routine if the separate colours are sent. Really, then, the BIN routine really wants the parameters sent like this: CALL ABSOLUTE(AsciiValue%, BIOSPosition%, Attribute%, BINAddress%) Your front-end routine, then, can take care of all of these conversions, thus minimising the assembly code so it performs only that which QBasic can't do for itself: SUB BIOSPrint (Char$, Row%, Col%, Fore%, Back%) STATIC BINFile$ IF BINFile$ = "" THEN BINFile$ = LoadBinFile("BIOSPRT.BIN") AsciiValue% = ASC(Char$) BIOSPosition% = (Row% - 1) * 256 + (Col% - 1) Attribute% = (Fore% AND 16) * 8 + (Back% AND 7) * 16 + (Fore% AND 16) DEF SEG VARSEG(BINFile$) CALL ABSOLUTE(AsciiValue%, BIOSPosition%, Attribute%, SADD(BINFile$)) END SUB The front-end routine also answers the FUNCTION problem. Here is the code for a fictitious CurDrive function which would act identically to the one in the QBIN demo program: FUNCTION CurDrive% STATIC BinFile$ ' returns logged drive (a=1, b=2, etc) The QBNews Page 32 Volume 3, Number 1 March 29, 1992 IF BinFile$ = "" THEN BINFile$ = LoadBinFile("CurDrv.BIN") AXReg% = 0 DEF SEG VARSEG(BinFile$) CALL ABSOLUTE(AXReg%, SADD(BinFile$)) CurDrive% = AXReg% MOD 256 + 1 END FUNCTION Since you can't return values to CALL ABSOLUTE in the same way you would when writing assembly FUNCTIONs for compiled QuickBASIC, you have to provide your assembly routine with references to variables to be changed and then use the front-end routine to pass them to the caller via a FUNCTION. The second point I'd like to make is more far-reaching and has much more potential for power - the use of strings containing BIN files as parameters to your BASIC routines. Anyone who has used Nantucket's Clipper 5.x product will know the power of "code blocks". These are sections of code which can be assigned to a variable and passed between routines like data. Actually, the code is static in memory and the routines receive references, but knowledge of these details is not necessary. In this way (among others), the Clipper implementation differs from that of C and PASCAL, for instance, where you use "pointers to functions", which demand that you dwell on some of these details directly. Let's say you want to write a generic sort routine. You want it to support ascending and descending sorts, as well as sorting starting at a certain column of the input strings. The normal way to do this would be to provide a passed argument specifying which of the hard-wired methods to use, for example: CALL SortArray ( Array$(), TypeOfSort% ) Deeper inside the routine, you would have code similar to: SUB SortArray (... ... SELECT CASE TypeOfSort% ' choose hard-wired method CASE 1 ' ascending IF Array$(i) < Array$(j) THEN SWAP Array$(i), Array$(j) CASE 2 ' descending IF Array$(i) > Array$(j) THEN SWAP Array$(i), Array$(j) CASE 3 ' sort from column 5 ascending IF MID$(Array$(i),5) < MID$(Array$(j),5) THEN SWAP ... ...etc If you were to write a series of ASM routines, however, each of which took two string parameters, performed a comparison according to your latest whim, and returned a true-or-false value, then saved these as BIN files, you could simplify your routine to: Compare$ = LoadBINFile( "MyAsmFn.BIN" ) The QBNews Page 33 Volume 3, Number 1 March 29, 1992 CALL SortArray ( Array$(), Compare$ ) SUB SortArray (... ... IF CallBINFunc(Compare$, Array$(i), Array$(j)) THEN SWAP ... You and your users can now write ASM modules yourselves to your specs and extend the functionality of your routine without touching its innards. You could also use this method to send a user-defined filter condition to your display routine, or to tell an event handling routine what to do when it is triggered - all completely extensible without recompiling your main code or even having access to source! Unfortunately, the kind of flexibility that would really make these methods shine (access to QBasic variables, definition of code blocks in QBasic itself, pointers to functions...) isn't available with external BIN files. Perhaps Microsoft has been thinking of something like this for the future? I would be interested in anyone's ideas on how to expand on these concepts. I can be found lurking about many QuickBASIC conferences on the various BBS networks, including Fido's QUIK_BAS echo, ILink, RelayNet (RIME), and NorthAmeriNet (NANET). Feel free to drop by and say hello. SOURCE CODE FOR THIS ARTICLE CAN BE FOUND IN QBASIC.ZIP ====================================================================== Brent Ashley is a Technical Support Analyst for the Ontario Government, troubleshooting a province-wide data communications network and providing hardware and software support to a 24-hour micro/mainframe helpdesk operation. He can be reached in care of this newsletter. ======================================================================