PC Basic
PC Basic
TABLE OF CONTENTS INTRODUCTION Part I: UNDER THE HOOD Chapter 1. An Introduction to Compiled BASIC Chapter 2. Variables and Constant Data Chapter 3. Programming Methods Part II: HANDS-ON PROGRAMMING Chapter Chapter Chapter Chapter Chapter 4. 5. 6. 7. 8. Debugging Strategies Compiling and Linking File and Device Handling Database and Network Programming Sorting and Searching
Part II: BEYOND BASIC Chapter Chapter Chapter Chapter 9. 10. 11. 12. Program Optimization Key Memory Areas in the PC Accessing DOS and BIOS Services Assembly Language Programming
-2 -
ACKNOWLEDGEMENTS Many people helped me during the preparation of this book. First and foremost I want to thank my publisher, Cindy Hudson, for her outstanding support and encouragement, and for making it all happen. I also want to thank "black belt" editor Deborah Craig for a truly outstanding job. Never had I seen someone reduce a sentence from 24 words to less than half that, and improve the meaning in the process. [Unfortunately, readers of this disk version are unable to benefit from Deborah's excellent work.] Several other people deserve praise as well: Don Malin for his programming advice, and for eliminating all my GOTO statements. Jonathan Zuck for his contribution on database and network programming, including all of the dBASE file access routines. Paul Passarelli for unraveling the mysteries of floating point emulation, and for sharing that expertise with me. Philip Martin Valley for his research and examples showing how to read Lotus 1-2-3 binary files. Jim Mack for his skillful proof-reading of my manuscript, and countless good ideas. My wife Elli for her support and encouragement during the eight long months it took to write this book. ABOUT THE AUTHOR Ethan Winer is the founder of Crescent Software, Inc. located Ridgefield Connecticut. He has programmed in BASIC and assembly language since 1980, and is the author of Crescent's QuickPak Professional and P.D.Q. products. Ethan has also served as a contributing editor for both PC Magazine and BASICPro (now Visual Basic Programmer's Journal), and has written numerous feature articles for other popular computer publications. In 1992 Ethan retired from writing software professionally, and now spends his free time writing and performing music. PREFACE INTRODUCTION ============ BASIC has always been the most popular language for personal computers. It is easy to learn and use, extremely powerful, and some form of BASIC is included for free with nearly every PC. Although BASIC is often associated with beginners and students, it is in fact ideally suited for a wide range of programming projects. Because it offers the best features of a highlevel language coupled with direct access to DOS and BIOS system services, BASIC is fast becoming the language of choice for beginners and professional developers alike. This book is about power programming using Microsoft compiled BASIC. It is intended for people who already possess a fundamental understanding of BASIC programming concepts, but want to achieve the best performance possible from their BASIC compiler. Power programming is knowing when and how to use BASIC commands such as CALL INTERRUPT, VARSEG and VARPTR, and even PEEK and POKE effectively. It
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book -3 -
involves understanding the PC's memory organization sufficiently to determine how much stack space is needed for a recursive subprogram or function. A power programmer knows how to translate a time-critical portion of a BASIC program into assembly language when needed. Finally, and perhaps most importantly, power programming means knowing enough about BASIC's internal operation to determine which sequence of instructions is smaller or faster than another. This book will show you how to go beyond creating programs that merely work. Because it explains how the compiler operates and how it interacts with the BASIC runtime language library, this book will teach you how to write programs that are as small and fast as possible. Although the emphasis here is on Microsoft QuickBASIC and the BASIC Professional Development System (PDS), much of the information will apply to other BASIC compilers such as Power Basic from Spectra Publishing. Despite what you may have read, BASIC is the most capable and easy to learn of the high-level languages. Modern BASIC compilers are highly optimizing, and can thus create extremely efficient executable programs. In addition, you can often achieve with just a few BASIC statements what would take many pages of code in another high-level language. Moreover, beginners can be immediately productive in BASIC, while serious programmers have a wealth of powerful capabilities at their disposal. Microsoft BASIC has many capabilities that are not available in any other high-level language. Among these are dynamic (variable-length) strings, automatic memory allocation and heap management, built-in support for sophisticated graphics, and interrupt-driven communications. Add to that huge arrays, network file handling, music and sound, and protection against inadvertently overwriting memory, and you can see why BASIC is so popular. This book aims to provide intermediate to advanced programmers with information that is not available elsewhere. It does not, however, cover elementary topics such as navigating the QuickBASIC editor, loading and saving files, or using the Search and Replace feature. That information is readily available elsewhere. Rather, it delves into previously uncharted territory, and examines compiled BASIC at its innermost layer. Besides the discussions and programs in the text, this book includes a companion disk [separate ZIP file] that contains all of the subroutines and other code listed in this book, including several useful utilities. Installing these programs is described in the Appendix. CONVENTIONS USED IN THIS BOOK ============================= This book uses the terms QuickBASIC and QB to mean the Microsoft QuickBASIC 4.x and 7.x editing environments. BC and Compiler indicate the [Link] command-line compiler that comes with QuickBASIC, Microsoft BASIC PDS, and the now-discontinued BASIC 6.0. When a distinction is necessary, QBX will refer to the QuickBASIC Extended editor that comes with the BASIC Professional Development System (PDS). In most cases, the discussions will be the same for all of these versions of BASIC. When a difference does occur, the PDS and QBX exceptions will be indicated. [Because there is no way to indicate italics in a disk file, where they would have been used for emphasis or clarity the words are instead surrounded by asterisks (*).] HOW THIS BOOK IS ORGANIZED ========================== This book is divided into parts, and each part contains several chapters that discuss a specific aspect of BASIC programming. You needn't fully understand an entire chapter before moving on to the next one. Each topic will be covered in great depth, and in many cases you will want to return to a given chapter as your knowledge and understanding of the subject matter increases.
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book -4 -
Part 1 is "Under the Hood," and its three chapters describe in detail how your BASIC source code is manipulated throughout the compiling and linking process. Chapter 1 presents an overview of compilers in general, and BASIC compilers in particular, It discusses what BASIC compilers are all about and how they work, and how the compiled code that is generated interacts with routines in the runtime libraries. Chapter 2 discusses variables, constants, and other program data, and how they fit within the context of the PC's memory organization. This chapter also covers bit manipulation using AND, OR, and XOR. Chapter 3 examines the various control flow methods available in BASIC, showing which statements and procedure constructs are appropriate in different situations. In particular, you will learn the relative advantages and disadvantages of each method, based on their capabilities, code size, and speed. Part 2, "Programming Hands On," examines programming techniques, and shows specific examples of writing effective code and also making it work. Chapter 4 explores program debugging using the facilities built into the QuickBASIC editing environment, as well as the CodeView utility that comes with Microsoft BASIC PDS. This chapter also discusses common programming problems, along with the appropriate solutions. Chapter 5 explains compiling and linking, both from within the QB environment, and directly from DOS. A number of compiler options are inadequately documented by Microsoft, and each is discussed here in great detail. A thorough discussion of the [Link] utility program included with BASIC explains how libraries are manipulated and organized. Chapter 6 covers all aspects of file and device handling, and discusses the many different ways in which data may be read and written. The emphasis here is on speeding file handling as much as possible, and storing data on disk efficiently. Because input/output (I/O) devices are accessed similarly, they too are described here in detail. Chapter 7 explains the basics of writing database and network applications, and discusses file locking strategies using practical programming examples. A series of subroutines show how to read and write files using the popular dBASE format, and these may be incorporated into program that you write. Chapter 9 shows how to sort and search array data as quickly as possible. Several methods are examined including conventional and indexed sorting, and many useful subroutines are presented. The final part, "Beyond BASIC," includes information that is rarely covered in books about BASIC. Its three chapters go far beyond the information provided in any of the Microsoft manuals. Chapter 10 identifies many of the key memory areas in the PC, and shows when and how they can be manipulated in a BASIC program. Chapter 11 presents an in-depth discussion of accessing DOS and BIOS services using CALL INTERRUPT. These services offer a wealth of functionality that BASIC cannot otherwise provide directly. Chapter 12 is an introduction to assembly language, from a BASIC programmer's perspective. This chapter presents many useful subroutines, and includes a thorough discussion of how they work. Finally, the Appendix describes the additional source files that accompany this book.
A BRIEF HISTORY OF MICROSOFT COMPILED BASIC =========================================== In March of 1982, IBM released the first BASIC compiler for the IBM PC. This compiler, BASCOM 1.0, was written by Microsoft for IBM using code and methods developed by Bill Gates, Greg Whitten, and others. Although Microsoft had already written BASIC compilers for the Apple II and CP/M computers, BASCOM 1.0 was the most powerful they had produced so far. Compared to the Microsoft BASIC interpreters available at that time, BASCOM 1.0 offered many additional capabilities, and also an enormous
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book -5 -
increase in program execution speed. Line numbers were no longer mandatory, program statements could exceed 255 characters, and a single string could be as long as 32,767 characters. Further, assembly language subroutines could be linked directly to a compiled BASIC application. Over the next few years, Microsoft continued to enhance the compiler, and in 1985 it was released by IBM as BASCOM 2.0. This version offered many improvements over the older BASCOM 1.0. Among the most important were multi-line DEF FN functions, dynamic arrays, descriptive line labels (as opposed to numbers), network record locking, and an ISAM file handler. With named subroutines programmers were finally able to exceed the 64K code size limitation, by writing separate modules that could then be linked together. The inclusion of subroutine parameters--long overdue for BASIC-was an equally important step toward fostering structured programming techniques in the language. At the same time that IBM released BASCOM 2.0, Microsoft offered essentially the same product as QuickBASIC 1.0, but without the ISAM file handler. However, there was one other big difference between these compilers: QuickBASIC 1.0 carried a list price of only $99. This low price was perhaps the most important feature of all, because high-performance BASIC was finally available to everyone, and not just professional developers. Encouraged by the tremendous acceptance of QuickBASIC 1.0, Microsoft quickly followed that with QuickBASIC version 2.0 in early 1986. This important new release added an integrated editing environment, as well as EGA graphics capabilities. The editor was especially welcome, because it allowed programs to be developed and tested very rapidly. The environment was further enhanced with the advent of Quick Libraries, which allowed assembly language subroutines to be easily added to a BASIC program. Quick Libraries also helped launch the start of a new class of BASIC product: third-party add-on libraries. In early 1987 Microsoft released the next major enhancement to QuickBASIC, version 3.0. QuickBASIC 3.0 included a limited form of step and trace debugging, as well as the ability to monitor a variable's value continuously during program execution. Also added was support for the EGA's 43-line text mode, and several new language features. Perhaps most impressive of the new features was the control flow statements DO and LOOP, and SELECT CASE. Beyond merely providing a useful alternative to the IF statement, these constructs also let the compiler generate more efficient code. Also added with version 3.0 was optional support for an 8087 numeric coprocessor. In order to support a coprocessor, however, Microsoft had to abandon their own proprietary numeric format. Both the Microsoft and IEEE methods for storing single- and double precision numbers use four bytes and eight bytes respectively, but the bits are organized differently. Although the IEEE format which the 8087 requires is substantially slower than Microsoft's own, it is the current standard. Therefore, a second version of the compiler was included solely to support IEEE math. By the time QuickBASIC 4.0 was announced in late 1987, hundreds of thousands of copies of QuickBASIC were already in use world-wide. With QuickBASIC 4.0, Microsoft had created the most sophisticated programming environment ever seen in a main-stream language: the threaded p-code interpreter. This remarkable technology allowed programmers to enjoy the best features of an interpreted language, but with the execution speed of a compiler. In addition to an Immediate mode whereby program statements could be executed one by one, QuickBASIC 4.0 also supported program break-points, monitoring the value of multiple variables and expressions, and even stepping *backwards* through a program. This greatly enhanced the debugging capabilities of the language, and increased programmer productivity enormously. Also new in QuickBASIC 4.0 was support for inter-language calling. Although this meant that a program written in Microsoft BASIC could now call subroutines written in any of the other Microsoft languages, it also meant that IEEE math was no longer an option--it became mandatory. When
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book -6 -
a QuickBASIC 4.0 program was run on a PC equipped with a coprocessor, floating point math was performed very quickly indeed. However, it was very much slower on every other computer! This remained a sore point for many BASIC programmers, until Microsoft introduced BASIC 6.0 later that year. That version included an alternate math library that was similar to their original proprietary format. Also added in QuickBASIC 4.0 were huge arrays, long (4-byte) integer variables, user-defined TYPE variables, fixed-length strings, true functions, and support for CodeView debugging. With the introduction of huge arrays, BASIC programmers could create arrays that exceeded 64K in size, with only a few restrictions. TYPE variables let the programmer define a composite data type comprised of any mix of BASIC's intrinsic data forms, thus adding structure to a program's data as well as to its code. The newly added FUNCTION procedures greatly improved on BASIC's earlier DEF FN-style functions by allowing recursion, the passing of TYPE variables and entire arrays as arguments, and the ability to modify an incoming parameter. Although BASIC 6.0 provided essentially the same environment and compiler as QuickBASIC 4.0, it also included the ability to create programs that could be run under OS/2. Other features of this release were a utility program to create custom run-time libraries, and a copy of the Microsoft Programmer's Editor. The custom run-time utility was particularly valuable, since it allowed programmers to combine frequentlyused subroutines with the [Link] language library, and then share those routines among any number of chained modules. QuickBASIC 4.5 was introduced in 1988, although the only major enhancement over the earlier 4.0 version was a new help system and slightly improved pull-down menus. Unfortunately, the new menus required much more memory than QuickBASIC 4.0, and the "improved" environment reduced the memory available for programs and data by approximately 40K. To this day, many programmers continue to use QuickBASIC 4.0 precisely because of its increased program capacity. In answer to programmer's demands for more string memory and smaller, more efficient programs, Microsoft released the BASIC Professional Development System version 7.0 in late 1989. This was an enormous project even for a company the size of Microsoft, and at one point more than fifty programmers were working on the new compiler and QBX environment. PDS version 7.0 finally let BASIC programmers exceed the usual 64K string memory limit, albeit with some limitations. Other features introduced with that version were an ISAM file handler, improved library granularity, example tool box packages for creating simple graphics and pull-down menus, local error handling, arrays within TYPE variables, and greatly improved documentation. Because the QBX editor uses expanded memory to store subprograms and functions, much larger programs could be developed without resorting to editing and compiling outside of the environment. Sixth months later PDS version 7.1 was released, with the long-overdue ability to redimension an array but without destroying its contents. Also added in that version were support for passing fixed-length string arrays to subprograms and functions, and an option to pass parameters by value to BASIC procedures. Although the BYVAL option had been available since QuickBASIC 4.0, it was useable only with subroutines written in non-BASIC languages. With this mechanism, BASIC can now create more efficient object code than ever before. [Just as this book was being completed, Microsoft released Visual Basic for DOS. Although this book does not address VB/DOS specifically, most of the information about BASIC PDS applies to VB/DOS. One notable exception is that VB/DOS supports far strings only, where BASIC PDS lets you specify either near strings or far. Because far strings are stored in a separate "far" area of DOS memory, it takes slightly longer to access those strings. Therefore, a VB/DOS program that is string-intensive will not be as fast as an equivalent compiled with QuickBASIC or with PDS near strings. This book also does not cover the pseudo event-driven forms used by VB/DOS.]
-7 -
README-File Notes on this disk version of "BASIC Techniques and Utilities" ENTIRE CONTENTS OF THIS TEXT AND SOFTWARE COPYRIGHT (C) 1994 ETHAN WINER This is a disk version of "PC Magazine BASIC Techniques and Utilities", which was originally published by Ziff-Davis Press in Emeryville, CA. When Ziff-Davis Press decided it was no longer profitable for them to continue printing it, they returned the rights to me. This disk version of my book is provided free as a service to the programming community. You are welcome to use any of the code fragments or complete programs in any way you see fit for no charge, including for commercial applications. However, the author retains all copyrights for the text and the programs. You may share this book and the accompanying programs with others, but only if you distribute the entire [Link] file as it was originally uploaded by me to CompuServe. While I should not have to belabor the obvious: All of this software and the accompanying text are provided "as is", with no warranty expressed or implied. The author is not liable for any damages whatsoever, including incidental or consequential. Use this information at your own risk. If you wipe out your hard disk or CMOS memory, I am not responsible! Although this book is provided at no charge, I hope I will be allowed one small commercial plug: If you find this information useful and would like to learn more about BASIC and assembly language programming, please considering purchasing QuickPak Professional and/or P.D.Q. from Crescent Software. A brief advertisement for Crescent describing their products for DOS BASIC is in the [Link] file. The text is divided into individual chapter files rather than one huge file, to make it easier to locate information in each chapter. The text you see here is what I sent to the publisher, and does not include any editing for style they applied. You may print this book by copying the chapter files to a printer from a DOS prompt using the COPY command: COPY CHAP*.TXT LPT1. Or you may view it using any ASCII file browsing program such as Vern Buerg's LIST utility. Where appropriate, the CHR$(12) hard page feeds were retained before and after long program listings, to aid print formatting. these will appear as the universal Female symbol when viewed with LIST. There was no easy way to create a page index for a book supplied as text files, but the included TEXTFIND utility will help you locate information in the text. TEXTFIND accepts a file specification and search string, and then searches all files that match that specification for the string. So to determine which CHAP*.TXT files mention, say, DEF SEG, you would start TEXTFIND like this: TEXTFIND CHAP*.TXT and then enter "DEF SEG" (without the quotes) at the prompt. Note that TEXTFIND searches without regard to capitalization in either the search string or the file's text, so entering "def seg" would also work. I have also included a version of this program called [Link] (find text), which is essentially the same program but compiled with Crescent's P.D.Q. add-on library. If you look at the size of this program (4956 bytes) and compare that with what you get after compiling and linking TEXTFIND with VB/DOS (46698 bytes), you can see the enormous improvement that P.D.Q. offers. In some cases, figures from the printed book could not be included. In the printed book Chapter 6 contains a picture of a floppy disk showing how the sectors and clusters are organized. And in Chapter 4 there are some
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book -8 -
figures of CodeView display screens that were originally created as .GIF file graphics-mode screen shots. I have tried to recreate as many of the other figures as possible with standard and extended ASCII characters. If your printer does not support extended characters (those with ASCII values greater than 127), figures that contain lines and boxes may print as rows of italicized letters and numbers. You will notice a few comments here and there that were added to this disk version of my book only, and they are enclosed in square brackets: [] Some of these comments relate to VB/DOS, which was not covered in the original printed version. Others were added as I read the book one last time before uploading it, to clarify or enhance the information herein. But since I do not use VB/DOS on a regular basis, I can't guarantee that all of the VB/DOS differences and features are documented completely. In most cases, however, the information about BASIC PDS applies equally to VB/DOS. Also notice how the individual sections within each chapter are delineated. Most printed books identify the different levels of section headings with different fonts and type styles. For example, major section headings are often printed in bold capitalized text; smaller, less-bold fonts are used for the lower section levels. This disk version of my book uses uppercase "underlined" text for major section headings, plain uppercase for the next lower level, and mixed case for the lowest heading levels. I will happily provide support for this book and answer questions as time permits in sections 13 and 14 of the MSBASIC forum on CompuServe. My CIS account number is 72241,63. I prefer to answer questions there rather than through EMAIL, because public messages let others benefit from the answers. -- End of [Link] Ethan Winer
CHAPTER 1 AN INTRODUCTION TO COMPILED BASIC This chapter explores the internal workings of the BASIC compiler. Many people view a compiler simply as a "black box" which magically transforms BASIC source files into executable code. Of course, magic does not play a part in any computer program, and the BC compiler that comes with Microsoft BASIC is no exception. It is merely a program that processes data in the same way any other program would. In this case, the data is your BASIC source code. You will learn here what the BASIC compiler does, and how it does it. You will also get an inside glimpse at some of the decisions a compiler must make, as it transforms your code into the assembly language commands the CPU will execute. By truly understanding the compiler's role, you will be able to exploit its strengths and also avoid its weaknesses. COMPILER FUNDAMENTALS ===================== No matter what language a program is written in, at some point it must be translated into the binary codes that the PC's processor can understand. Unlike BASIC commands, the CPU within every PC is capable of acting on only very rudimentary instructions. Some typical examples of these instructions are "Add 3 to the value stored in memory location 100", and "Compare the value stored at address 4012 to the number -12 and jump to the code at address 2015 if it is less". Therefore, one very important value of a high-level language such as BASIC is that a programmer can use meaningful names instead of memory addresses when referring to variables and subroutines. Another is the ability to perform complex actions that
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book -9 -
require many separate small steps using only one or two statements. As an example, when you use the command PRINT X% in a program, the value of X% must first be converted from its native two-byte binary format into an ASCII string suitable for display. Next, the current cursor location must be determined, at which point the characters in the string are placed into the screen's memory area. Further, the cursor position has to be updated, to place it just past the digits that were printed. Finally, if the last digit happened to end up at the bottom-right corner of the screen, the display must also be scrolled up a line. As you can see, that's an awful lot of activity for such a seemingly simple statement! A compiler, then, is a program that translates these English-like BASIC source statements into the many separate and tiny steps the microprocessor requires. The BASIC compiler has four major responsibilities, as shown in Figure 1-1 below. 1. Translate BASIC statements into an equivalent series of assembly language commands. 2. Assign addresses in memory to hold each of the variables being used by the program. 3. Remember the addresses in the generated code where each line number or label occurs, for GOTO and GOSUB statements. 4. Generate additional code to test for events and detect errors when the /v, /w, or /d compile options are used. Figure 1-1: The primary actions performed by a BASIC compiler. As the compiler processes a program's source code, it translates only the most basic statements directly into assembly language. For other, more complex statements, it instead generates calls to routines in the BASIC run-time library that is supplied with your compiler. When designing a BASIC program you would most likely identify operations that need to be performed more than once, and then create subprograms or functions rather than add the same code in-line repeatedly. Likewise, the compiler takes advantage of the inherent efficiency of using called subroutines. For example, when you use a BASIC statement such as PRINT Work$, the compiler processes it as if you had used CALL PRINT(Work$). That is, PRINT really is a called subroutine. Similarly, when you write OPEN FileName$ FOR RANDOM AS #1 LEN = 1024, the compiler treats that as a call to its Open routine, and it creates code identical to CALL OPEN(FileName$, 1, 1024, 4). Here, the first argument is the file name, the second is the file number you specified, the third is the record length, and the value 4 is BASIC's internal code for RANDOM. Because these are BASIC key words, the CALL statement is of course not required. But the end result is identical. While the BC compiler could certainly create code to print the string or open the file directly, that would be much less efficient than using subroutines. Indeed, all of the subroutines in the Microsoft-supplied libraries are written in assembly language for the smallest size and highest possible performance. DATA STORAGE The second important job the compiler must perform is to identify all of the variables and other data your program is using, and allocate space for them in the object file. There are two kinds of data that are manipulated in a BASIC program--static data and dynamic data. The term static data refers to any variable whose address and size does not change during the execution of a program. That is, all simple numeric and TYPE variables, and static numeric and TYPE arrays. String constants such as "Press a key to continue" and DATA items are also considered to be static data, since
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 10 -
their contents never change. Dynamic data is that which changes in size or location when the program runs. One example of dynamic data is a dynamic array, because space to hold its contents is allocated when the program runs. Another is string data, which is constantly moved around in memory as new strings are assigned and old ones are erased. Variable and array storage is discussed in depth in Chapter 2, so I won't belabor that now. The goal here is simply to introduce the concept of variable storage. The important point is that BC deals only with static data, because that must be placed into the object file. As the compiler processes your source code, it must remember each variable that is encountered, and allocate space in the object file to hold it. Further, all of this data must be able to fit into a single 64K segment, which is called DGROUP (for Data Group). Although the compiled code in each object file may be as large as 64K, static data is combined from all of the files in a multi-module program, and may not exceed 64K in total size. Note that this limitation is inherent in the design of the Intel microprocessors, and has nothing to do with BC, LINK, or DOS. As each new variable is encountered, room to hold it is placed into the next available data address in the object file. (In truth, the compiler retains all variable information in memory, and writes it to the end of the file all at once following the generated code.) For each integer variable, two bytes are set aside. Long integer and single precision variables require four bytes each, while double precision variables occupy eight bytes. Fixed-length string and TYPE variables use a varying number of bytes, depending on the components you have defined. Static numeric and TYPE arrays are also written to the object file by the compiler. The number of bytes that are written of course depends on how many elements have been specified in the DIM statement. Also, notice that no matter what type of variable or array is encountered, only zeroes are written to the file. The only exceptions are quoted string constants and DATA items, in which case the actual text must be stored. Unlike numeric, TYPE, and fixed-length variables, strings must be handled somewhat differently. For each string variable a program uses, a four-byte table called a *string descriptor* is placed into the object file. However, since the actual string data is not assigned until the program is run, space for that data need not be handled by the compiler. With string arrays--whether static or dynamic--a table of four-byte descriptors is allocated. Finally, each array in the program also requires an array descriptor. This is simply a table that shows where the array's data is located in memory, how many elements it currently holds, the length in bytes of each element, and so forth. ASSEMBLY LANGUAGE CONSIDERATIONS In order to fully appreciate how the translation process operates, you will first need to understand what assembly language is all about. Please understand that there is nothing inherently difficult about assembly language. Like BASIC, assembly language is comprised of individual instructions that are executed in sequence. However, each of these instructions does much less than a typical BASIC statement. Therefore, many more steps are required to achieve a given result than in a high-level language. Some of these steps will be shown in the following examples. If you are not comfortable with the idea of tackling assembly language concepts just yet, please feel free to come back to this section at a later time. Let's begin by examining some very simple BASIC statements, and see how they are translated by the compiler. For simplicity, I will show only integer math operations. The 80x86 family of microprocessors can manipulate integer values directly, as opposed to single and double precision numbers which are much more complex. The short code fragment in Listing 1-1 shows some very simple BASIC instructions, along with the resulting compiled assembly code. In case you are interested,
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 11 -
disassemblies such as those you are about to see are easy to create for yourself using the Microsoft CodeView utility. CodeView is included with the Macro Assembler as well as with BASIC PDS. A% = 12 MOV WORD PTR [A%],12 X% = X% + 1 INC WORD PTR [X%] Y% = Y% + 100 ADD WORD PTR [Y%],100 Z% = A% MOV ADD MOV + B% AX,WORD PTR [B%] AX,WORD PTR [A%] WORD PTR [Z%],AX
;move a 12 into the word variable A% ;add 1 to the word variable X% ;add 100 to the word variable Y% ;move the contents of B% into AX ;add to that the value of A% ;move the result into Z%
Listing 1-1: These short examples show the compiled results of some simple BASIC math operations. The first statement, A% = 12, is directly translated to its assembler equivalent. Here, the value 12 is *moved* into the word-sized address named A%. Although an integer is the smallest data type supported by BASIC, the microprocessor can in fact deal with variables as small as one byte. Therefore, the WORD PTR (word pointer) argument is needed to specify that A% is a full two-byte integer, rather than a single byte. Notice that in assembly language, brackets are used to specify the contents of a memory address. This is not unlike BASIC's PEEK() function, where parentheses are used for that purpose. In the second statement, X% = X% + 1, the compiler generates assembly language code to increment, or add 1 to, the word-sized variable in the location named X%. Since adding or subtracting a value of 1 is such a common operation in all programming languages, the designers of the 80x86 included the INC (and complementary DEC) instruction to handle that. Y% = Y% + 100 is similarly translated, but in this case to assembler code that adds the value 100 to the word-sized variable at address Y%. As you can see, the simple BASIC statements shown thus far have a direct assembly language equivalent. Therefore, the code that BC creates is extremely efficient, and in fact could not be improved upon even by a human hand-coding those statements in assembly language. The last statement, Z% = A% + B%, is only slightly more complicated than the others. This is because separate steps are required to retrieve the contents of one memory location, before manipulating it and assigning the result to another location. Here, the value held in variable B% is moved into one of the processor's registers (AX). The value of variable A% is then added to AX, and finally the result is moved into Z%. There are about a dozen registers within the CPU, and you can think of them as special variables that can be accessed very quickly. The next example in Listing 1-2 shows how BASIC passes arguments to its internal routines, in this case PRINT and OPEN. Whenever a variable is passed to a routine, what is actually sent is the address (memory location) of the variable. This way, the routine can go to that address, and read the value that is stored there. As in Listing 1-1, the BASIC source code is shown along with the resultant compiler-generated assembler instructions. It may also be worth mentioning that the order in which the arguments are sent to these routines is determined by how the routines are designed. In BASIC, if a SUB is designed to accept, say, three parameters in a certain order, then the caller must pass its arguments in that same order. Parameters in assembler routines are handled in exactly the same manner. Of course, any arbitrary order could be used, and what's important is simply that they match.
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 12 -
PRINT Work$ MOV AX,OFFSET Work$ PUSH AX CALL B$PESD OPEN FileName$ FOR OUTPUT AS MOV AX,OFFSET FileName$ PUSH AX MOV AX,1 PUSH AX MOV AX,-1 PUSH AX MOV AX,2 PUSH AX CALL B$OPEN
;move the address of Work$ into AX ;push that onto the CPU stack ;call the string printing routine #1 ;load the address of FileName$ ;push that onto the stack ;load the specified file number ;and push that as well ;-1 means that a LEN= was not given ;and push that ;2 is the internal code for OUTPUT ;pass that on too ;finally, call the OPEN routine
Listing 1-2: Many BASIC statements create assembler code that passes arguments to internal routines, as shown above. When you tell BASIC to print a string, it first loads the address of the string into AX, and then pushes that onto the stack. The stack is a special area in memory that all programs can access, and it is often used in compiled languages to hold the arguments being sent to subroutines. In this case, the OFFSET operator tells the CPU to obtain the address where the variable resides, as opposed to the current contents of the variable. Notice that the words offset, address, and memory location all mean the same thing. Also notice that calls in assembly language work exactly the same as calls in BASIC. When the called routine has finished, execution in the main program resumes with the next statement in sequence. Once the address for Work$ has been pushed, BASIC's B$PESD routine is called. Internally, one of the first things that B$PESD does is to retrieve the incoming address from the stack. This way it can locate the characters that are to be printed. B$PESD is responsible for printing strings, and other BASIC library routines are provided to print each type of data such as integers and single precision values. In case you are interested, PESD stands for Print End-of-line String Descriptor. Had a semicolon been used in the print statement--that is, PRINT Work$;--then B$PSSD would have been called instead (Print Semicolon String Descriptor). Likewise, printing a 4-byte long integer with a trailing comma as in PRINT Value&, would result in a call to B$PCI4 (Print Comma Integer 4), where the 4 indicates the integer's size in bytes. In the second example of Listing 1-2 the OPEN routine is set up and called in a similar fashion, except that four parameters are required instead of only one. Again, each parameter is pushed onto the stack in turn, followed by a call to the routine. Most of BASIC's internal routines begin with the characters "B$", to avoid a conflict with subroutines of your own. Since a dollar sign is illegal in a BASIC procedure name, there is no chance that you will inadvertently choose one of the same names that BASIC uses. As you can see, there is nothing mysterious or even difficult about assembly language, or the translations performed by the BASIC compiler. However, a sequence of many small steps is often needed to perform even simple calculations and assignments. We will discuss assembly language in much greater depth in Chapter 14, and my purpose here is merely to present the underlying concepts. Please note that variable names are not retained after a program has been compiled. Once BC has finished its job, all references to each variable name have been replaced with an equivalent memory addresses in the object file. Further, once LINK has joined the object files and linked them to the BASIC language libraries, the procedure names are lost as well. These issues will be explored in much greater detail in Chapter 14.
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 13 -
COMPILER DIRECTIVES As you have seen, some code is translated by the compiler into the equivalent assembly language statements, while other code is instead converted to calls to the language routines in the BASIC libraries. Some statements, however, are not translated at all. Rather, they are known as *compiler directives* that merely provide information to the compiler as it works. Some examples of these non-executable BASIC statements include DEFINT, OPTION BASE, and REM, as well as the various "metacommands" such as '$INCLUDE and '$DYNAMIC. Some others are SHARED, BYVAL, DATA, DECLARE, CONST, and TYPE. For our purposes here, it is important to understand that DIM when used on a static array is also a non-executable statement. Because the size of the array is known when the program is compiled, BC can simply set aside memory in the object file to hold the array contents. Therefore, code does not need to be generated to actually create the array. Similarly, TYPE/END TYPE statements also merely define a given number of bytes that will ultimately end up in the program file when the TYPE variable is later dimensioned by your program. EVENT AND ERROR CHECKING The last compiler responsibility I will discuss here is the generation of additional code to test for events and debugging errors. This occurs whenever a program is compiled using the /d, /w, or /v command line switches. Although event trapping and debugging are entirely separate issues, they are handled in a similar manner. Let's start with event trapping. When the IBM PC was first introduced, the ability to handle interruptdriven events distinguished it from its then-current Apple and Commodore counterparts. Interrupts can provide an enormous advantage over polling methods, since polling requires a program to check constantly for, say, keyboard or communications activity. With polling, a program must periodically examine the keyboard using INKEY$, to determine if a key was pressed. But when interrupts are used, the program can simply go about its business, confident that any keystrokes will be processed. Here's how that works: Each time a key is pressed on a PC, the keyboard generates a hardware interrupt that suspends whatever is currently happening and then calls a routine in the ROM BIOS. That routine in turn reads the character from the keyboard's output port, places it into the PC's keyboard buffer, and returns to the interrupted application. The next time a program looks for a keystroke, that key is already waiting to be read. For example, a program could begin writing a huge multi-megabyte disk file, and any keystrokes will still be handled even if the operator continues to type. Understand that hardware interrupts are made possible by a direct physical connection between the keyboard circuitry and the PC's microprocessor. The use of interrupts is a powerful concept, and one which is important to understand. Unfortunately, BASIC does not use interrupts in most cases, and this discussion is presented solely in the interest of completeness. Event Trapping BASIC provides a number of event handling statements that perhaps *could* be handled via interrupts, but aren't. When you use ON TIMER, for example, code is added to periodically call a central event handler to check if the number of seconds specified has elapsed. Because there are so many possible event traps that could be active at one time, it would be unreasonable to expect BASIC to set up separate interrupts to handle each possibility. In some situations, such as ON KEY, there is a corresponding interrupt. In this case, the keyboard interrupt. However, some events
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 14 -
such as ON PLAY(Count), where a GOSUB is made whenever the PLAY buffer has fewer than Count characters remaining, have no corresponding physical interrupt. Therefore, polling for that condition is the only reasonable method. The example in Listing 1-3 shows what happens when you compile using the /v switch. Notice that the calls to B$EVCK (Event Check) are not part of the original source code. Rather, they show the additional code that BC places just before each program statement. DEFINT A-Z CALL B$EVCK 'this call is generated ON TIMER(1) GOSUB HandleTime CALL B$EVCK 'this call is generated TIMER ON CALL B$EVCK 'this call is generated X = 10 CALL B$EVCK 'this call is generated Y = 100 CALL B$EVCK 'this call is generated END HandleTime: CALL B$EVCK BEEP CALL B$EVCK RETURN
by BC by BC by BC by BC by BC
Listing 1-3: When the /v compiler switch is used, BC generates calls to a central event handler at each BASIC statement. At five bytes per call, you can see that using /v can quickly bloat a program to an unacceptable size. One alternative is to instead use /w. In fact, /w can be particularly attractive in those cases where event handling cannot be avoided, because it lets you specify where a call to B$EVCK is made: at each line label or line number in your source code. The only downside to using line numbers and labels is that additional working memory is needed by BC to remember the addresses in the code where those labels are placed. This is not usually a problem, though, unless the program is very large or every line is labeled. All of the various BASIC event handling commands are specified using the ON statement. It is important to understand, however, that ON GOTO and ON GOSUB do not involve events. That is, they are really just an alternate form of GOTO and GOSUB respectively, and thus do not require compiling with /w or /v. Error Trapping The last compiler option to consider here is the /d switch, because it too generates extra code that you might not otherwise be aware of. When a program is compiled with /d, two things are added. First, for every BASIC statement a call is made to a routine named B$LINA, which merely checks to see if Ctrl-Break has been pressed. Normally, a compiled BASIC program is immune to pressing the Ctrl-C and Ctrl-Break keys, except during an INPUT or LINE INPUT statement. Since much of the purpose of a debugging mode is to let you break out of an errant program gone berserk, the Ctrl-Break checking must be performed frequently. These checks are handled in much the same way as event trapping, by calling a special routine once for each line in your source code. Another important factor resulting from the use of /d is that all array references are handled through a special called routine which ensures that the element number specified is in fact legal. Many people don't realize this, but when a program is compiled without /d and an invalid element is
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 15 -
given, BASIC will blindly write to the wrong memory locations. For example, if you use DIM Array%(1 TO 100) and then attempt to assign, say, element number 200, BASIC is glad to oblige. Of course, there *is* no element 200 in that case, and some other data will no doubt be overwritten in the process. To prevent these errors from going undetected, BC calls the B$HARY (Huge Array) routine to calculate the address based on the element number specified. If B$HARY determines that the array reference is out of bounds, it invokes an internal error handler and you receive the familiar "Subscript out of range" message. Normally, the compiler accesses array elements using as little code as possible, to achieve the highest possible performance. If a static array is dimensioned to 100 elements and you assign element 10, BC knows at the time it compiles your program the address at which that element resides. It can therefore access that element directly, just as if it were a non-array variable. Even when you use a variable to specify an array element such as Array%(X) = 12, the starting address of the array is known, and the value in X can be used to quickly calculate how far into the array that element is located. Therefore, the lack of bounds checking in programs that do not use /d is not a bug in BASIC. Rather, it is merely a trade-off to obtain very high performance. Indeed, one of the primary purposes of using /d is to let BC find mistakes in your programs during development, though at the cost of execution speed. The biggest complication from BASIC's point of view is when huge (greater than 64K) arrays are being manipulated. In fact, B$HARY is the very same routine that BC calls when you use the /ah switch to specify huge arrays (hence the name HARY). Since extra code is needed to set up and call B$HARY compared to the normal array access, using /ah also creates programs that are larger and slower than when it is not used. Further, because B$HARY is used by both /d and /ah, invalid element accesses will also be trapped when you compile using /ah. Overflow Errors The final result of using /d is that extra code is generated after certain math operations, to check for overflow errors that might otherwise go undetected. Overflow errors are those that result in a value too large for a given data type. For example, if you multiply two integers and the result exceeds 32767, that causes an overflow error. Similarly, an underflow error would be created by a calculation resulting a value that is too small. When a floating point math operation is performed, errors that result from overflow are detected by the routines that perform the calculation. When that happens there is no recourse other than halting your program with an appropriate message. Integer operations, however, are handled directly by 80x86 instructions. Further, an out of bounds result is not necessarily illegal to the CPU. Thus, programs compiled without the /d option can produce erroneous results, and without any indication that an error occurred. To prove this to yourself, compile and run the short program shown in Listing 1-4, but without using /d. Although the correct result should be 90000, the answer that is actually displayed is 24464. And you will notice that no error message is displayed! As with illegal array references, BC would rather optimize for speed, and give you the option of using /d as an aid for tracking down such errors as they occur. If you compile the program in Listing 1-4 with the /d option, then BASIC will report the error as expected. Since an overflow resulting from integer operations is not technically an error as far as the CPU is concerned, how, then, can BASIC trap for that? Although an error in the usual sense is not created, there is a special flag variable within the CPU that is set whenever such a condition occurs. Further, a little-used assembler instruction, INTO (Interrupt 4 if Overflow), will generate software Interrupt 4 if that flag is set. Therefore, all BC has to do is create an Interrupt 4 handler, and then
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 16 -
place an INTO instruction after every integer math operation in the compiled code. The interrupt handler will receive control and display an "Overflow" message whenever an INTO calls it. Since the INTO instruction is only one byte and is also very fast, using it this way results in very little size or performance degradation. X% = 30000 Y% = X% * 10 PRINT Y% Listing 1-4: This brief program illustrates how overflow errors are handled in BASIC. COMPILER OPTIMIZATION Designing a compiler for a language as complex as BASIC involves some very tricky programming indeed. Although it is one thing to translate a BASIC source file into a series of assembly language commands, it is another matter entirely to do it well! Consider that the compiler must be able to accept a BASIC statement such as X! = ABS(SQR((Y# + Z!) ^ VAL(Work$))), and reduce that to the individual steps necessary to arrive at the correct result. Many, many details must be accounted for and handled, not the least of which are syntax or other errors in the source code. Moreover, there are an infinite number of ways that a programmer can accomplish the same thing. Therefore, the compiler must be able to recognize many different programming patterns, and substitute efficient blocks of assembler code whenever it can. This is the role of an *optimizing compiler*. One important type of optimization is called *constant folding*. This means that as much math as possible is performed during compilation, rather than creating code to do that when the program runs. For example, if you have a statement such as X = 4 * Y * 3 BC can, and does, change that to X = Y * 12. After all, why multiply 3 times 4 later, when the answer can be determined now? This substitution is performed entirely by the BC compiler, without your knowing about it. Another important type of optimization is BASIC's ability to remember calculations it has already performed, and use the results again later if possible. BC is especially brilliant in this regard, and it can look ahead many lines in your source code for a repeated use of the same calculations. Listing 1-5 shows a short fragment of BASIC source code, along with the resultant assembler output. X% = 3 * MOV IMUL MOV Y% * 4 AX,12 WORD PTR [Y%] WORD PTR [X%],AX
;move the value 12 into AX ;Integer-Multiply that times Y% ;assign the result in AX to X% ;save the result from above in BX ;then assign AX to 100 ;now multiply AX times S% ;and assign A% from the result ;assign Z% from the earlier result
A% = S% * 100 MOV BX,AX MOV AX,100 IMUL WORD PTR [S%] MOV WORD PTR [A%],AX Z% = Y% * 12 MOV WORD PTR [Z%],BX
Listing 1-5: These short code fragments illustrate how adept BC is at reusing the result of earlier calculations already performed. As you can see in the first part of Listing 1-5, the value of 3 times 4 was resolved to 12 by the compiler. Code was then generated to multiply the
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 17 -
12 times Y%, and the result is in turn assigned to X%. This is similar to the compiled code examined earlier in Listing 1-1. Notice, however, that before the second multiplication of S% is performed, the result currently in AX is saved in the BX register. Although AX is destroyed by the subsequent multiplication of S% times 100, the result that was saved earlier in BX can be used to assign Z% later on. Also notice that even though 3 * 4 was used first, BC was smart enough to realize that this is the same as the 12 used later. While the compiler can actually look ahead in your source code as it works, such optimization will be thwarted by the presence of line numbers and labels, as well as IF blocks. Since a GOTO or GOSUB could jump to a labeled source line from anywhere in the program, there is no way for BC to be sure that earlier statements were executed in sequence. Likewise, the compiler has no way to know which path in an IF/ELSE block will be taken at run time, and thus cannot optimize across those statements. THE BASIC RUN-TIME LIBRARIES Microsoft compiled BASIC lets you create two fundamentally different types of programs. Those that are entirely self-contained in one .EXE file are compiled with the /o command line switch. In this case, the compiler creates translations such as those we have already discussed, and also generates calls to the BASIC language routines contained in the library files supplied by Microsoft. When your compiled program is subsequently linked, only those routines that are actually used will be added to your program. When /o is not used, a completely different method is employed. In this case, a special .EXE file that contains support for every BASIC statement is loaded along with the BASIC program when the program is run from the DOS command line. As you are about to see, there are advantages and disadvantages to each method. For the purpose of this discussion I will refer to stand-alone programs as BCOM programs, after the [Link] library name used in all versions of QuickBASIC. Programs that instead require the [Link] library to be present at run time will be called BRUN programs. Beginning with BASIC 7 PDS, the library naming conventions used by Microsoft have become more obscure. This is because PDS includes a number of variations for each method, depending on the type of "math package" that is specified when compiling and whether you are compiling a program to run under DOS or OS/2. These variations will be discussed fully in Chapter 6, when we examine all of the possible options that each compiler version has to offer. But for now, we will consider only the two basic methods--BCOM and BRUN. The primary differences between these two types of programs are shown in Figure 1-2. 1. BCOM programs require less memory, run faster, and do not require the presence of the [Link] file when the program is run. 2. BRUN programs occupy less disk space, and also allow subsequent chaining to other programs that can share the common library code which is already resident. Chained-to programs also load quickly because the BRUN library is already in memory. Figure 1-2: A comparison of the fundamental differences between BCOM and BRUN programs. Stand-alone BCOM programs are always larger than an equivalent BRUN program because the library code for PRINT, INSTR, and so forth is included in the final .EXE file. However, less *memory* will be required when the program runs, since only the code that is really needed is loaded into the PC. Likewise, a BRUN program will take less disk space, because it contains only the compiled code. The actual routines to handle each BASIC
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 18 -
statements are stored in the [Link] library, and that library is loaded automatically when the main program is run from DOS. You might think that since a BRUN program is physically smaller on disk it will load faster, but this is not necessarily true. When you execute a BRUN program from the DOS command line, one of the first things it does is load the BRUN .EXE support file. Since this support file is fairly large, the overall load time will be much greater than the compiled BASIC program's file size would indicate. However, if the main program subsequently chains to another BASIC program, that program will load quickly because the BRUN file does not need to be loaded a second time. One other important difference between these two methods is the way that the BASIC language routines are accessed. When a BCOM program is compiled and linked, the necessary routines are called in the usual fashion. That is, the compiler generates code that calls the routines in the BCOM library directly. When the program is subsequently linked, the procedure names are translated by LINK into the equivalent memory addresses. That is, a call to PRINT is in effect translated from CALL B$PESD to CALL ####:####, where ####:#### is a segment and address. BRUN programs, on the other hand, instead use a system of interrupts to access the BASIC language routines. Since there is no way for LINK to know exactly where in memory the [Link] file will be ultimately loaded, the interrupt vector table located in low memory is used to hold the various routine addresses. Although many of these interrupt entries are used by the PC's system resources, many others are available. Again, I will defer a thorough treatment of call methods and interrupts until Chapter 14. But for now, suffice it to say that a direct call is slightly faster than an indirect call, where the address to be called must first be retrieved from a table. As an interesting aside, the routines in the [Link] file in fact modify the caller's code to perform a direct call, rather than an interrupt instruction. Therefore, the first time a given block of code is executed, it calls the run-time routines through an interrupt instruction. Thereafter, the address where the BRUN file has been loaded is known, and will be used the next time that same block of code is executed. In practice, however, this improves only code that lies within a FOR/NEXT, WHILE, or DO loop. Further, code that is executed only once will actually be much slower than in a BCOM program, because of the added selfmodification (the program changes itself) instructions. Notice that when BC compiles your program, it places the name of the appropriate library into the object file. The name BC uses depends on which compiler options were given. This way you don't have to specify the correct name manually, and LINK can read that name and act accordingly. Although QuickBASIC provides only two libraries--one for BCOM programs and one for BRUN--BASIC PDS offers a number of additional options. Each of these options requires the program to be linked with a different library. That is, there are both BRUN and BCOM libraries for use with OS/2, for near and far strings, and for using IEEE or Microsoft's alternate math libraries. Yet another library is provided for 8087-only operation. GRANULARITY Until now, we have examined only the actions and methods used by the BC compiler. However, the process of creating an .EXE file that can be run from the DOS command line is not complete until the compiled object file has been linked to the BASIC libraries. I stated earlier that when a stand-alone program is created using the /o switch, only those routines in the BCOM library that are actually needed will be added to the program. Unfortunately, that is not entirely accurate. While it is true that LINK is very smart and will bring in only those routines that are actually called, there is one catch. Imagine that you have written a BASIC program which is comprised of two separate modules. In one file is the main program that contains only inline code, and in the other are two BASIC subprograms. Even if the main program calls only one of those subprograms, both will be added when the
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 19 -
program is linked. That is, LINK can resolve routines to the source file level only, but cannot extract a single routine from an object module which contains multiple routines. Since an .LIB library file is merely a collection of separate object modules, all of the routines that reside in a given module will be added to a program, even if only one has been accessed. This property is called *granularity*, and it determines how finely LINK can remove routines from a library. In the case of the libraries supplied with BASIC, the determining factor is which assembly language routines were combined with which other routines in the same source file by the programmers at Microsoft. In QuickBASIC 4.5, for example, when a program uses the CLS statement, the routines that handle COLOR, CSRLIN, POS(0), LOCATE, and the function form of SCREEN are also added. This is true even if none of those other statements have been used. Fortunately, Microsoft has done much to improve this situation in BASIC PDS, but there is still room for improvement. In BASIC PDS, CLS is stored in a separate file, however POS(0), CSRLIN, and SCREEN are still together, as are COLOR and LOCATE. Obviously, Microsoft has their reasons for doing what they do, and I won't attempt to second guess their expertise here. The BASIC language libraries are extremely complex and contain many routines. (The QuickBASIC 4.5 [Link] file contains 1,485 separate assembler procedures.) With such an enormous number of assembly language source files to deal with, it no doubt makes a lot of sense to organize the related routines together. But it is worth mentioning that Crescent Software's P.D.Q. library can replace much of the functionality of the BCOM libraries, and with complete granularity. In fact, P.D.Q. can create working .EXE programs from BASIC source that are less than 800 bytes in size. SUMMARY ======= In this chapter, you learned about the process of compiling, and the kinds of decisions a sophisticated compiler such as Microsoft BASIC must make. In some cases, the BASIC compiler performs a direct translation of your BASIC source code into assembly language, and in others it creates calls to existing routines in the BCOM libraries. Besides creating the actual assembler code, BASIC must also allocate space for all of the data used in a program. You also learned some basics about assembly language, which will be covered in more detail in Chapter 13. However, examples in upcoming chapters will also use brief assembly language examples to show the relative efficiency of different coding styles. In Chapter 2, you will learn how variables and other data are stored in memory.
CHAPTER 2 VARIABLES AND DATA DATA BASICS =========== In Chapter 1 you examined the role of a compiler, and learned how it translates BASIC source code into the assembly language commands a PC requires. But no matter how important the compiler is when creating a final executable program, it is only half of the story. This chapter discusses the equally important other half: data. Indeed, some form of data is integral to the operation of every useful program you will ever write. Even a program that merely prints "Hello" to the display screen requires the data "Hello". Data comes in many shapes and sizes, starting with a single bit, continuing through eight-byte double precision variables, and extending all
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 20 -
the way to multi-megabyte disk files. In this chapter you will learn about the many types of data that are available to you, and how they are manipulated in a BASIC program. You will also learn how data is stored and assigned, and how BASIC's memory management routines operate. Compiled BASIC supports two fundamental types of data (numeric and string), two primary methods of storage (static and dynamic), and two kinds of memory allocation (near and far). Of course, the myriad of data types and methods is not present to confuse you. Rather, each is appropriate in certain situations. By fully understanding this complex subject, you will be able to write programs that operate as quickly as possible, and use the least amount of memory. I will discuss each of the following types of data: integer and floating point numeric data, fixed-length and dynamic (variable-length) string data, and user-defined TYPE variables. Besides variables which are identified by name, BASIC supports named constant data such as literal numbers and quoted strings. I will also present a complete comparison of the memory storage methods used by BASIC, to compare near versus far storage, and dynamic versus static allocation. It is important to understand that near storage refers to variables and other data that compete for the same 64K data space that is often referred to as Near Memory or Data Space. By contrast, far storage refers to the remaining memory in a PC, up to the 640K limit that DOS imposes. The distinction between dynamic and static allocation is also important to establish now. Dynamic data is allocated in whatever memory is available when a program runs, and it may be resized or erased as necessary. Static data, on the other hand, is created by the compiler and placed directly into the .EXE file. Therefore, the memory that holds static data may not be relinquished for other uses. Each type of data has its advantages and disadvantages, as does each storage method. To use an extreme example, you could store all numeric data in string variables if you really wanted to. But this would require using STR$ every time a value was to be assigned, and VAL whenever a calculation had to be made. Because STR$ and VAL are relatively slow, using strings this way will greatly reduce a program's performance. Further, storing numbers as ASCII digits can also be very wasteful of memory. That is, the double precision value 123456789.12345 requires fifteen bytes, as opposed to the usual eight. Much of BASIC's broad appeal is that it lets you do pretty much anything you choose, using the style of programming you prefer. But as the example above illustrates, selecting an appropriate data type can have a decided impact on a program's efficiency. With that in mind, let's examine each kind of data that can be used with BASIC, beginning with integers. INTEGERS AND LONG INTEGERS ========================== An integer is the smallest unit of numeric storage that BASIC supports, and it occupies two bytes of memory, or one "word". Although various tricks can be used to store single bytes in a one-character string, the integer remains the most compact data type that can be directly manipulated as a numeric value. Since the 80x86 microprocessor can operate on integers directly, using them in calculations will be faster and require less code than any other type of data. An integer can hold any whole number within the range of -32768 to 32767 inclusive, and it should be used in all situations where that range is sufficient. Indeed, the emphasis on using integers whenever possible will be a recurring theme throughout this book. When the range of integer values is not adequate in a given programming situation, a long integer should be used. Like the regular integer, long integers can accommodate whole numbers only. A long integer, however, occupies four bytes of memory, and can thus hold more information. This yields an allowable range of values that spans from -2147483648 through 2147483647 (approximately +/- 2.15 billion). Although the PC's processor cannot directly manipulate a long integer in most situations,
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 21 -
calculations using them will still be much faster and require less code when compared to floating point numbers. Regardless of which type of integer is being considered, the way they are stored in memory is very similar. That is, each integer is comprised of either two or four bytes, and each of those bytes contains eight bits. Since a bit can hold a value of either 0 or 1 only, you can see why a larger number of bits is needed to accommodate a wider range of values. Two bits are required to count up to three, three bits to count to seven, four bits to count to fifteen, and so forth. A single byte can hold any value between 0 and 255, however that same range can also be considered as spanning from -128 to 127. Similarly, an integer value can hold numbers that range from either 0 to 65535 or -32768 through 32767, depending on your perspective. When the range is considered to be 0 to 65535 the values are referred to as *unsigned*, because only positive values may be represented. BASIC does not, however, support unsigned integer values. Therefore, that same range is used in BASIC programs to represent values between -32768 and 32767. When integer numbers are considered as using this range they are called *signed*. If you compile and run the short program in the listing that follows, the transition from positive to negative numbers will show how BASIC treats values that exceed the integer range of 32767. Be sure not to use the /d debugging option, since that will cause an overflow error to be generated at the transition point. The BASIC environment performs the same checking as /d does, and it too will report an error before this program can run to completion. Number% = 32760 FOR X% = 1 TO 14 Number% = Number% + 1 PRINT Number%, NEXT Displayed result: 32761 32766 -32765 32762 32767 -32764 32763 -32768 -32763 32764 -32767 -32762 32765 -32766 -32761
As you can see, once an integer reaches 32767, adding 1 again causes the value to "wrap" around to -32768. When Number% is further incremented its value continues to rise as expected, but in this case by becoming "less negative". In order to appreciate why this happens you must understand how an integer is constructed from individual bits. I am not going to belabor binary number theory or other esoteric material, and the brief discussion that follows is presented solely in the interest of completeness. BITS 'N' BYTES ============== Sixteen bits are required to store an integer value. These bits are numbered 0 through 15, and the least significant bit is bit number 0. To help understand this terminology, consider the decimal number 1234. Here, 4 is the least significant digit, because it contributes the least value to the entire number. Similarly, 1 is the most significant portion, because it tells how many thousands there are, thus contributing the most to the total value. The binary numbers that a PC uses are structured in an identical manner. But instead of ones, tens, and hundreds, each binary digit represents the number of ones, twos, fours, eights, and so forth that comprise a given byte or word. To represent the range of values between 0 and 32767 requires fifteen bits, as does the range from -32768 to -1. When considered as signed numbers, the most significant bit is used to indicate which range is being
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 22 -
considered. This bit is therefore called the sign bit. Long integers use the same method except that four bytes are used, so the sign bit is kept in the highest position of the fourth byte. Selected portions of the successive range from 0 through -1 (or 65535) are shown in Table 2-1, to illustrate how binary counting operates. When counting with decimal numbers, once you reach 9 the number is wrapped around to 0, and then a 1 is placed in the next column. Since binary bits can count only to one, they wrap around much more frequently. The Hexadecimal equivalents are also shown in the table, since they too are related to binary numbering. That is, any Hex value whose most significant digit is 8 or higher is by definition negative. Signed Decimal ------0 1 2 3 4 . . 32765 32766 32767 -32768 -32767 -32766 . . -4 -3 -2 -1 0 Unsigned Decimal -------0 1 2 3 4 . . 32765 32767 32767 32768 32769 32770 . . 65531 65532 65533 65534 65535
Binary ------------------0000 0000 0000 0000 0000 0000 0000 0001 0000 0000 0000 0010 0000 0000 0000 0011 0000 0000 0000 0100 . . 0111 1111 1111 1101 0111 1111 1111 1110 0111 1111 1111 1111 1000 0000 0000 0000 1000 0000 0000 0001 1000 0000 0000 0010 . . 1111 1111 1111 1100 1111 1111 1111 1101 1111 1111 1111 1110 1111 1111 1111 1111 0000 0000 0000 0000
Hex ---0000 0001 0002 0003 0004 . . 7FFD 7FFE 7FFF 8000 8001 8002 . . FFFB FFFC FFFD FFFE FFFF
Table 2-1: When a signed integer is incremented past 32767, its value wraps around and becomes negative. MEMORY ADDRESSES AND POINTERS ============================= Before we can discuss such issues as variable and data storage, a few terms must be clarified. A memory address is a numbered location in which a given piece of data is said to reside. Addresses refer to places that exist in a PC's memory, and they are referenced by those numbers. Every PC has thousands of memory addresses in which both data and code instructions may be stored. A *pointer* is simply a variable that holds an address. Consider a single precision variable named Value that has been stored by the compiler at memory address 10. If another variable--let's call it Address%--is then assigned the value 10, Address% could be considered to be a pointer to Value. Pointer variables are the bread and butter of languages such as C and assembler, because data is often read and written by referring to one variable which in turn holds the address of another variable. Although BASIC shields you as the programmer from such details, pointers are in fact used internally by the BASIC language library routines. This method of using pointers is sometimes called indirection, because an additional, indirect step is needed to first go to one variable, get an address, and then go to that address to access the actual data. Now let's see how these memory issues affect a BASIC program.
- 23 -
INTEGER STORAGE =============== When a conventional two-byte integer is stored in the PC's memory, the lower byte is kept in the lower memory address. For example, if X% is said to reside at address 10, then the least significant byte is at address 10 and the most significant byte is at address 11. Likewise, a long integer stored at address 102 actually occupies addresses 102 through 105, with the least significant portion at the lowest address. This is shown graphically in Figure 2-1. +------------- X% ------------+ LSB MSB - ----------------------------------- - 1001101000101101 - ----------------------------------- - ^ ^ ^ +- Address 10 +- Address 11 +- Address 12 Figure 2-1: An integer is stored in two adjacent memory locations, with the Least Significant Byte at the lower address, and the Most Significant Byte at the higher. This arrangement certainly seems sensible, and it is. However, some people get confused when looking at a range of memory addresses being displayed, because the values in lower addresses are listed at the left and the higher address values are shown on the right. For example, the DEBUG utility that comes with DOS will display the Hex number ABCD as CD followed by AB. I mention this only because the order in which digits are displayed will become important when we discuss advanced debugging in Chapter 4. In case you are wondering, the compiler assigns addresses in the order in which variables are encountered. The first address used is generally 36 Hex, so in the program below the variables will be stored at addresses 36, 38, 3A, and then 3C. Hex numbering is used for these examples because that's the way DEBUG and CodeView report them. A% B% C% D% = = = = 1 2 3 4 'this 'this 'this 'this is is is is at at at at address address address address &H36 &H38 &H3A &H3C
FLOATING POINT VALUES ===================== Floating point variables and numbers are constructed in an entirely different manner than integers. Where integers and long integers simply use the entire two or four bytes to hold a single binary number, floating point data is divided into portions. The first portion is called the mantissa, and it holds the base value of the number. The second portion is the exponent, and it indicates to what power the mantissa must be raised to express the complete value. Like integers, a sign bit is used to show if the number is positive or negative. The structure of single precision values in both IEEE and the original proprietary Microsoft Binary Format (MBF) is shown in Figure 2-2. For IEEE numbers, the sign bit is in the most significant position, followed by eight exponent bits, which are in turn followed by 23 bits for the mantissa. Double precision IEEE values are structured similarly, except eleven bits are used for the exponent and 52 for the mantissa. Double precision MBF numbers use only eight bits for an exponent rather than eleven, trading a reduced absolute range for increased resolution. That is, there are fewer exponent bits than the IEEE method
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 24 -
uses, which means that extremely large and extremely small numbers cannot be represented. However, the additional mantissa bits offer more absolute digits of precision. The IEEE format: +-----------------------------------+ SEEEEEEEEMMMMMMMMMMMMMMMMMMMMMMM +-----------------------------------+ The MBF format: +-----------------------------------+ EEEEEEEESMMMMMMMMMMMMMMMMMMMMMMM +-----------------------------------+ Figure 2-2: A single precision value is comprised of a Sign bit, eight Exponent bits, and 23 bits to represent the Mantissa. Each letter shown here represents one bit, and the bytes on the left are at higher addresses. Notice that with IEEE numbers, the exponent spans a byte boundary. This undoubtedly contributes to the slow speed that results from using numbers in this format when a coprocessor is not present. Contrast that with Microsoft's MBF format in which the sign bit is placed between the exponent and mantissa. This allows direct access to the exponent with fewer assembler instructions, since the various bits don't have to be shifted around. The IEEE format is used in QuickBASIC 4.0 and later, and BASIC PDS unless the /fpa option is used. BASIC PDS uses the /fpa switch to specify an alternate math package which provides increased speed but with a slightly reduced accuracy. Although the /fpa format is in fact newer than the original MBF used in interpreted BASIC and QuickBASIC 2 and 3, it is not quite as fast. As was already mentioned, double precision data requires twice as many bytes as single precision. Further, due to the inherent complexity of the way floating point data is stored, an enormous amount of assembly language code is required to manipulate it. Common sense therefore indicates that you would use single precision variables whenever possible, and reserve double precision only for those cases where the added accuracy is truly necessary. Using either floating point variable type, however, is still very much slower than using integers and long integers. Worse, rounding errors are inevitable with any floating point method, as the following short program fragment illustrates. FOR X% = 1 TO 10000 Number! = Number! + 1.1 NEXT PRINT Number! Displayed result: 10999.52 Although the correct answer should be 11000, the result of adding 1.1 ten thousand times is incorrect by a small amount. If you are writing a program that computes, say, tax returns, even this small error will be unacceptable. Recognizing this problem, Microsoft developed a new Currency data type which was introduced with BASIC PDS version 7.0. The Currency data type is a cross between an integer and a floating point number. Like a double precision value, Currency data also uses eight bytes for storage. However, the numbers are stored in an integer format
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 25 -
with an implied scaling of 10000. That is, a binary value of 1 is used to represent the value .0001, and a binary value of 20000 is treated as a 2. This yields an absolute accuracy to four decimal places, which is more than sufficient for financial work. The absolute range of Currency data is plus or minus 9.22 times 10 ^ 14 ( 9.22E14 or 922,000,000,000,000.0000), which is very wide indeed. This type of storage is called Fixed-Point, because the number of decimal places is fixed (in this case at four places). Currency data offers the best compromise of all, since only whole numbers are represented and the fractional portion is implied. Further, since a separate exponent and mantissa are not used, calculations involving Currency data are extremely fast. In practice, a loop that adds a series of Currency variables will run about half as fast as the same loop using long integers. Since twice as many bytes must be manipulated, the net effect is an overall efficiency that is comparable to long integers. Compare that to double precision calculations, where manipulating the same eight bytes takes more than six times longer. As you have seen, there is a great deal more to "simple" numeric data than would appear initially. But this hardly begins to scratch the surface of data storage and manipulation in BASIC. We will continue our tour of BASIC's data types with conventional dynamic (variable-length) strings, before proceeding to fixed-length strings and TYPE variables. DYNAMIC STRINGS =============== One of the most important advantages that BASIC holds over all of the other popular high-level languages is its support for dynamic string data. In Pascal, for example, you must declare every string that your program will use, as well as its length, before the program can be compiled. If you determine during execution of the program that additional characters must be stored in a string, you're out of luck. Likewise, strings in C are treated internally as an array of single character bytes, and there is no graceful way to extend or shorten them. Specifying more characters than necessary will of course waste memory, and specifying too few will cause subsequent data to be overwritten. Since C performs virtually no error checking during program execution, assigning to a string that is not long enough will corrupt memory. And indeed, problems such as this cause untold grief for C programmers. Dynamic string memory handling is built into BASIC, and those routines are written in assembly language. BASIC is therefore extremely efficient and very fast in this regard. Since C is a high-level language, writing an equivalent memory manager in C would be quite slow and bulky by comparison. I feel it is important to point out BASIC's superiority over C in this regard, because C has an undeserved reputation for being a very fast and powerful language. Compiled BASIC implements dynamic strings with varying lengths by maintaining a *string descriptor* for each string. A string descriptor is simply a four-byte table that holds the current length of the string as well as its current address. The format for a BASIC string descriptor is shown in Figure 2-3. In QuickBASIC programs and BASIC PDS when far strings are not specified, all strings are stored in an area of memory called the *near heap*. The string data in this memory area is frequently shuffled around, as new strings are assigned and old ones are abandoned. +------+ Higher addresses 64 ^ +------ Address B2 ------ 00 +------ Length 0A _---------------- VARPTR(Work$) +------+
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 26 -
Figure 2-3: Each string in a QuickBASIC program has a corresponding string descriptor, which holds its current length and address. The string in this example has a length of ten characters (0A Hex) and its data is presently at address 25778 (64B2 Hex). The lower two bytes in a string descriptor together hold the current length of the string, and the second two bytes hold its address. The memory location at the bottom of Figure 2-3 is at the lowest address. The short program below shows how you could access a string by peeking at its descriptor. DEFINT A-Z Test$ = "BASIC Techniques and Utilities" Descr = VARPTR(Test$) Length = PEEK(Descr) + 256 * PEEK(Descr + 1) Addr = PEEK(Descr + 2) + 256 * PEEK(Descr + 3) PRINT "The length is"; Length PRINT "The address is"; Addr PRINT "The string contains "; FOR X = Addr TO Addr + Length - 1 PRINT CHR$(PEEK(X)); NEXT Displayed result: The length is 17 The address is 15646 (this will vary) The string contains BASIC Techniques and Utilities Each time a string is assigned or reassigned, memory in the heap is claimed and the string's descriptor is updated to reflect its new length and address. The old data is then marked as being abandoned, so the space it occupied may be reclaimed later on if it is needed. Since each assignment claims new memory, at some point the heap will become full. When this happens, BASIC shuffles all of the string data that is currently in use downward on top of the older, abandoned data. This heap compaction process is often referred to colorfully as *garbage collection*. In practice, there are two ways to avoid having BASIC claim new space for each string assignment--which takes time--and you should consider these when speed is paramount. One method is to use LSET or RSET, to insert new characters into an existing string. Although this cannot be used to make a string longer or shorter, it is very much faster than a straight assignment which invokes the memory management routines. The second method is to use the statement form of MID$, which is not quite as fast as LSET, but is more flexible. Microsoft BASIC performs some additional trickery as it manages the string data in a program. For example, whenever a string is assigned, an even number of bytes is always requested. Thus, if a five-character string is reassigned to one with six characters, the same space can be reused. Since claiming new memory requires a finite amount of time and also causes garbage collection periodically, this technique helps to speed up the string assignment process. For example, in a program that builds a string by adding new characters to the end in a loop, BASIC can reduce the number of times it must claim new memory to only every other assignment. Another advantage to always allocating an even number of bytes is that the 80286 and later microprocessors can copy two-byte words much faster than they can copy the equivalent number of bytes. This has an obvious advantage when long strings are being assigned.
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 27 -
In most cases, BASIC's use of string descriptors is much more efficient than the method used by C and other languages. In C, each string has an extra trailing CHR$(0) byte just to mark where it ends. While using a single byte is less wasteful than requiring a four-byte table, BASIC's method is many times faster. In C the entire string must be searched just to see how long it is, which takes time. Likewise, comparing and concatenating strings in C requires scanning both strings for the terminating zero character. The same operations in BASIC require but a single step to obtain the current length. Pascal uses a method that is similar to BASIC's, in that it remembers the current length of the string. The length is stored with the actual string data, in a byte just before the first character. Unfortunately, using a single byte limits the maximum length of a Pascal string to only 255 characters. And again, when a string is shortened in Pascal, the extra characters are not released for use by other data. But it is only fair to point out that Pascal's method is both fast and compact. And since strings in C and Pascal never move around in memory, garbage collection is not required. Although a BASIC string descriptor uses four bytes of additional memory beyond that needed for the actual data, this is only part of the story. An additional two bytes are needed to hold a special "variable" called a *back pointer*. A back pointer is an integer word that is stored in memory immediately before the actual string data, and it holds the address of the data's string descriptor. Thus, it is called a back pointer because it points back to the descriptor, as opposed to the descriptor which points to the data. Because of this back pointer, six additional bytes are actually needed to store each string, beyond the number of characters that it contains. For example, the statement Work$ = "BASIC" requires twelve bytes of data memory--five for the string itself, one more because an even number of bytes is always claimed, four for the descriptor, and two more for a back pointer. Every string that is defined in a program has a corresponding descriptor which is always present, however a back pointer is maintained only while the string has characters assigned to it. Therefore, when a string is erased the two bytes for its back pointer are also relinquished. I won't belabor this discussion of back pointers further, because understanding them is of little practical use. Suffice it to say that a back pointer helps speed up the heap compaction process. Since the address portion of the descriptor must be updated whenever the string data is moved, this pointer provides a fast link between the data being moved and its descriptor. By the way, the term "pointer" refers to any variable that holds a memory address, regardless of what language is being considered. FAR STRINGS IN BASIC PDS BASIC PDS offers an option to specify "far strings", whereby the string data is not stored in the same 64K memory area that holds most of a program's data. The method of storage used for far strings is of necessity much more complex than near strings, because both an address and a segment must be kept track of. Although Microsoft has made it clear that the structure of far string descriptors may change in the future, I would be remiss if this undocumented information were not revealed here. The following description is valid as of BASIC 7.1 [it is still valid for VB/DOS too]. For each far string in a program, a four-byte descriptor is maintained in near memory. The lower two bytes of the descriptor together hold the address of an integer variable that holds yet another address: that of the string length and data. The second pair of bytes also holds the address of a pointer, in this case a pointer to a variable that indicates the segment in which the string data resides. Thus, by retrieving the address and segment from the descriptor, you can locate the string's length and data, albeit with an extra level of indirection. It is interesting to note that when far strings are being used, the string's length is kept just before its data, much like the way Pascal
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 28 -
operates. Therefore, the address pointer holds the address of the length word which immediately precedes the actual string data. The short program that follows shows how to locate all of the components of a far string based on examining its descriptor and related pointers. Notice that long integers are used to avoid the possibility of an overflow error if the segment or addresses happen to be higher than 32767. This way you can run the program in the QBX [or VB/DOS] editing environment. Figure 2-4 in turn illustrates the relationship between the address and pointer information graphically. DEF FNPeekWord& (A&) FNPeekWord& = PEEK(A&) + 256& * PEEK(A& + 1) END DEF Work$ = "This is a test" DescAddr& = VARPTR(Work$) AddressPtr& = FNPeekWord&(DescAddr&) SegmentPtr& = FNPeekWord&(DescAddr& + 2) Segment& = FNPeekWord&(SegmentPtr&) DEF SEG = Segment& DataAddr& = FNPeekWord&(AddressPtr&) Length% = FNPeekWord&(DataAddr&) StrAddr& = DataAddr& + 2 PRINT PRINT PRINT PRINT PRINT "The descriptor address " The data segment " The length "The string data starts " And the string data is:"; DescAddr& is:"; Segment& is:"; Length% at:"; StrAddr& is: ";
FOR X& = StrAddr& TO StrAddr& + Length% - 1 PRINT CHR$(PEEK(X&)); NEXT Displayed result (the addresses may vary): The descriptor address The data segment The length The string data starts And the string data is: is: is: at: is: 17220 40787 14 106 This is a test hold the segment, address, and length values, combine the results. This is the purpose of defined at the start of the program. Note after the number 256, which ensures that the an overflow error. I will discuss such use identifiers later in this chapter.
Because two bytes are used to we must PEEK both of them and the PeekWord function that is the placement of an ampersand multiplication will not cause of numeric constants and type
+---------------------- The string length +----------- The string data +--------------------+ +-->0A00This is a test<-- Segment &H8F00 +--------------------+ ^ +------- 8F00:0070 +--------------------+ +---7000..............<-- Segment &H8F00 +--------------------+
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 29 -
^ +------- 8F00:002E This is the "near" segment ------+ +-------------------+ +----------------------------------------+ 008F.........2E00D403............ +----------------------------------------+ ^ ^ +---+ +- Address 03D4 +-------+-- VARPTR(Work$) +-----------------------+ Figure 2-4: A far string descriptor holds the addresses of other addresses, in this case addresses that hold a far string's segment and its length and actual data. Even in a far-string program, some of the string data will be near. For example, DATA items and quoted string constants are stored in the same 64K DGROUP data segment that holds simple numeric and TYPE variables. The same "indirect" method is used, whereby you must look in one place to get the address of another address. In this case, however, the "far" segment that is reported is simply the normal near data segment. [DATA items in VB/DOS programs are still kept in near memory, but quoted strings are now kept in a separate segment.] One final complication worth mentioning is that strings within a FIELD buffer (and possibly in other special situations) are handled slightly differently. Since all of the strings in a FIELD buffer must be contiguous, BASIC cannot store the length word adjacent to the string data. Therefore, a different method must be used. This case is indicated by setting the sign bit (the highest bit) in the length word as a flag. Since no string can have a negative length, that bit can safely be used for this purpose. When a string is stored using this alternate method, the bytes that follow the length word are used as additional pointers to the string's actual data segment and address. FIXED-LENGTH STRINGS One of the most important new features Microsoft added beginning with QuickBASIC 4.0 was fixed-length string and TYPE variables. Although fixedlength strings are less flexible than conventional BASIC strings, they offer many advantages in certain programming situations. One advantage is that they are static, which means their data does not move around in memory as with conventional strings. You can therefore obtain the address of a fixed-length string just once using VARPTR, confident that this address will never change. With dynamic strings, SADD must be used each time the address is needed, which takes time and adds code. Another important feature is that arrays of fixed-length strings can be stored in far memory, outside of the normal 64K data area. We will discuss near and far array memory allocation momentarily. With every advantage, however, comes a disadvantage. The most severe limitation is that when a fixed-length string is used where a conventional string is expected, BASIC must generate code to create a temporary dynamic string, and then copy the data to it. That is, all of BASIC's internal routines that operate on strings expect a string descriptor. Therefore, when you print a fixed-length string, or use MID$ or INSTR or indeed nearly any statement or function that accepts a string, it must be copied to a form that BASIC's internal routines can accept. In many cases, additional code is created to delete the temporary string afterward. In others, the data remains until the next time the same BASIC statement is executed, and a new temporary string is assigned freeing the older one. To illustrate, twenty bytes of assembly language code are required to
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 30 -
print a fixed-length string, compared to only nine for a conventional dynamic string. Worse, when a fixed-length string is passed as an argument to a subprogram or function, BASIC not only makes a copy before passing the string, but it also copies the data back again in case the subroutine changed it! The extra steps the compiler performs are shown as BASIC equivalents in the listing that follows. '----- This is the code you write: DIM Work AS STRING * 20 CALL TestSub(Work$) '----- This is what BASIC actually does: Temp$ = SPACE$(20) LSET Temp$ = Work$ CALL TestSub(Temp$) LSET Work$ = Temp$ Temp$ = "" 'create a temporary string 'copy Work$ to it 'call the subprogram 'copy the data back again 'erase the temporary data
As you can imagine, all of this copying creates an enormous amount of additional code in your programs. Where only nine bytes are required to pass a conventional string to a subprogram, 64 are needed when a fixedlength string is being sent. But you cannot assume unequivocally that conventional strings are always better or that fixed-length strings are always better. Rather, I can only present the facts, and let you decide based on the knowledge of what is really happening. In the discussion of debugging later in Chapter 4, you will learn how to use CodeView to see the code that BASIC generates. You can thus explore these issues further, and draw your own conclusions. Arrays Within Types As I mentioned earlier, the TYPE variable is an important and powerful addition to modern compiled BASIC. Its primary purpose is to let programmers create composite data structures using any combination of native data types. C and Pascal have had such user-defined data types since their inception, and they are called Structures and Records respectively in each language. One immediately obvious use for being able to create a new, composite data type is to define the structure of a random access data file. Another is to simulate an array comprised of varied types of data. Obviously, no language can support a mix of different data types within a single array. That is, an array cannot be created where some of the elements are, say, integer while others are double precision. But a TYPE variable lets you do something very close to that, and you can even create arrays of TYPE variables. In the listing that follows a TYPE is defined using a mix of integer, single precision, double precision, and fixed-length string components. Also shown below is how a TYPE variable is dimensioned, and how each of its components are assigned and referenced. TYPE MyType I AS INTEGER S AS SINGLE D AS DOUBLE F AS STRING * 20 END TYPE DIM MyData as MyType
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 31 -
= = = =
'assign the integer portion 'and then the single part 'and then the double 'and finally the string 'now print the string
PRINT MyData.F
Once the TYPE structure has been established, the DIM statement must be used to create an actual variable using that arrangement. Although DIM is usually associated with the definition of arrays, it is also used to identify a variable name with a particular type of data. In this case, DIM tells BASIC to set aside an area of memory to hold that many bytes. You may also use DIM with conventional variable types. For example, DIM LastName AS STRING or DIM PayPeriod AS DOUBLE lets you omit the dollar sign and pound sign when you reference them later in the program. In my opinion, however, that style leads to programs that are difficult to maintain, since many pages later in the source listing you may not remember what type of data is actually being referred to. As you can see, a period is needed to indicate which portion of the TYPE variable is being referenced. The base name is that given when you dimensioned the variable, but the portion being referenced is identified using the name within the original TYPE definition. You cannot print a TYPE variable directly, but must instead print each component separately. Likewise, assignments to a TYPE variable must also be made through its individual components, with two exceptions. You may assign an entire TYPE variable from another identical TYPE directly, or from a dissimilar TYPE variable using LSET. For example, if we had used DIM MyData AS MyType and then DIM HisData AS MyType, the entire contents of HisData could be assigned to MyData using the statement MyData = HisData. Had HisData been dimensioned using a different TYPE definition, then LSET would be required. That is, LSET MyData = HisData will copy as many characters from HisData as will fit into MyData, and then pad the remainder, if any, with blanks. It is important to understand that this behavior can cause strange results indeed. Since CHR$(32) blanks are used to pad what remains in the TYPE variable being assigned, numeric components may receive some unusual values. Therefore, you should assign differing TYPE variables only when those overlapping portions being assigned are structured identically. Arrays Within Types With the introduction of BASIC PDS, programmers may also establish static arrays within a single TYPE definition. An array is dimensioned within a TYPE as shown in the listing that follows. As with a conventional DIM statement for an array, the number of elements are indicated and a non-zero lower bound may optionally be specified. Please understand, though, that you cannot use a variable for the number of elements in the array. That is, using PayHistory(1 TO NumDates) would be illegal. TYPE ArrayType AmountDue AS SINGLE PayHistory(1 TO 52) AS SINGLE LastName AS STRING * 15 END TYPE DIM TypeArray AS ArrayType There are several advantages to using an array within a TYPE variable. One is that you can reference a portion of the TYPE by using a variable to specify the element number. For example, [Link](PayPeriod) = 344.95 will assign the value 344.95 to element number PayPeriod. Without
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 32 -
the ability to use an array, each of the 52 components would need to be identified by name. Further, arrays allows you to define a large number of TYPE elements with a single program statement. This can help to improve a program's readability. STATIC VS. DYNAMIC DATA ======================= Preceding sections have touched only briefly on the concept of static and dynamic memory storage. Let's now explore this subject in depth, and learn which methods are most appropriate in which situations. By definition, static data is that which never changes in size, and never moves around in memory. In compiled BASIC this definition is further extended to mean all data that is stored in the 64K near memory area known as DGROUP. This includes all numeric variables, fixed-length strings, and TYPE variables. Technically speaking, the string descriptors that accompany each conventional (not fixed-length) string are also considered to be static, even though the string data itself is not. The string descriptors that comprise a dynamic string array, however, are dynamic data, because they move around in memory (as a group) and may be resized and erased. Numeric arrays that are dimensioned with constant (not variable) subscripts are also static, unless the '$DYNAMIC metacommand has been used in a preceding program statement. That is, DIM Array#(0 TO 100) will create a static array, while DIM Array#(0 TO MaxElements) creates a dynamic array. Likewise, arrays of fixed-length strings and TYPE variables will be static, as long as numbers are used to specify the size. There are advantages and disadvantages to each storage method. Access to static data is always faster than access to dynamic data, because the compiler knows the address where the data resides at the time it creates your program. It can therefore create assembly language instructions that go directly to that address. In contrast, dynamic data always requires a pointer to hold the current address of the data. An extra step is therefore needed to first get the data address from that pointer, before access to the actual data is possible. Static data is also in the near data segment, thus avoiding the need for additional code that switches segments. The overwhelming disadvantage of static data, though, is that it may never be erased. Once a static variable or array has been used in a program, the memory it occupies can never be released for other uses. Again, it is impossible to state that static arrays are always better than dynamic arrays or vice versa. Which you use must be dictated by your program's memory requirements, when compared to its execution speed. DYNAMIC ARRAYS You have already seen how dynamic strings operate, by using a four-byte pointer table called a string descriptor. Similarly, a dynamic array also needs a table to show where the array data is located, how many elements there are, the length of each element, and so forth. This table is called an array descriptor, and it is structured as shown in Table 2-2. There is little reason to use the information in an array descriptor in a BASIC program, and indeed, BASIC provides no direct way to access it anyway. But when writing routines in assembly language for use with BASIC, this knowledge can be quite helpful. As with BASIC PDS far string descriptors, none of this information is documented, and relying on it is most certainly not endorsed by Microsoft. Perhaps that's what makes it so much fun to discuss! Technically speaking, only dynamic arrays require an array descriptor, since static arrays do not move or change size. But BASIC creates an array descriptor for every array, so only one method of code generation is necessary. For example, when you pass an entire array to a subprogram using empty parentheses, it is the address of the array descriptor that is
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 33 -
actually sent. The subprogram can then access the data through that descriptor, regardless of whether the array is static or dynamic. Offset -----00 00 04 06 08 09 Size ---02 02 02 02 01 01 Description ---------------------------------Address where array data begins Segment where that address resides Far heap descriptor, pointer Far heap descriptor, block size Number of dimensions in the array Array Bit Bit Bit Bit type and storage method: 0 set = far array 1 set = huge (/ah) array 6 set = static array 7 set = string array
0A 0C 0E 10 12 14 . .
02 02 02 02 02 02 02 02
Adjusted Offset Length in bytes of each element Number of elements in the last dimension (UBOUND - LBOUND + 1) First element number in that dimension (LBOUND) Number of elements in the second from last dimension First element number in that dimension Repeat number of elements and first element number as necessary, through the first dimension
Table 2-2: Every array in a BASIC program has an associated array descriptor such as the one shown here. This descriptor contains important information about the array. The first four bytes together hold the segmented address where the array data proper begins in memory. Following the standard convention, the address is stored in the lower word, with the segment immediately following. The next two words comprise the Far Heap Descriptor, which holds a pointer to the next dynamic array descriptor and the current size of the array. For static arrays both of these entries are zero. When multiple dynamic arrays are used in a program, the array descriptors are created in static DGROUP memory in the order BC encounters them. The Far Heap Pointer in the first array therefore points to the next array descriptor in memory. The last descriptor in the chain can be identified because it points to a word that holds a value of zero. The block size portion of the Far Heap Descriptor holds the size of the array, using a byte count for string arrays and a "paragraph" count for numeric, fixed-length, and TYPE arrays. For string arrays--whether near or far--the byte count is based on the four bytes that each descriptor occupies. With numeric arrays the size is instead the number of 16-byte paragraphs that are needed to store the array.
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 34 -
The next entry is a single byte that holds the number of dimensions in the array. That is, DIM Array(1 TO 10) has one dimension and DIM Array(1 TO 10, 2 TO 20) has two. The next item is also a byte, and it is called the Feature byte because the various bits it holds tell what type of array it is. As shown in the table, separate bits are used to indicate if the array is stored in far memory, whether or not /ah was used to specify huge arrays, if the array is static, and if it is a string array. Multiple bits are used for each of these array properties, since they may be active in combination. However, BASIC never sets the far and huge bits for string arrays, even when the PDS /fs option is used and the strings are in fact in far memory. Of particular interest is the Adjusted Offset entry. Even though the segmented address where the array data begins is the first entry in the descriptor, it is useful only when the first element number in the array is zero. This would be the case with DIM Array(0 TO N), or simply DIM Array(N). To achieve the fastest performance possible when retrieving or assigning a given element, the Adjusted Offset is calculated when the array is dimensioned to compensate for an LBOUND other than 0. For example, if an integer array is dimensioned starting at element 1, the Adjusted Offset is set to point two bytes before the actual starting address of the data. This way, the compiler can take the specified element number, multiply that times two (each element comprises two bytes), and then add that to the Adjusted Offset to immediately point at the correct element in memory. Otherwise, additional code would be needed to subtract the LBOUND value each time the array is accessed. Since the array's LBOUND is simply constant information, it would be wasteful to calculate that repeatedly at run time. Of course, the Adjusted Offset calculation is correspondingly more complex when dealing with multi-dimensional arrays. The remaining entries identify the length of each element in bytes, and the upper and lower bounds. String arrays always have a 4 in the length location, because that's the length of each string descriptor. A separate pair of words is needed for each array subscript, to identify the LBOUND value and the number of elements. The UBOUND is not actually stored in the array descriptor, since it can be calculated very easily when needed. Notice that for multi-dimensional arrays, the last (right-most) subscript is identified first, followed by the second from the last, and continuing to the first one. One final note worth mentioning about dynamic array storage is the location in memory of the first array element. For numeric arrays, the starting address is always zero, within the specified segment. (A new segment can start at any 16-byte address boundary, so at most 15 bytes may be wasted.) However, BASIC sometimes positions fixed-length string and TYPE arrays farther into the segment. BASIC will not allow an array element to span a segment boundary under any circumstances. This could never happen with numeric data, because each element has a length that is a power of 2. That is, 16,384 long integer elements will exactly fit in a single 64K segment. But when a fixed-length string or TYPE array is created, nearly any element length may be specified. For example, if you use REDIM Array(1 TO 10) AS STRING * 13000, 130,000 bytes are needed and element 6 would straddle a segment. To prevent that from happening, BASIC's dynamic DIM routine fudges the first element to instead be placed at address 536. Thus, the last byte in element 5 will be at the end of the 64K segment, and the first byte in element 6 will fall exactly at the start of the second 64K code segment. The only limitation is that arrays with odd lengths like this can never exceed 128K in total size, because the inevitable split would occur at the start of the third segment. Arrays whose element lengths are a power of 2, such as 32 or 4096 bytes, do not have this problem. (Bear in mind that 1K is actually 1,024 bytes, so 128K really equals 131,072 bytes). This is shown graphically below in Figure 2-5. Element 10 is the last that evenly fits -+ Segment boundary ----+ _ _
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 35 -
+------------------------------------------- - +---+---+---+---+--__--+---+---+---+--- +------------------------------------------- - __ _ _ _ +------_ Address 0 +--------------------_ Element 2 +------------------------_ Element 1 +--------------------------_ Address 536 +---------------------------_ Address 0 Figure 2-5 FAR DATA VERSUS NEAR DATA ========================= You have already used the terms "near" and "far" to describe BASIC's data, and now let's see exactly what they mean. The 8086 family of microprocessors that are used in IBM PC and compatible computers use what is called a *segmented architecture*. This means that while an 8086 can access a megabyte of memory, it can do so only in 64K blocks at a time. Before you think this is a terrible way to design a CPU, consider the alternative. For example, the 68000 family used in the Apple Macintosh and Atari computers use linear addressing, whereby any data anywhere may be accessed without restriction. But the problem is that with millions of possible addresses, many bytes are needed to specify those addresses. Because the data segment is implied when dealing with an 80x86, a single integer can refer to any address quickly and with very little code. Therefore, assembler instructions for the 68000 that reference memory tend to be long, making those programs larger. Since being able to manipulate only one 64K segment is restrictive, the 8086's designers provided four different segment registers. One of these, the DS (Data Segment) register, is set to specify a single segment, which is then used by the program as much as possible. This data segment is also named DGROUP, and it holds all of the static data in a BASIC program. Again, data in DGROUP can be accessed much faster and with less code than can data in any other segment. In order to assign an element in a far array, for example, BASIC requires two additional steps which generates additional code. The first step is to retrieve the array's segment from the array descriptor, and the second is to assign the ES (Extra Segment) register to access the data. Far data in a BASIC program therefore refers to any data that is outside of the 64K DGROUP segment. Technically, this could encompass the entire 1 Megabyte that DOS recognizes, however the memory beyond 640K is reserved for video adapters, the BIOS, expanded memory cards, and the like. BASIC uses far memory (outside the 64K data segment but within the first 640K) for numeric, fixed-length string, and TYPE arrays, although BASIC PDS can optionally store conventional strings there when the /fs (Far String) option is used. Communications buffers are also kept in far memory, and this is where incoming characters are placed before your program actually reads them. Near memory is therefore very crowded, with many varied types of data competing for space. Earlier I stated that all variables, static arrays, and quoted strings are stored in near memory (DGROUP). But other BASIC data is also stored there as well. This includes DATA items, string descriptors, array descriptors, the stack, file buffers, and the internal working variables used by BASIC's run-time library routines. When you open a disk file for input, an area in near memory is used as a buffer to improve the speed of subsequent reads. And like subprograms and function that you write, BASIC's internal routines also need their own variables to operate. For example, a translation table is maintained in DGROUP to relate the file numbers you use when opening a file to the file handles that DOS issues. One final note on the items that compete for DGROUP is that in many
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 36 -
cases data is stored *twice*. When you use READ to assign a string from a DATA item, the data itself remains at the data statement, and is also duplicated in the string being assigned. There is simply no way to remove the original data. Similarly, when you assign a string from a constant as in Message$ = "Press any key", the original quoted string is always present, and Message$ receives a second copy. When string space is very tight, the only purely BASIC solution is to instead store the data in a disk file. Speaking of DATA, bear in mind that reading numeric variables is relatively slow and often even more wasteful. Since all DATA items are stored as strings, each time you use READ the VAL routine is called internally by BASIC. VAL is not a particularly fast operation, because of the complexity of what it must do. Worse, by storing numbers as strings, even more memory can be wasted than you might think. For example, storing an integer value such as -20556 requires six bytes as a string, even though it will be placed ultimately into a two-byte integer. ASSESSING MEMORY WITH FRE() Since memory is very important to the operation of most programs, it is often useful to know how much of it is available at any given moment. BASIC provides the FRE function to do this, however there are a number of variations in its use. Let's take an inside look at the various forms of FRE, and see how they can be put to good use. There are no less than six different arguments that can be used with FRE. The first to consider is FRE(0), which reports the amount of free string space but without first compacting the string pool. Therefore, the value returned by FRE(0) may be much lower than what actually could be available. FRE when used with a string argument, for example FRE("") or FRE(Temp$), also returns the amount of DGROUP memory that is available, however it first calls the heap compaction routines. This guarantees that the size reported accurately reflects what is really available. Although FRE(0) may seem to be of little value, it is in fact much faster than FRE when a string argument is given. Therefore, you could periodically examine FRE(0), and if it becomes unacceptably low use FRE("") to determine the actual amount of memory that is available. With BASIC PDS far strings, FRE(0) is illegal, FRE("") reports the number of bytes available for temporary strings, and FRE(Any$) reports the free size of the segment in which Any$ resides. Temporary strings were discussed earlier, when we saw how they are used when passing fixed-length string arguments to procedures. FRE(-1) was introduced beginning with QuickBASIC 1, and it reports the total amount of memory that is currently available for use with far arrays. Thus, you could use it in a program before dimensioning a large numeric array, to avoid receiving an "Out of memory" error which would halt your program. Although there is a distinction between near and far memory in any PC program, BASIC does an admirable job of making available as much memory as you need for various uses. For example, it is possible to have plenty of near memory available, but not enough for all of the dynamic arrays that are needed. In this case, BASIC will reduce the amount of memory available in DGROUP, and instead relinquish it for far arrays. FRE(-1) is also useful if you use SHELL within your programs, because at least 20K or so of memory is needed to load the necessary additional copy of [Link]. It is interesting to observe that not having enough memory to execute a SHELL results in an "Illegal function call" error, rather than the expected "Out of memory". FRE(-2) was added to QuickBASIC beginning with version 4.0, and it reports the amount of available stack space. The stack is a special area within DGROUP that is used primarily for passing the addresses of variables and other data to subroutines. The stack is also used to store variables when the STATIC option is omitted from a subprogram or function definition. I will discuss static and non-static subroutines later in Chapter 3, but for now suffice it to say that enough stack memory is necessary when many variables are present and STATIC is omitted.
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 37 -
FRE(-3) was added with BASIC PDS, mainly for use within the QBX editing environment. This newest variant reports the amount of expanded (EMS) memory that is available, although EMS cannot be accessed by your programs directly using BASIC statements. However, QBX uses that memory to store subroutines and optionally numeric, fixed-length, and TYPE arrays. The ISAM file handler that comes with BASIC PDS can also utilize expanded memory, as can the PDS overlay manager. SETMEM AND STACK Besides the various forms of the FRE function, SETMEM can be used to assess the size of the far heap, as well as modify that size if necessary. The STACK function is available only with BASIC PDS, and it reports the largest possible size the stack can be set to. Let's see how these functions can be useful to you. Although SETMEM is technically a function (because it returns information), it is also used to re-size the far heap. When given an argument of zero, SETMEM returns the current size of the far heap. However, this value is not the amount of memory that is free. Rather, it is the maximum heap size regardless of what currently resides there. The following short program shows this in context. PRINT SETMEM(0) REDIM Array!(10000) PRINT SETMEM(0) 'display the heap size 'allocate 40,000 bytes 'the total size remains
Displayed result (the numbers will vary): 276256 276256 When a program starts, the far heap is set as large as possible by BASIC and DOS, which is sensible in most cases. But there are some situations in which you might need to reduce that size, most notably when calling C routines that need to allocate their own memory. Also, BASIC moves arrays around in the far heap as arrays are dimensioned and then erased. This is much like the near heap string compaction that is performed periodically. If the far heap were not rearranged periodically, it is likely that many small portions would be available, but not a single block sufficient for a large array. In some cases a program may need to claim memory that is guaranteed not to move. Therefore, you could ask SETMEM to relinquish a portion of the far heap, and then call a DOS interrupt to claim that memory for your own use. (DOS provides services to allocate and release memory, which C and assembly language programs use to dimension arrays manually.) Unlike BASIC, DOS does not use sophisticated heap management techniques, therefore the memory it manages does not move. I will discuss using SETMEM this way later on in Chapter 12. Finally, the STACK function will report the largest amount of memory that can be allocated for use as a stack. Like SETMEM, it doesn't reflect how much of that memory is actually in use. Rather, it simply reports how large the stack could be if you wanted or needed to increase it. Because the stack resides in DGROUP, its maximum possible size is dependent on how many variables and other data items are present. When run in the QBX environment, the following program fragment shows how creating a dynamic string array reduces the amount of memory that could be used for the stack. Since the string descriptors are kept in DGROUP, they impinge on the potentially available stack space. PRINT STACK REDIM Array$(1000)
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 38 -
PRINT STACK ERASE Array$ PRINT STACK Displayed result: 47904 43808 47904 Since BASIC PDS does not support FRE(0), the STACK function can be used to determine how much near memory is available. The only real difference between FRE(0) and STACK is that STACK includes the current stack size, where FRE(0) does not. The STACK function is mentioned here because it relates to assessing how much memory is available for data. Sizing the stack will be covered in depth in Chapter 3, when we discuss subprograms, functions, and recursion. VARPTR, VARSEG, AND SADD One of the least understood aspects of BASIC programming is undoubtedly the use of VARPTR and its related functions, VARSEG and SADD. Though you probably already know that VARPTR returns the address of a variable, you might be wondering how that information could be useful. After all, the whole point of a high-level language such as BASIC is to shield the programmer from variable addresses, pointers, and other messy low-level details. And by and large, that is correct. Although VARPTR is not a particularly common function, it can be invaluable in some programming situations. VARPTR is a built-in BASIC function which returns the address of any variable. VARSEG is similar, however it reports the memory segment in which that address is located. SADD is meant for use with conventional (not fixed-length) strings only, and it tells the address where the first character in a string begins. In BASIC PDS, SSEG is used instead of VARSEG for conventional strings, to identify the segment in which the string data is kept. Together, these functions identify the location of any variable in memory. The primary use for VARPTR in purely BASIC programming is in conjunction with BSAVE and BLOAD, as well as PEEK and POKE. For example, to save an entire array quickly to a disk file with BSAVE, you must specify the address where the array is located. In most cases VARSEG is also needed, to identify the array's segment as well. When used on all simple variables, static arrays, and all string arrays, VARSEG returns the normal DGROUP segment. When used on a dynamic numeric array, it instead returns the segment at the which the specified element resides. The short example below creates and fills an integer array, and then uses VARSEG and VARPTR to save it very quickly to disk. REDIM Array%(1 TO 1000) FOR X% = 1 TO 1000 Array%(X%) = X% NEXT DEF SEG = VARSEG(Array%(1)) BSAVE "[Link]", VARPTR(Array%(1)), 2000 Here, DEF SEG indicates in which segment the data that BSAVE will be saving is located. VARPTR is then used to specify the address within that segment. The 2000 tells BSAVE how many bytes are to be written to disk, which is determined by multiplying the number of array elements times the size of each element. We will come back to using VARPTR repeatedly in
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 39 -
Chapter 12 when we discuss accessing DOS and BIOS services with CALL Interrupt. However, it is important to point out here exactly how VARPTR and VARSEG work with each type of variable. When VARPTR is used with a numeric variable, as in Address = VARPTR(Value!), the address of the first byte in memory that the variable occupies is reported. Value! is a single-precision variable which spans four bytes of memory, and it is the lowest of the four addresses that is returned. Likewise, VARPTR when used with static fixed-length string and TYPE variables reports the lowest address where the data begins. But when you ask for the VARPTR of a string variable, what is returned is the address of the string's descriptor. To obtain the address of the actual data in a string requires the SADD (String Address) function. Internally, BASIC simply looks at the address portion of the string descriptor to retrieve the address. Likewise, the LEN function also gets its information directly from the descriptor. When used with any string, VARSEG always reports the normal DGROUP data segment, because that is where all strings and their descriptors are kept. Beginning with BASIC PDS and its support for far strings, the SSEG function was added to return the segment where the string's data is stored. But even when far strings are being used, VARSEG always returns the segment for the descriptor, which is in DGROUP. SADD is not legal with a fixed-length string, and you must instead use VARPTR. Perhaps in a future version BASIC will allow either to be used interchangeably. SADD is likewise illegal for use with the fixed-length string portion of a TYPE variable or array. Again, VARPTR will return the address of any component in a TYPE, within the segment reported by VARSEG. Another important use for VARPTR is to assist passing arrays to assembly language routines. When a single array element is specified using early versions of Microsoft compiled BASIC, the starting address of the element is sent as expected. Beginning with QuickBASIC 4.0 and its support for far data residing in multiple segments, a more complicated arrangement was devised. Here's how that works. When an element in a dynamic array is passed as a parameter, BASIC makes a copy of the element into a temporary variable in near memory, and then sends the address of the copy. When the routine returns, the data in the temporary variable is copied back to the original array element, in case the called routine changed the data. In many cases this behavior is quite sensible, since the called routine can assume that the variable is in near memory and thus operate that much faster. Further, BASIC subroutines *require* a non-array parameter (not passed with empty parentheses) to be in DGROUP. That is, any time a single element in an integer array is passed to a routine, that routine would be designed to expect a single integer variable. This is shown in the brief example below, where a single element in an array is passed, as opposed to the entire array. REDIM Array%(1 TO 100) Array%(25) = -14 CALL MyProc(Array%(25)) . . . SUB MyProc(IntVar%) STATIC PRINT IntVar% END SUB Displayed result: -14 Unfortunately, this copying not only generates a lot of extra code to implement, it also takes memory from DGROUP to hold the copy, and that memory is taken permanently. Worse still, *each* occurrence of an array
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 40 -
element passed in a CALL statement reserves however many bytes are needed to store the element. For a large TYPE structure this can be a lot of memory indeed! So you won't think that I'm being an alarmist about this issue, here are some facts based on programs compiled using BASIC 7.1 PDS. These examples document the amount of additional code that is generated to pass a near string array element as an argument to a subprogram or function. Passing a string array element requires 56 bytes when a copy is made, compared to only 17 when it is not. The same operations in QuickBASIC 4.5 create 47 and 18 bytes respectively, so QB 4.5 is actually better when making the copy, but a tad worse when not. The code used in these examples is shown below, and Array$ is a dynamic near string array. (I will explain the purpose of BYVAL in just a moment.) Again, the difference in byte counts reflects the additional code that BC creates to assign and then delete the temporary copies. CALL Routine(Array$(2)) CALL Routine(BYVAL VARPTR(Array$(2))) Worse still, with either compiler 73 bytes of code are created to pass an element in a TYPE array the usual way, compared to 18 when the copying is avoided. And this byte count does not include the DGROUP memory required to hold the copy. Is that reduction in code size worth working for? You bet it is! And best of all, hardly any extra effort is needed to avoid having BASIC make these copies--just the appropriate knowledge. The key, as you can see, is VARPTR. If you are calling an assembly language routine that expects a string and you want to pass an element from a string array, you must use BYVAL along with VARPTR. CALL Routine(BYVAL VARPTR(Array$(Element))) is functionally identical to CALL Routine(Array$(Element)), although they sure do look different! In either case, the integer address of a string is passed to the routine. Unlike the usual way that BASIC passes a variable by sending its address, BYVAL instead sends the actual data. In this case, the value of an address is what we wanted to begin with anyway. (Without the BYVAL, BASIC would make a temporary copy of the integer value that VARPTR returns, and send the address of that copy.) Best of all, asking for the address directly defeats the built-in copying mechanism. Although creating a copy of a far numeric array element is sensible as we saw earlier, it is not clear to me why BC does this with string array data that is in DGROUP already. Although you can't normally send an integer--which is what VARPTR actually returns--to a BASIC subprogram that expects a string, you can if that subprogram is in a different file and the files are compiled separately. This will also work if the BASIC code has been pre-compiled and placed in a Quick Library. But there is another, equally important reason to use VARPTR with array elements. If you are calling an assembler routine that will sort an array, it must have access to the array element's address, and not the address of a copy. All of the elements in any array are contiguous, and a sort routine would need to know where in memory the first element is located. From that it can then access all of the successive elements. With VARPTR we are telling BASIC that what is needed is the actual address of the specified element. Bear in mind that this relates primarily to passing arrays to assembly language (and possibly C) routines only. After all, if you are designing a sort routine using purely BASIC commands, you would pass and receive the array using empty parentheses. Indeed, this is yet another important advantage that BASIC holds over C and Pascal, since neither of those languages have array descriptors. Writing a sort routine in C requires that *you* do all of the work to locate and compare each element in turn, based on some base starting address. There is one final issue that we must discuss, and that is passing far array data to external assembly language routines. I already explained
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 41 -
that by making a copy of a far array element, the called routine does not have to be written to deal with far (two-word segmented) addresses. But in some cases, writing a routine that way will be more efficient. Further, like C, assembly language routines thrive on manipulating pointers to data. Although an assembler routine could be written to read the segment and address from the array descriptor, this is not a common method. One reason is that if Microsoft changes the format of the descriptor, the routine will no longer work. Another is that it is frankly easier to have the caller simply pass the full segmented address of the first element. This brings us to the SEG directive, which is a combination of BYVAL and VARPTR and also BYVAL and VARSEG. As with BYVAL VARPTR, using SEG before a variable or array element in a call tells BASIC that the value of the array's full address is needed. A typical example would be CALL Routine(SEG Array#(1)), and in this case, BASIC sends not one address word but two to the routine. You could also pass the full address of an array element by value using VARSEG and VARPTR, and this next example produces the identical result: CALL Routine(BYVAL VARSEG(Array#(1)), BYVAL VARPTR(Array#(1))). Using SEG results in somewhat less code, though, because BASIC will obtain the segment and address in a single operation. In fact, this is one area where the compiler does a poor job of optimizing, because using VARSEG and VARPTR in a single program statement generates a similar sequence of code twice. There is one unfortunate complication here, which arises when SEG is used with a fixed-length string array. What SEG *should* do in that case is pass the segmented address of the specified element. But it doesn't. Instead, BASIC creates a temporary copy of the specified element in a conventional dynamic string, and then passes the segmented address of the copy's descriptor. Of course, this is useless in most programming situations. There are two possible solutions to this problem. The first is to use the slightly less efficient BYVAL VARSEG and BYVAL VARPTR combination as shown above. The second solution is to create an equivalent fixed-length string array by using a dummy TYPE that is comprised solely of a single string component. Since TYPE variables are passed correctly when SEG is used, using a TYPE eliminates the problem. Both of these methods are shown in the listing that follows. '----- this creates more code and looks clumsy REDIM Array(1 TO 1000) AS STRING * 50 CALL Routine(BYVAL VARSEG(Array(1)), BYVAL VARPTR(Array(1))) '----- this creates less code and reads clearly TYPE FLen S AS STRING * 100 END TYPE REDIM Array(1 TO 1000) AS FLen CALL Routine(SEG Array(1)) Although SEG looks like a single parameter is being passed, in fact two integers are sent to the called routine--a segment and an address. This is why a single SEG can replace both a VARSEG and a VARPTR in one call. Chapter 13 will return to BYVAL, VARPTR, and SEG, though the purpose there will be to learn how to write routines that accept such parameters. CONSTANTS ========= The final data type to examine is constants. By definition, a constant is
- 42 -
simply any value that does not change, as opposed to a variable that can. For example, in the statement I% = 10, the value 10 is a constant. Similarly, the quoted string "Hello" is a constant when you write PRINT "Hello". There are two types of constants that can appear in a BASIC program. One is simple numbers and quoted strings as described above, and the other is the named constant which is defined using a CONST statement. For example, you can write CONST MaxRows = 25 as well as CONST Message$ = "Insert disk in drive", and so forth. It is even possible to define one CONST value based on a previous one, as in CONST NumRows = 25, ScrnSize = NumRows * 80. Then, you could use these meaningful names later in the program, instead of the values they represent. It is important to understand that using named constants is identical to using the numbers themselves. The value of this will become apparent when you see the relative advantages and disadvantages of using numbers as opposed to variables. Let's begin this discussion of numbers with how they are stored by the compiler. Or rather, how they are sometimes stored. When a CONST statement is used in a BASIC program, BASIC does absolutely nothing with the value, other than to remember that you defined it. Therefore, you could have a hundred CONST statements which are never used, and the final .EXE program will be no larger than if none had been defined. If a CONST value is used as an argument to, say, LOCATE or perhaps as a parameter to a subroutine, BASIC simply substitutes the value you originally gave it. When a variable is assigned as in Value% = 100, BASIC sets aside memory to hold the variable. With a constant definition such as CONST Value% = 100, no memory is set aside and BASIC merely remembers that any use of Value% is to be replaced by the number 100. But how are these numbers represented internally. When you create an integer assignment such as Count% = 5, the BASIC compiler generates code to move the value 5 into the integer variable, as you saw in Chapter 1. Therefore, the actual value 5 is never stored as data anywhere. Rather, it is placed into the code as part of an assembly language instruction. Now, if you instead assign a single or double precision variable from a number--and again it doesn't matter whether that number is a literal or a CONST--the appropriate floating point representation of that number is placed in DGROUP at compile time, and then used as the source for a normal floating point assignment. That is, it is assigned as if it were a variable. There is no reasonable way to imbed a floating point value into an assembly language instruction, because the CPU cannot deal with such values directly. Therefore, assigning X% = 3 treats the number 3 as an integer value, while assigning Y# = 3 treats it as a double precision value. Again, it doesn't matter whether the 3 is a literal number as shown here, or a CONST that has been defined. In fact, if you use CONST Three! = 3, a subsequent assignment such as Value% = Three! treats Three! as an integer resulting in less resultant code. As you can see, the compiler is extremely smart in how it handles these constants, and it understands the context in which they are being used. In general, BASIC uses the minimum precision possible when representing a number. However, you can coerce a number to a different precision with an explicit type identifier. For example, if you are calling a routine in a separate module that expects a double precision value, you could add a pound sign (#) to the number like this: CALL Something(45#). Without the double precision identifier, BASIC would treat the 45 as an integer, which is of course incorrect. Likewise, BASIC can be forced to evaluate a numeric expression that might otherwise overflow by placing a type identifier after it. One typical situation is when constructing a value from two byte portions. The usual way to do this would be Value& = LoByte% + 256 * HiByte%. Although the result of this expression can clearly fit into the long integer no matter what the values of LoByte% and HiByte% might be, an overflow error can still occur. (But as we saw earlier, this will happen only in the QB environment, or if you have compiled to disk with the /d debugging option.) The problem arises when HiByte% is greater than 127, because the
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 43 -
result of multiplying HiByte% times 256 exceeds the capacity of a regular integer. Normally, BASIC is to be commended for the way it minimizes overhead by reducing calculations to the smallest possible data type. But in this case it creates a problem, because the result cannot be expressed as an integer. The solution, then, is to add an ampersand after the 256, as in Value& = LoByte% + 256& * HiByte%. By establishing the value 256 as a long integer, you are telling BASIC to perform the calculation to the full precision of a long integer. And since the result of the multiplication is treated as a long integer, so is the addition of that result to LoByte%. A single precision exclamation point could also be used, but that would require a floating point multiplication. Since a long integer multiply is much faster and needs less code, this is the preferred solution. One final item worth noting is the way the QB and QBX editing environments sometimes modify constants. For example, if you attempt to enter a statement such as Value! = 1.0, you will see the constant changed to read 1! instead. This happens when you press Enter to terminate the line. Similarly, if you write D# = 1234567.8901234, BASIC will add a trailing pound sign to the number. This behavior is your clue that these numbers are being stored internally as single and double precision values respectively. PASSING NUMERIC CONSTANTS TO A PROCEDURE Normally, any constant that could be an integer is passed to a subprogram or function as an integer. That is, calling an external procedure as in CALL External(100) passes the 100 as an integer value. If the called routine has been designed to expect a variable of a different type, you must add the appropriate type identifier. If a long integer is expected, for example, you must use CALL External(100&). If, on the other hand, the called routine is in the same module (that is, the same physical source file), QB will create a suitable DECLARE statement automatically. This lets QB and BC know what is expected so they can pass the value in the correct format. Thus, BASIC is doing you a favor by interpreting the constant's type in a manner that is relevant to your program. This "favor" has a nasty quirk, though. If you are developing a multi-module program in the QuickBASIC editor, the automatic type conversion is done for you automatically, even when the call is to a different module. Your program uses, say, CALL Routine(25), and QB or QBX send the value in the correct format automatically. But when the modules are compiled and linked, the same program that had worked correctly in the environment will now fail. Since each module in a multi-module program is compiled separately, BC has no way to know what the called routine actually expects. In fact, this is one of the primary purposes of the DECLARE statement--to advise BASIC as to how arguments are to be passed. For example, DECLARE SUB Marine(Trident!) tells BASIC that any constant passed to Marine is to be sent as a single precision value. You could optionally use the AS SINGLE directive, thus: DECLARE SUB Marine(Trident AS SINGLE). In general, I prefer the more compact form since it conveys the necessary information with less clutter. Another important use for adding a type identifier to a numeric constant is to improve a program's accuracy. Running the short program below will illustrate this in context. Although neither answer is entirely accurate, the calculation that uses the double precision constant is much closer. In this case, a decimal number that does not have an explicit type identifier is assumed to have only single precision accuracy. That is, the value is stored in only four bytes instead of eight. FOR X% = 1 TO 10000 Y# = Y# + 1.1 Z# = Z# + 1.1# NEXT
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 44 -
You have already learned that BASIC often makes a temporary copy of a variable when calling a subprogram or function. But you should know that this also happens whenever a constant is passed as an argument. For example, in a function call such as Result = Calculate!(Value!, 100), where Calculate! has been declared as a function, the integer value 100 is copied to a temporary location. Since BASIC procedures require the address of a parameter, a temporary variable must be created and the address of that variable passed. The important point to remember is that for each occurrence of a constant in a CALL or function invocation, a new area of DGROUP is taken. You might think that BASIC should simply store a 100 somewhere in DGROUP once, and then pass the address of that value. Indeed, this would save an awful lot of memory when many constants are being used. The reason this isn't done, however, is that subroutines can change incoming parameters. Therefore, if a single integer 100 was stored and its address passed to a routine that changed it, subsequent calls using 100 would receive an incorrect value. The ideal solution to this problem is to create a variable with the required value. For example, if you are now passing the value 2 as a literal many times in a program, instead assign a variable, perhaps named Two%, early in your program. That is, Two% = 2. Then, each time you need that value, instead pass the variable. For the record, six bytes are needed to assign an integer such as Two%, and four bytes are generated each time that variable is passed in a call. Contrast that to the 10 bytes generated to create and store a temporary copy and pass its address, not including the two bytes the copy permanently takes from near memory. Even if you use the value only twice, the savings will be worthwhile (24 vs. 30 bytes). Because a value of zero is very common, it is also an ideal candidate for being replaced with a variable. Even better, you don't even have to assign it! That is, CALL SomeProc(Zero%) will send a zero, without requiring a previous Zero% = 0 assignment. STRING CONSTANTS ================ Like numeric constants, string constants that are defined in a CONST statement but never referenced will not be added to the final .EXE file. Constants that are used--whether as literals or as CONST statements--are always stored in DGROUP. If your program has the statement PRINT "I like BASIC", then the twelve characters in the string are placed into DGROUP. But since the PRINT statement requires a string descriptor in order to locate the string and determine its length, an additional four bytes are allocated by BASIC just for that purpose. Variables are always stored at an even-numbered address, so odd-length strings also waste one extra byte. Because string constants have a ferocious appetite for near memory, BC has been designed to be particularly intelligent in the way they are handled. Although there is no way to avoid the storage of a descriptor for each constant, there is another, even better trick that can be employed. For each string constant you reference in a program that is longer than four characters, BC stores it only once. Even if you have the statement PRINT "Press any key to continue" twenty-five times in your program, BC will store the characters just once, and each PRINT statement will refer to the same string. In order to do this, the compiler must remember each string constant it encounters as it processes your program, and save it in an internal working array. When many string constants are being used, this can cause the compiler to run out of memory. Remember, BC has an enormous amount of
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 45 -
information it must deal with as it processes your BASIC source file, and keeping track of string constants is but one part of the job. To solve this problem Microsoft has provided the /s (String) option, which tells BC not to combine like data. Although this may have the net effect of making the final .EXE file larger and also taking more string space, it may be the only solution with some large programs. Contrary to the BASIC documentation, however, using /s in reality often makes a program *smaller*. This issue will be described in detail in Chapter 5, where all of the various BC command line options are discussed.
PASSING STRING CONSTANTS TO A PROCEDURE As you have repeatedly seen, BASIC often generates additional code to create copies of variables and constants. It should come as no surprise, therefore, to learn that this happens with string constants as well. When you print the same string more than once in a program, BASIC knows that its own PRINT routine will never change the data. But as with numeric constants, if you send a string constant to a subprogram or function, there is no such guarantee. For example, if you have a statement such as CALL PrintIt(Work$) in your program, it is very possible--even likely--that the PrintIt routine may change or reassign its incoming parameter. Even if *you* know that PrintIt will not change the string, BASIC has no way to know this. To avoid any possibility of that happening, BASIC generates code to create a temporary copy of every string constant that is used as an argument. And this is done for every call. If the statement CALL PrintMessage("Press a key") appears in your program ten times, then code to copy that message is generated ten times! Beginning with BASIC 7.1 PDS, you can now specify that variables are to be sent by value to BASIC procedures. This lets you avoid the creation of temporary copies, and this subject will also be explored in more detail in Chapter 3. With either QuickBASIC 4.5 or BASIC PDS, calling a routine with a single quoted string as an argument generates 31 bytes of code. Passing a string variable instead requires only nine bytes. Both of these byte counts includes the five bytes to process the call itself. The real difference is therefore 4 bytes vs. 26--for a net ratio of 6.5 to 1. (Part of those 31 bytes is code that erases the temporary string.) So as with numeric constants that are used more than once, your programs will be smaller if a variable is assigned once, and that variable is passed as an argument. While we are on the topic of temporary variables, there is yet another situation that causes BASIC to create them. When the result of an expression is passed as an argument, BASIC must evaluate that expression, and store the result somewhere. Again, since nearly all procedures require the address of a parameter rather than its value, an address of that result is needed. And without storing the result, there can of course be no address. When you use a statement such as CALL Home(Elli + Lou), BASIC calculates the sum of Elli plus Lou, and stores that in a reserved place in DGROUP which is not used for any other purpose. That address is then sent to the Home routine as if it were a single variable, and Home is none the wiser. Likewise, a string concatenation creates a temporary string, for the same reason. Although the requisite descriptor permanently steals four bytes of DGROUP memory, the temporary string itself is erased by BASIC automatically after the call. Thus, the first example in the listing below is similar in efficiency to the second. The four-byte difference is due to BASIC calling a special routine that deletes the temporary copy it created, as opposed to the slightly more involved code that assigns Temp$ from the null string ("") to erase it. CALL DoIt(First$ + Last$) 'this makes 41 bytes
- 46 -
Temp$ = First$ + Last$ CALL DoIt(Temp$) Temp$ = "" UNUSUAL STRING CONSTANTS
One final topic worth mentioning is that QuickBASIC also lets you imbed control and extended characters into a string constant. Consider the program shown below. Here, several of the IBM extended characters are used to define a box, but without requiring CHR$ to be used repeatedly. Characters with ASCII values greater than 127 can be entered easily by simply pressing and holding the Alt key, typing the desired ASCII value on the PC's numeric key-pad, and then releasing the Alt key. This will not work using the number keys along the top row of the keyboard. DIM Box$(1 TO 4) Box$(1) Box$(2) Box$(3) Box$(4) = = = = 'define a box
"+------------------+" " " " " "+------------------+" 'now display the box
To enter control characters (those with ASCII values less than 32) requires a different trick. Although the Alt-keypad method is in fact built into the BIOS of all PCs, this next one is specific to QuickBASIC, QBX, and some word processor programs. To do this, first press Ctrl-P, observing the ^P symbol that QB displays at the bottom right of the screen. This lets you know that the next control character you press will be accepted literally. For example, Ctrl-P followed by Ctrl-L will display the female symbol, and Ctrl-P followed by Ctrl-[ will enter the Escape character. Bear in mind that some control codes will cause unusual behavior if your program is listed on a printer. For example, an embedded CHR$(7) will sound the buzzer if your printer has one, a CHR$(8) will back up the print head one column, and a CHR$(12) will issue a form feed and skip to the next page. Indeed, you can use this to advantage to intentionally force a form feed, perhaps with a statement such as REM followed by the Ctrl-L female symbol. I should mention that different versions of the QB editor respond differently to the Ctrl-P command. QuickBASIC 4.0 requires Ctrl-[ to enter the Escape code, while QBX takes either Ctrl-[ or the Escape key itself. I should also mention that you must never imbed a CHR$(26) into a BASIC source file. That character is recognized by DOS to indicate the end of a file, and BC will stop dead at that point when compiling your program. QB, however, will load the file correctly. WOULDN'T IT BE NICE IF DEPT. ============================ No discussion of constants would be complete without a mention of initialized data. Unfortunately, as of this writing BASIC does not support that feature! The concept is simple, and it would be trivial for BASIC's designers to implement. Here's how initialized data works. Whenever a variable requires a certain value, the only way to give it that value is to assign it. Some languages let you declare a variable's initial value in the source code, saving the few bytes it takes to assign it later. Since space for every variable is in the .EXE file anyway, there
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 47 -
would be no additional penalty imposed by adding this capability. I envision a syntax such as DIM X = 3.9 AS SINGLE, or perhaps simply DIM Y% = 3, or even DIM PassWord$ = "GuessThis". Where Y% = 3 creates a six-byte code sequence to put the value 3 into Y%, what I am proposing would have the compiler place that value there at the time it creates the program. Equally desireable would be allowing string constants to be defined using CHR$ arguments. For example, CONST EOF$ = CHR$(26) would be a terrific enhancement to the language, and allowing code such as CONST CRLF$ = CHR$(13) + CHR$(10) would be even more powerful. Again, we can only hope that this feature will be added in a future version. Yet another constant optimization that BASIC could do but doesn't is constant string function evaluation. In many programming situations the programmer is faced with deciding between program efficiency and readability. A perfect example of this is testing an integer value to see whether it represents a legal character. For instance, IF Char < 65 is not nearly as meaningful as IF Char < ASC("A"). Clearly, BC could and should resolve the expression ASC("A") while it is compiling your program, and generate simple code that compares two integers. Instead, it stores the "A" as a one-byte string (which with its descriptor takes five bytes), and generates code to call the internal ASC function before performing the comparison. The point here is that no matter how intelligent BC is, folks like us will always find some reason to complain! BIT OPERATIONS ============== The last important subject this chapter will cover is bit manipulation using AND, OR, XOR, and NOT. These logical operators have two similar, but very different, uses in a BASIC program. The first use--the one I will discuss here--is to manipulate the individual bits in an integer or long integer variable. The second use is for directing a program's flow, and that will be covered in Chapter 3. Each of the bit manipulation operators performs a very simple Binary function. Most of these functions operate on the contents of two integers, using those bits that are in an equivalent position. The examples shown in Figure 2-6 use a single byte only, solely for clarity. In practice, the same operations would be extended to either the sixteen bits in an integer, or the 32 bits in a long integer. 13 = 0000 1101 25 = 0001 1001 --------0000 1001 result when AND is used ^ ^ +----------- both of the bits are set in each column 13 = 0000 1101 25 = 0001 1001 --------0001 1101 result when OR is used ^ ^^ ^ +------------- one or both bits are set in each column 13 = 0000 1101 25 = 0001 1001 --------0001 0100 ^ ^
+------------- the bits are different in each column 13 = 0000 0000 0000 1101 ------------------1111 1111 1111 0010 Figure 2-6 The examples given here use the same decimal values 13 and 25, and these are also shown in their Binary equivalents. What is important when viewing Binary numbers is to consider the two bits in each vertical column. In the first example, the result in a given column is 1 (or True) only when that bit is set in the first number AND the same bit is also set in the second. This condition is true for only two of the bits in these particular numbers. The result bits therefore represent the answer in Binary, which in this case is 13 AND 25 = 9. What is important here is not that 13 AND 25 equals 9, but how the bits interact with each other. The second example shows OR at work, and it sets the result bits for any position where a given bit is set in one byte OR that bit is set in the other. Of course, if both are set the OR result is also true. In this case, four of the columns have one bit or the other (or both) set to 1. By the way, these results can be proven easily in BASIC by simply typing the expression. That is, PRINT 13 OR 25 will display the answer 29. The third example is for XOR, which stands for Exclusive Or. XOR sets a result bit only when the two bits being compared are different. Here, two of the bits are different, thus 13 XOR 25 = 20. Again, it is not the decimal result we are after, but how the bits in one variable can be used to set or clear the bits in another. The NOT operator uses only one value, and it simply reverses all of the bits. Any bit that was a 1 is changed to 0, and any bit that had been 0 is now 1. A full word is used in this example, to illustrate the fact that NOT on any positive number makes it negative, and vice versa. As you learned earlier in this chapter, the highest, or left-most bit is used to store the sign of a number. Therefore, toggling this bit also switches the number between positive and negative. In this case, NOT 13 = -14. All of the logical operators can be very useful in some situations, although admittedly those situations are generally when accessing DOS or interfacing with assembly language routines. For example, many DOS services indicate a failure such as "File not found" by setting the Carry flag. You would thus use AND after a CALL Interrupt to test that bit. Another good application for bit manipulation is to store True or False information in each of the sixteen bits in an integer, thus preserving memory. That is, instead of sixteen separate Yes/No variables, you could use just one integer. Bit operations can also be used to replace calculations in certain situations. One common practice is to use division and MOD to break an integer word into its component byte portions. The usual way to obtain the lower byte is LoByte% = Word% MOD 256, where MOD provides the remainder after dividing. While there is nothing wrong with doing it that way, Word% = LoByte% AND 255 operates slightly faster. Division is simply a slower operation than AND, especially on the 8088. Newer chips such as the 80286 and 80386 have improved algorithms, and division is not nearly as slow as with the older CPU. Chapter 3 will look at some other purely BASIC uses of AND and OR. SUMMARY ======= As you have seen in this chapter, there is much more to variables and data than the BASIC manuals indicate. You have learned how data is constructed and stored, how the compiler manipulates that data, and how to determine
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 49 -
for yourself the amount of memory that is needed and is available. In particular, you have seen how data is copied frequently but with no indication that this is happening. Because such copying requires additional memory, it is a frequent cause of "Out of memory" errors that on the surface appear to be unfounded. You have also learned about BASIC's near and far heaps, and how they are managed using string and array descriptors. With its dynamic allocation methods and periodic rearrangement of the data in your program, BASIC is able to prevent memory from becoming fragmented. Although such sophisticated memory management techniques require additional code to implement, they provide an important service that programmers would otherwise have to devise for themselves. Finally, you have learned how the various bit manipulation operations in BASIC work. This chapter will prove to be an important foundation for the information presented in upcoming chapters. Indeed, a thorough understanding of data and memory issues will be invaluable when you learn about accessing DOS and BIOS services in Chapter 12.
CHAPTER 3 PROGRAMMING METHODS In Chapters 1 and 2 you learned how the BASIC compiler translates a source file into the equivalent assembly language statements, and how it allocates memory to store variables and constants. In particular, you saw that the BC compiler generates assembly language code directly for some statements, while for others it creates calls to routines in the BASIC libraries. Most of the code examples presented in that chapter dealt with simple variable assignments and calculations. Of course, the compiler must do much more than merely assign and manipulate variables and other data. Equally important is controlling how your program operates, and determining which paths are to be taken as it progresses. In this chapter we will delve into the inner workings of control flow structures, with an eye toward writing programs that are as efficient as possible. As with the earlier chapters, this discussion includes numerous disassemblies of compiled BASIC code. Thus, you will see exactly what the compiler does, and how each control flow statement is handled. This chapter also discusses the design of both static and non-static subprograms and functions, and compares the relative merits of each method. Many programmers do not fully understand the term Static, and find the related subject of recursive subroutines especially difficult to grasp. BASIC supports four types of subroutines, and each will be described in this chapter: GOSUB routines, subprograms, DEF FN functions, and what I call "formal functions". YOu will notice that I use the terms subroutine and procedure interchangeably, to indicate a single block of code that may be executed more than once. You will also learn how parameters are passed to these procedures. Finally, in this chapter I will discuss programming style. Programming in any language is arguably as much of an art as it is a science. But unlike, say, music, where a composer can write any sequence of notes and proclaim them acceptable, a computer program must at least work correctly. There are an infinite number of ways to accomplish any programming task, and I can make recommendations only. Which approach you choose will reflect both your own personal taste and style, as well as your current level of competence and understanding of programming in general. CONTROL FLOW ============ All programs--regardless of the language in which they are written--require
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 50 -
a mechanism for testing certain conditions and then performing different actions based on those conditions. Although there are many ways to perform tests and branches in a BASIC program, all of them do essentially the same thing. The BASIC control flow statements are GOTO, DO/LOOP, WHILE/WEND, IF/THEN/ELSE, FOR/NEXT, SELECT CASE, ON GOTO, and ON GOSUB. Because the capabilities of WHILE/WEND are also available with a DO/LOOP construct, the two will be discussed together. In almost all cases, the BASIC compiler directly generates the code that controls a program's flow. One exception is when floating point values are used as a FOR counter, or as a WHILE or UNTIL condition. In those situations, calls are made to the floating point comparison routines in the BASIC runtime library. Another place is when you have a statement such as CASE ASC(X$), or IF LEFT$(X$, 10) = Y$. ASC and LEFT$ are also subroutines in the BASIC language library, and they too are invoked by calls. It is important to reiterate that when dealing with integer test conditions, BC will in many cases create assembly language code that is as good as a human programmer would write. In the short program fragment that follows, all of the BASIC source code is shown translated to the equivalent assembly language statements. This listing was derived by compiling and linking the BASIC program for Microsoft CodeView, and then using CodeView to display the resultant code. This is what you write: DO X% = X% + 1 LOOP WHILE X% < 100 This is the result after compilation: 30: INC CMP JL WORD PTR [X%] WORD PTR [X%],64 30 ;X% = X% + 1 ;compare X% to 100 ;jump if less to 30
Here the variable X% is incremented, and then compared to the value 100. (64 is the Hex equivalent to 100, which is how CodeView displays values.) If X% is indeed less than 100, the program jumps back to address 30 and continues processing the loop. Notice that while this example does not use a named label in the BASIC source code as the target for a GOTO, the equivalent assembly language code does. In this case, the label is the code at address 30. Do not confuse the addresses that assembly language must use as jump targets with the numbered labels that in BASIC are optional. THE DREADED GOTO Modern programming philosophy dictates that GOTO and GOSUB statements should be avoided at all cost, in favor of DO and WHILE loops. However, all of these methods result in nearly identical code. Indeed, there is nothing inherently wrong with using GOTO when circumstances warrant it. By examining the program listing below, you will see that BASIC generates code that is identical for a GOTO as for a DO loop. This is what you write: Label: X% = X% + 1 IF X% < 100 THEN GOTO Label
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 51 -
This is the result after compilation: 30: INC CMP JL WORD PTR [X%] WORD PTR [X%],64 30 ;X% = X% + 1 ;compare X% to 100 ;jump if less to 30
Since GOTO and DO/LOOP produce the same results, which one is better, and why? In general, a DO/LOOP is preferable for two reasons. First, it is a nuisance to have to create a new and unique label name for every location that a program may need to branch to. Admittedly, in a short program this will not be a problem. But in a large application with many small loops that test for keyboard input, you end up creating many labels with names such as GetKey1, GetKey2, and so forth. And if you inadvertently use the wrong label name, your program will not work correctly. More important, however, is that for each label you define in a program, the BC compiler must remember its name and the equivalent address in the object code that the label identifies. Since label names can be as long as 40 characters and memory addresses require 2 bytes each to identify, a finite number of label names can be accommodated. By avoiding unnecessary labels, you are giving BC that much more memory to use for compiling your program. There are several situations in which GOTO is preferable to a DO or WHILE loop. Indeed, one of my personal pet peeves is when a programmer tries to shoehorn structure into a program no matter what the cost. Consider the three different code fragments below; each waits for a key press and then assigns it to the variable Ky$. This approach is the worst: Ky$ = "" WHILE Ky$ = "" Ky$ = INKEY$ WEND This method is better: Label: Ky$ = INKEY$ IF Ky$ = "" GOTO Label And this is better still: DO Ky$ = INKEY$ LOOP WHILE Ky$ = "" In the first example, an extra step is needed solely to clear Ky$ to a null string, so the initial WHILE will be true and execute at least once. Every string assignment adds 13 bytes to a program, and those 13 bytes can add up quickly in a large application. The second example avoids the unnecessary assignment, but adds a label for GOTO to jump to. Although this label does require a small amount of additional memory while the program is being compiled, it does not increase the size of the final executable program file. The last example is better still, because it avoids the need for a line label and also avoids an extra string assignment. Since a DO loop allows the test to be placed at either the top or bottom of the loop, you can force the loop to be executed at least once by putting the test at the
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 52 -
bottom as shown here. However, even this can be improved upon by eliminating the string comparison that checks if Ky$ is equal to a null string. If we replace LOOP WHILE Ky$ = "" with LOOP UNTIL LEN(Ky$), only 13 bytes of code are generated instead of 15. When two strings are compared (Ky$ and ""), each must be passed to the string comparison routine. Since LEN requires only one argument, the code to pass the second parameter is avoided. There are some situations for which the GOTO is ideally suited. In the first two examples below, a complex expression is used as the condition for executing a DO WHILE loop, and the same expression is then used again within the loop. DO WHILE (X% + Y%) * Z% > 13 IF (X% + Y%) * Z% = 100 THEN PRINT ... ... LOOP DO WHILE ASC(MID$(S$, A%, B%)) > 13 IF ASC(MID$(S$, A%, B%)) > 100 THEN PRINT ... ... LOOP Label: Temp% = ASC(MID$(S$, A%, B%)) IF Temp% > 13 THEN IF Temp% > 100 THEN PRINT ... ... GOTO Label END IF In the first example, BASIC remembers the results of its test that checks if a (X% + Y%) * Z% is greater than 13, and it uses the result it just calculated in the next test that compares the same expression to 100. This is one more example of the kinds of optimizations BC performs as it compiles your programs. String expressions such as those used in the second example are of necessity more complex, and require calls to library routines. With this added complexity, BASIC unfortunately cannot retain the result of the earlier comparison, and it generates identical code a second time. A more elegant solution in this case is therefore the GOTO as shown in the last example. Because the result of evaluating the expression is saved manually, it may be reused within the loop. As proof, the second DO WHILE example above requires 73 bytes to implement, as opposed to only 53 when Temp% and GOTO are used. I should also point out that the most common and valuable use for GOTO is to get out of a deeply nested series of IF or other blocks of code. It is not uncommon to have a FOR/NEXT loop that contains a SELECT CASE block, and within that a series of IF/ELSE tests. The only way to jump out of all three levels at once is with a GOTO. FOR/NEXT LOOPS Unlike WHILE and DO loops that can test for nearly any condition and at either the top or bottom of the loop, a FOR/NEXT loop is intended to perform a block of statements a fixed number of times. A FOR/NEXT loop could also be replaced with code that compares a value and uses GOTO to reenter the loop if needed, but that is hardly necessary. My point is to
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 53 -
yet again illustrate that all of BASIC's seemingly fancy constructs are no more than tests and GOTOs deep down at the assembly language level. A FOR/NEXT loop determines the number of iterations that will be executed once ahead of time, before the loop begins. For example, the listing below shows a loop that changes the upper limit inside the loop. However the loop still executes 10 times. Limit% = 10 FOR X% = 1 TO Limit% Limit% = 5 PRINT Limit% NEXT The code that BASIC produces for the FOR/NEXT loop in the previous example is translated to the following equivalent during the compilation process. Limit% = 10 Temp% = Limit% X% = 1 GOTO Next: For: Limit% = 5 PRINT Limit% X% = X% + 1 Next: IF X% <= Temp% THEN GOTO For Please understand that changing a loop condition inside the loop is considered bad practice, because the program becomes difficult to understand. If you really need to alter the limit inside a loop, the loop should be recoded to use WHILE or DO instead. Another good reason for avoiding such code is because it is possible that future versions of BASIC will behave differently than the one you are using now. If Microsoft were to modify BASIC such that the limit condition were reevaluated at the NEXT statement, your code would no longer work. It is also considered bad practice to modify the loop counter variable itself (X% in the previous examples). However, this causes no real harm, and you should not be afraid to do that if the situation warrants it. Of course, changing the loop counter will affect the number of times the loop is executed. IF/THEN/ELSE AND SELECT CASE BASIC provides two methods for testing conditions in a program, and executing different blocks of code based on the result. The most common method is the IF test, which can be used on a single variable, the result of an expression, the returned value from a function, or any combination of these. I won't belabor the most common uses for IF here, but I do want to point out some of its less obvious properties. Also, there are some situations where IF and ELSEIF are appropriate, and others where their counterpart, SELECT CASE, is better. As you have already learned, a simple IF test will in most cases be translated into the equivalent assembler instructions directly. In some cases, however, the condition you specify is tested, while in others the *opposite* condition is tested. If you say IF X > 10 THEN GOTO Label, BASIC may change that to IF X <= 10 GOTO [next statement]. Which BASIC uses depends on what you will do if the condition is true, and how far away in the generated code the statements that will be executed are located. When a GOTO is to be performed if the test passes, then the relative position of the target label is also a factor. A jump to a location either ahead in the code or more than 128 bytes
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 54 -
backwards requires BASIC to generate more code. The 128 byte displacement is significant, because the 80x86 can perform a *conditional jump* to an address only a limited distance away. That is, after a comparison is made, the target address for a conditional jump such as "Jump if Greater" must be no more than that many bytes distant. However, an unconditional jump can be to any address within the same 64K code segment. (Bear with me for a moment, because the significance of this will soon become apparent.) This is shown in the next listing following. IF X% = 100 THEN CMP Word Ptr [X%],64 JE 003A JMP Label 003A: Y% = 2 MOV Word Ptr [Y%],2 END IF Label: IF X > 8 GOTO Label CMP Word Ptr [X%],8 JG Label
In the first example above, BASIC compares the value of X% to 100 (64 Hex), and if equal jumps ahead to a label it created at address 003A Hex. Otherwise, a jump is made to the next statement in the program, which in this case is a named label. Although using two jumps may seem unnecessarily convoluted, it is necessary because BASIC has no way of knowing how many statements will follow at the time it compiles the IF test. Thus, it also cannot know whether the statement following the END IF will end up being 128 or more bytes ahead. By jumping to another, unconditional jump, BC is assured that the generated code will be legal. (When BC finally encounters the END IF, it goes back to the code it created earlier, and completes the portion of the unconditional jump instruction that tells how far to go.) Some compilers avoid this situation and create the longer, two-jump code on a trial basis, but then go back and change it to the shorter form if possible. These are called two-pass compilers, because they process your source code in two phases. Unfortunately, current versions of Microsoft BASIC do not use more than one pass. In the second example Label has already been encountered, and BC knows that the label is within 128 bytes. Therefore, it can translate the IF statement directly, without having to conditionally jump to yet another jump. Had the earlier label been farther away, though, an extra jump would have been needed. It is important to understand that forward jumps are always handled with more code than is likely necessary, because BASIC does not know how far ahead the jump must go. In fact, this same issue must be dealt with when writing in assembly language, since the conditional jump distance limitation is inherent in the 80x86 microprocessor. The bottom line, therefore, is that you can in many cases reduce the size of your programs by controlling in which direction a conditional jump will be performed. For example, almost all programs must at some point sit in a loop waiting until a key is pressed. The next listing shows two common ways to do this, with one testing for a key press at the top of the loop, and the other doing the test at the bottom. DO UNTIL LEN(INKEY$) 0030: CALL B$INKY PUSH AX CALL B$FLEN AND AX,AX ;this comprises 18 bytes ;call INKEY$ ;pass the result to LEN ;AX now holds the length ;see if it's zero
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 55 -
0044: DO LOOP UNTIL LEN(INKEY$) CALL B$INKY PUSH AX CALL B$FLEN AND AX,AX JZ 0044
;this is only 15 bytes ;call INKEY$ ;as above ;jump back if zero
Viewed from a purely BASIC perspective, these two examples operate identically. But as you can see, the code that BASIC creates is more efficient for the second example. When BASIC encounters the first DO statement, it has no idea how many more statements there will be until the terminating LOOP. Therefore, it has no recourse but to create an extra jump. In the second example, the location of the DO is already known to be within 128 bytes, so the LOOP test can branch back using the shorter and more direct method. An ELSEIF statement block is handled in a similar fashion, with code that directly compares each condition and branches accordingly. Because the code to be executed if the IF is true is always after the IF test itself, the less efficient two-jump code must be generated. A simple IF/ELSEIF follows, shown as a mix of BASIC and assembly language statements. IF X% > 9 THEN CMP Word Ptr [X%],9 JG 003A JMP 0043 003A: Y% = 1 MOV Word Ptr [Y%],1 JMP 0066 ELSEIF X% > 5 THEN 0043: CMP Word Ptr [X%],5 JG 004D JMP 0066 004D: Y% = 2 MOV Word Ptr [Y%],2 END IF 0066: ... ...
;compare X% to 9 ;assign Y% if greater ;else jump to next test ;assign Y% ;jump out of the block ;as above
Aside from the additional jumping over jumps that are added to all forward address references, this code is translated quite efficiently. In this situation, the compiled output is identical to that produced had SELECT CASE been used. However, there is one important situation in which SELECT CASE is more efficient than IF and ELSEIF. For each ELSEIF test condition, code is generated to create a separate comparison. When a simple comparison such as X% > 9 is being made, only one assembly language statement is needed. But when an expression is tested--for example, ABS((X% + Y%) * Z%)) > 9--identical code is generated repeatedly. This is illustrated in the listing that follows.
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 56 -
IF ABS((X% + Y%) * Z%) = 5 THEN A% = 1 ELSEIF ABS((X% + Y%) * Z%) = 6 THEN A% = 2 ELSEIF ABS((X% + Y%) * Z%) = 7 THEN A% = 3 END IF Each time BC encounters the expression ABS((X% + Y%) * Z%), it duplicates the same assembly language statements. But when SELECT CASE is used, the expression is evaluated once, and used for each subsequent test. The first example in the next listing shows how SELECT CASE could be used to provide the same functionality as the preceding IF/ELSEIF block, but with much less code. The second example then shows what SELECT CASE really does, using an IF/ELSEIF equivalent. You write it this way: SELECT CASE ABS((X% + Y%) * Z%) CASE 5: A% = 1 CASE 6: A% = 2 CASE 7: A% = 3 CASE ELSE END SELECT BASIC really does this: Temp% = ABS((X% + Y%) * Z%) IF Temp% = 5 THEN A% = 1 ELSEIF Temp% = 6 THEN A% = 2 ELSEIF Temp% = 7 A% = 3 END IF As you can see, SELECT CASE evaluates the expression once, stores the result in a temporary variable, and then uses that variable repeatedly for all subsequent comparisons. Therefore, when the same expression is to be tested multiple times, SELECT CASE will be more efficient than IF and ELSEIF. This is also true for string expressions and other functions. For example, SELECT CASE LEFT$(Work$, 10) will result in less code and faster performance than using IF and ELSEIF with that same expression more than once. Another important feature of SELECT CASE is its ability to use either variable or constant test conditions, and to operate on a range of values. For example, the C language Switch statement which is the equivalent of BASIC's SELECT CASE can use only constant numbers for each test. BASIC is particularly powerful in this regard, and allows any legal expression for each CASE condition. For example, CASE IS > (Y AND Z) is valid, and so is CASE 0 TO Max. CASE also accepts multiple conditions separated by commas such as CASE 1, 3, 4 TO 100, -10 TO -1. In this case, the statements that follow will be executed if the selected expression equals 1, 3, any value between 4 and 100 inclusive, or any value between -10 and -1 inclusive. It is also worth mentioning here that QuickBASIC version 4.0 contains an interesting and irritating quirk that requires a CASE ELSE in the event that none of the tests match. Had the CASE ELSE been omitted from the previous example and the value of the expression was not between 5 and 7, QuickBASIC 4.0 would issue a "CASE ELSE expected" error at run time.
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 57 -
Fortunately, this has been repaired in QuickBASIC 4.5 and later versions. Notice that this is not a bug in QuickBASIC. Rather, it is the behavior described in the ANSI (American National Standards Institute) specification for BASIC. At the time QuickBASIC 4.0 was introduced, Microsoft mistakenly believed the then-proposed ANSI standard for BASIC would be significant. As that standard approached fruition, it became clear to Microsoft that the only standard most programmers really cared about was Microsoft's. One final point I cannot make often enough is the inherent efficiency of integer operations and comparisons. This is especially true in the comparisons that are made in both IF and CASE tests. In the first example below, each of the characters in a string is tested in turn. The second example shows a much better way to write such a test, by obtaining the ASCII value once and using that for subsequent integer comparisons. Not recommended: FOR X = 1 TO LEN(Work$) SELECT CASE MID$(Work$, X, 1) CASE CHR$(9): PRINT "Tab key" CASE CHR$(13): PRINT "Enter key" CASE CHR$(27): PRINT "Escape key" CASE "A" TO "Z", "a" TO "z": PRINT "Letter" CASE "0" TO "9": PRINT "Number" END SELECT NEXT Much more efficient: FOR X = 1 TO LEN(Work$) SELECT CASE ASC(MID$(Work$, X, 1)) CASE 9: PRINT "Tab key" CASE 13: PRINT "Enter key" CASE 27: PRINT "Escape key" CASE 65 TO 90, 97 TO 122: PRINT "Letter" CASE 48 TO 57: PRINT "Number" END SELECT NEXT In the first program the SELECT itself generates 27 bytes, which is comprised of a call to the MID$ function and then a call to the string assign routine. A string assignment is needed to save the MID$ result in a temporary variable for the subsequent tests that follow. Each CASE test that uses CHR$ adds 27 bytes, and this includes the call to CHR$ as well as an additional call to the string comparison routine. Testing for the letters adds 75 bytes, and testing for the numbers adds 39 more. This results in a total code size of 222 bytes, not counting the FOR/NEXT loop. Contrast that with only 131 bytes for the second example, in which the SELECT portion requires only 26 bytes. Although an extra call is needed to obtain the ASCII value of the extracted character, the lack of a subsequent string assignment more than makes up for that. Further, the tests for 9, 13, and 27 require only 13 bytes each, compared to 27 when CHR$ values were used. The letters test requires 43 bytes, and the numbers test only 23. Clearly this is a significant improvement, especially in light of the small number of tests that are being performed here. In a real program that performs hundreds of string comparisons, replacing those with integer comparisons where appropriate will yield a substantial size reduction. AND, OR, EQV, and XOR When you use AND or OR in an IF test, what is really being compared is
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 58 -
either 0 or -1. That is, BASIC evaluates the *truth* of each expression being tested on both sides of the AND or OR, and a truth in BASIC always results in one or the other of these values. Once each expression has been evaluated, the results are combined using an assembly language AND or OR instruction, and a branch is then made accordingly. Remember that when integers are treated as unsigned, setting all of the bits to 1 results in a value of -1. In chapter 2 I showed how the various logical operators are used to manipulate bits in an integer or long integer variable. The concept is identical when these operators are used for decision-making in a BASIC program. The difference is really more a matter of semantics than definition. That is, the same bit manipulation is performed, only in this case on the result of the truth of a BASIC expression. This is shown in context below, where two test expressions are combined using AND. IF X > CMP MOV JLE DEC 003B: CMP MOV JGE DEC 0046: AND AND JNZ JMP Z = 3 004F: MOV END IF 0055: ... ... 1 AND Y < 2 THEN Word Ptr [X%],1 AX,0 003B AX Word Ptr [Y%],2 CX,0000 0046 CX CX,AX CX,CX 004F 0055 Word Ptr [Z%],3
;compare X% to 1 ;assume False ;we assumed correctly ;wrong, decrement to -1 ;now compare Y% to 2 ;assume False ;we assumed correctly ;wrong, decrement to -1 ;combine the results ;(this is redundant) ;if not 0 assign Z% ;else jump past END IF ;assign Z%
The result of the first comparison is saved in the AX register as either 0 or -1, and the second is saved in CX using similar code. Once both tests have been performed and AX and CX are holding the appropriate values, the registers are then tested against each other using AND. The instruction AND CX,AX not only combines the results, but it also sets the CPU's Zero Flag to indicate if the result was zero or not. Therefore, the second test that uses AND to compare CX against itself to check for a zero result is redundant. At only 2 additional bytes, the impact on a program's size is not terribly significant. However, this shows first-hand the difference between code written by a compiler and code written by a person. OR conditions are handled similarly, except the assembly language OR instruction is used instead of AND. When multiple conditions are being tested using combinations of AND and OR and perhaps nested parentheses as well, additional similar code is employed. There are many situations where all that is really necessary is to test for a zero or non-zero condition. For example, it is common to use an integer variable as a True/False "flag" which can be set in one part of a program, and tested in another. By understanding the underlying code that BASIC creates, you can help BASIC to reduce the size of your programs enormously. In particular, avoiding a comparison with an explicit value lets BASIC generate fewer comparison instructions. The listing below shows how you can test multiple flags using AND, but with much less resulting code than using an explicit comparison.
- 59 -
IF Flag1% AND Flag2% THEN MOV AX,[Flag2%] ;move Flag2% into AX AND AX,[Flag1%] ;AND that with Flag1% AND AX,AX ;(this is redundant) JNZ 0063 ;if not zero assign Z% JMP 0069 ;else skip past END IF Z% = 3 0063: MOV Word Ptr [Z%],3 END IF 0069: ... ... The key here is that zero is always used to represent False, and -1 to represent a True condition. That is, instead of writing IF Flag1% = -1 AND Flag2% = -1, using IF Flag1% AND Flag2% provides the same results. At only 20 bytes of generated code, this method is far superior to tests for an explicit -1 which require 37 bytes. If you recall, in Chapter 2 I showed how the various bits in a variable can be turned on or off with AND. Thus, 1111 AND 1111 equals 1111, while 1111 AND 0000 equals 0. Notice that using 0 and -1 has many other benefits as well. For example, the NOT operator which was also described in Chapter 2 can toggle a variable between those values. If all of the bits in a variable are presently zero, then NOT Variable% results in all ones (-1). This property can also be used to enhance a program's readability, by using NOT much like you would in an English sentence. For example, the code following the line IF NOT Flag% THEN will be executed if Flag% is 0 (False), but it will not be executed if Flag% is -1 (True). In fact, an explicit comparison is optional if you need to test only for a non-zero value. IF Variable <> 0 THEN can be reduced to IF Variable THEN, and the statements that follow will be executed as long as Variable is not 0. Notice that the only saving here is in the BASIC source, since either comparison creates ten bytes of assembler code. But when using long integers, the short form saves five bytes--14 bytes versus 19 for an explicit comparison to zero. NOT is equally valuable when toggling a flag variable between two values. If you have, say, an input routine that keeps track of the Insert key status, then you could use Insert% = NOT Insert% each time you detect that the Insert key was pressed. The first time the operator presses that Key, the Insert flag will be switched from the default start-up value of 0 to -1. Then using Insert% = NOT Insert% a second time will revert the bits back to all zeros. In fact, it is a common technique to define True and False variables (or constants) in a program using this: False% = 0 True% = NOT False% Most programmers understand how to use parentheses to force a particular order of evaluation. By default, BASIC performs multiplication and division before it does addition and substraction. When operators of the same precedence are being used, then BASIC simply works from left to right. However, the order in which logical comparisons are made is not always obvious. This can become particularly tricky if you are using some of the shorthand methods I described earlier. For example, consider the statements IF X AND Y > 12, IF NOT X OR Y, and IF X AND Y OR Z. In the first example, the truth of the expression Y > 12 is evaluated first, with a result of either 0 or -1. Then, that result is combined logically with the value of X using AND. The resulting order of evaluation is performed as if you had used IF X AND (Y > 12). The other expressions are evaluated as IF (NOT X) OR Y and IF (X AND Y) OR Z. The last logical operators we will consider are EQV and XOR. These are used rarely by most BASIC programmers, probably because they are not well understood. However, EQV can dramatically reduce the size of a program in
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 60 -
certain circumstances. It is not uncommon to test if two conditions are the same, whether True or False. EQV stands for Equivalent, meaning it tests if the expressions are the same--either both true or both false. All three program fragments below serve the same purpose, however the first generates 57 bytes, while the second and third create only 16 bytes. IF (X = -1 AND Y = -1) OR (X = 0 AND Y = 0) THEN ... END IF IF X EQV Y THEN ... END IF IF NOT (X XOR Y) THEN ... END IF Although these examples could be replaced with a simple comparison that tests if X equals Y, EQV can reduce other, more elaborate AND and OR tests. For example, you could replace this: IF (X = 10 AND Y = 100) OR (X <> 10 AND Y <> 100) with this: IF X = 10 EQV Y = 100 and gain a handsome reduction in code size. Notice that because of the way EQV works, the third example in the listing above results in identical assembly language code as the second. XOR is true only when the two conditions are different, thus NOT XOR is true when they are the same. One final point worth mentioning is that you can assign a variable based on the truth of one or more expressions. As you saw earlier, every IF test that is used in a BASIC program adds a minimum of 3 extra bytes for a second, unconditional jump. That additional code can be avoided in many cases by assigning a variable based on whether a particular condition is true or not. In the code examples that follow, both program fragments do the same thing, except the first requires 25 bytes compared to only 14 for the second. IF Variable = 20 THEN Flag = -1 ELSE Flag = 0 END IF Flag = (Variable = 20) In either case, the truth of the expression Variable = 20 must be evaluated. However, the IF method adds code to jump around to different addresses that assign either -1 or 0 to Flag. The second example simply assigns Flag directly from the 0 or -1 result of the truth test. Other variants on this type of programming are statements such as A = (B = C), and Flag = (LEN(Temp$) <> 0 AND Variable < 50). Note that the surrounding parentheses are shown here for clarity only, and BASIC produces the same results without them. Short Circuits
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 61 -
There is one important point regarding AND testing you should be aware of. Although the code that BASIC creates to implement these logical tests is very efficient, in some cases a different approach can yield even better results. When many conditions are tested, QuickBASIC creates assembly language code to evaluate all of them before making a decision. This can be wasteful, because often one of the conditions will be false, negating a need to test the remaining conditions. For example, this statement: IF Any$ = "Quit" AND IntVar% > 100 AND Float! <> 0 THEN PRINT "True" requires that all three conditions be tested before the program can proceed. But if Any$ is not equal to "Quit", there is no need reason to spend time evaluating the other tests. The solution is to instead use nested IF tests, preferably placing the most likely (or simplest) tests first, as shown below. IF Any$ = "Quit" THEN IF IntVar% > 100 THEN IF Float! <> 0 THEN PRINT "True" END IF END IF END IF Here, if the first test fails, no additional time is wasted testing the remaining conditions. Further, using the nested IF tests with QuickBASIC also results in less code: 50 bytes versus 64. Note, however, that BASIC PDS [and VB/DOS] incorporate a technique known as *short circuit expression evaluation*, which generates slightly more efficient code when AND is used. With the newer compilers, each condition is tested in sequence, and the first one that fails causes the program to skip over the code that prints "True". But even with this improved code generation, you should still place the most likely tests first. ON GOTO AND ON GOSUB STATEMENTS The last non-procedural control flow statements I will discuss here--ON GOTO and ON GOSUB--are used infrequently by many BASIC programmers. But when you need to test many different values *and* those values are sequential, ON GOTO and ON GOSUB can reduce substantially the amount of code that BASIC generates. For clarity, I will use ON GOTO for most of the examples that follow. Both work in a similar fashion except with ON GOSUB, execution resumes at the next BASIC statement when the subroutine returns. You have already seen that IF/ELSEIF and SELECT CASE blocks are not as efficient as they could be, because the compiler does not know how far ahead the END IF or END SELECT statements are located. Therefore, no matter how trivial the IF or CASE tests being performed are, a pair of jumps is always created even when a single jump would be sufficient. Further, when many tests are necessary, there is no avoiding at least some amount of code for each comparison. This is where ON GOTO can help. Rather than perform a series of separate tests for each value being compared, ON GOTO uses a lookup table which is imbedded in the code segment. This table is merely a list of addresses to branch to, based on the value of the variable or expression being evaluated. If the value being tested is 1, then a branch is taken to the first label in the list. If it is 2, the code at the second label is executed, and so forth. As many as 60 labels can be listed in an ON GOTO statement, although the number being tested can range from 0 to 255. If the value is 0 or higher than the number of items in the list, the ON GOTO command is ignored, and execution resumes with the statement following the ON GOTO. Negative values or values higher than 255 cause an "Illegal function call" error.
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 62 -
A simple example showing the basic syntax for ON GOTO is shown below. INPUT "Enter a value between 1 and 3: ", X ON X GOTO Label1, Label2, Label3 PRINT "Illegal entry!" END Label1: PRINT "You pressed 1" END Label2: PRINT "You pressed 2" END Label3: PRINT "You pressed 3" END Notice that the more labels there are, the bigger the savings in code size. ON GOTO adds a fixed overhead of 70 bytes, 61 of which is the size of the library routine that evaluates the value and actually jumps to the code at the appropriate label. The remaining 9 bytes are needed to load the value being tested and pass that on to the ON GOTO routine. However, for each label in the list, only 2 bytes are required in the lookup table to hold the address. Compare that to SELECT CASE which requires 6 bytes of set-up code (when an integer is being tested), and 13 bytes more to process each CASE. Thus, the crossover point at which ON GOTO is more efficient is when there are 6 or more comparisons. Notice that if ON GOTO is used in more than one place in a program, the savings are even greater because the 61-byte library routine is added only once. Again, ON GOTO has the important restriction that all of the values must be sequential. However, this limitation can also be turned into a feature by taking advantage of the inherent efficiency of lookup tables. Using a lookup table is a very powerful technique, because you can determine a result using an index rather than actually calculating the answer. A lookup table is commonly used to determine log and factorial functions, since those calculations are particularly tedious and time consuming. With a lookup table you would calculate all of the values once ahead of time, and fill an array with the answers. Then, to determine the factorial for, say, the number 14, you would simply read the answer from the fourteenth element in the array. You can apply this same technique in BASIC using a combination of INSTR and ON GOTO or ON GOSUB. Although INSTR is intended to find the position of one string within another, it is also ideal for looking up characters in a table. Imagine you have written an input routine that must handle a number of different keys, and branch according to which one was pressed. One way would be to use an IF/ELSEIF or SELECT CASE block, with one section devoted to each possible key. But as you saw earlier, once there are more than 5 keys to be recognized, either of those constructs are less efficient than ON GOTO. The approach I often use is to combine INSTR and ON GOSUB to branch according to which function key was pressed. The beauty of this method is that a value of zero (or one that is out of range) causes control to fall through to the next statement. Therefore any keys that are not explicitly being tested for are simply ignored. This is shown in context below. DO DO K$ = INKEY$ 'wait for a key press
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 63 -
Length% = LEN(K$) LOOP UNTIL Length% IF Length% = 2 THEN 'it's an extended key Code$ = RIGHT$(K$, 1) 'isolate the key code and branch accordingly ON INSTR(";<=>?@ABCD", Code$) GOSUB ... END IF LOOP UNTIL K$ = CHR$(27) 'until they press Esc
Here, extended keys are identified by a length of 2, and the key code is then isolated with RIGHT$. The punctuation and letters within the quotes are characters 59 through 68, which correspond to the extended codes for F1 through F10. (A list of all the extended key codes is in your BASIC owner's manual.) Of course, any arbitrary list of key codes could be used. Further, the key codes do not need to be contiguous. For example, to branch on the Up arrow, Down arrow, Ins, Del, PgUp, and PgDn keys you would use "HPRSIQ" as the source string. Any other mix of characters could also be used, including Alt keys. Another interesting and clever trick that combines INSTR and ON GOTO lets you test multiple keys regardless of capitalization. The short program below accepts a character, and uses INSTR to look it up in a table of upper and lower case character pairs. PRINT "Yes/No/Load/Save/Retry/Quit? "; DO K$ = INKEY$ LOOP UNTIL LEN(K$) = 1 ON (INSTR("YyNnLlSsRrQq", K$) + 1) \ 2 GOTO ... After adding 1 and dividing that by 2, the result will indicate in which character pair the choice was found. This technique could also be extended to include 3- or 4-character groups, or any other combination of characters. Since any value between 0 and 255 is legal for an ASCII character, INSTR can be used in other, more general lookup situations as well. A COMPARISON OF SUBROUTINE METHODS ================================== There are four primary subroutine types that BASIC supports: GOSUB subroutines, DEF FN functions, called subprograms, and what I refer to as "formal functions". Each has its own advantages and disadvantages, which I will describe momentarily. But I would first like to introduce several terms that will be used throughout the discussion that follows. The first is *module*, which is a series of BASIC program statements kept in their own separate source file. All modules have a main portion, and some also have procedures within a SUB or FUNCTION block. The main portion of a program is that which receives control when the program is first run. When a program is comprised of multiple modules, each additional module has a main portion, although code within that portion is rarely executed. In fact, there are only two ways to access code in the main portion of an ancillary module: One is to create a line label and use that as the target for ON ERROR or another "ON" event. The other is to define a DEF FN function and invoke the function. The second term is *variable scope*, which indicates where in a program a variable may be accessed. Variables that are used in the main portion of a program are accessible anywhere else in the main, but not within a SUB or FUNCTION block. Likewise, a variable that is defined within a SUB or
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 64 -
FUNCTION is by default private to that procedure. The overwhelming advantage of private variables is that you do not have to worry about errors caused by inadvertently using the same variable name twice. The third term is *SHARED*, and it overrides the default private scope of a variable used in a procedure. SHARED may be used in either of two ways. If it is specified with a DIM statement in the main body of a program--that is, DIM SHARED Variable--the variable is established as being shared throughout the entire source file. Even though DIM is usually associated with arrays, it can be used this way to extend a variable's scope. SHARED may also be used within a subroutine to share one or more variables with the main portion. Notice that the statement SHARED Variable inside a procedure defines the variable as being shared with the main portion of the program only. SHARED used within a procedure does not share the named variable with any other procedures. The only exception is when other procedures also use SHARED with the same variable name. In that case they are shared between procedures, as well as with the main program.
- 65 -
+-----------------------------+ DEFINT A-Z DIM SHARED Var1 +--+-->Var1 = 100 +----+-->Var2 = 200 CALL Sub1(Var2) CALL Sub2(Var2) END SUB Sub1 (Param) STATIC +--+---->Var1 = Param Var2 = Var1 END SUB SUB Sub2 (Param) STATIC SHARED Var2 +--+---->Var1 = Param +-----+---->Var2 = Var1 END SUB +-----------------------------+ Figure 3-1: How SHARED and DIM SHARED affect variable scope. that share the same identity are shown connected. Variables
The fourth term is *COMMON*, which is related to SHARED in that it also lets you share variables among procedures. However, COMMON has the additional property of allowing variables to be shared by procedures that are not in the same physical source file. When BC compiles your program, it translates your variable names to memory addresses. Thus, those names are not available when the program is linked to other object files. Variables that are listed in a COMMON statement are placed in a separate portion of the data segment which is reserved just for that purpose. Therefore, other program modules using COMMON can also access those variables in that portion of DGROUP.
- 66 -
[Link] +-----------------------------+ DEFINT A-Z COMMON SHARED Var1 +-----+-->Var1 = 100 +--+-->Var2 = 200 CALL Sub1(Var2) CALL Sub2(Var2) END SUB Sub1 (Param) STATIC +----+---->Var1 = Param Var2 = Var1 END SUB SUB Sub2 (Param) STATIC SHARED Var2 +----+---->Var1 = Param +--+---->Var2 = Var1 END SUB +-----------------------------+ [Link] +-----------------------------+ DEFINT A-Z COMMON Var1 +-----+-->Var1 = 100 +--+-->Var2 = 200 CALL Sub1(Var2) CALL Sub2(Var2) END SUB Sub1 (Param) STATIC Var1 = Param Var2 = Var1 END SUB SUB Sub2 (Param) STATIC SHARED Var2 Var1 = Param +--+---->Var2 = Var1 END SUB +-----------------------------+ Figure 3-2: How COMMON and COMMON SHARED affect variable scope. that share the same identity are shown connected. Variables
- 67 -
COMMON can also be combined with SHARED, to specify that one or more variables be shared throughout the main program as well as with other modules. That is, the statement COMMON SHARED Variable tells BASIC that Variable is to be both DIM SHARED and COMMON. To establish a TYPE variable as COMMON, you must state the type name as well: COMMON TypeVar AS MyType. In all cases, COMMON statements must precede the executable statements in a program. The only statements that may appear before COMMON are other non-executable statements such as DECLARE, CONST, and '$STATIC. Because the variable names listed in a COMMON statement are not stored in the final program, the names used in one module do not need to be the same as the corresponding names in another module. You could, for example, have COMMON X%, Y$, Z# in one file, and COMMON A%, B$, C# in another. Here, X% refers to the same memory location as A%; Y$ is the same variable as B$, and so forth. It is imperative, however, that the order and type of variables match. If one file has an integer followed by a string followed by a double precision variable, then all other files containing a COMMON statement must have their COMMON variables in that same order. This is one good reason for storing all COMMON statements in a single include file, which is included by each module that needs access to the COMMON variables. One or more arrays may also be listed as COMMON; however, the rules are different for static and dynamic arrays. When a dynamic array is to made COMMON, it should be dimensioned in the main program only, following the COMMON statement. (But you may use REDIM in another module if necessary, to change the array's size.) Static arrays must be dimensioned in each module, before the associated COMMON declaration. Of course, all array types must match across modules--you may not list a static array as the first COMMON item in one file, and then list a dynamic array in that same position in another file. There are actually two forms of COMMON statement: the blank COMMON and the named COMMON. The examples shown thus far are blank COMMON statements. A named COMMON block lets you specify selected variable groups as COMMON, to avoid having to list many variables when all of them are not needed in a given module. A COMMON block is named by preceding the variable list with a name surrounded by slash characters. For instance, this line: COMMON /IntVars/ X%, Y%, Z% establishes a named COMMON black called IntVars. By creating several such named blocks you may share only those that are actually needed in a given module. In this case, the block name is stored in the object file, and LINK ensures that the COMMON variables in each module share the same addresses. One important limitation of a named COMMON block is that it cannot be used to pass information between programs that use CHAIN. The fifth term is *STATIC*, which I described in a slightly different context in the section about data in Chapter 2. When you add the STATIC option to a SUB or FUNCTION definition, BASIC treats the variables within that procedure very differently than when STATIC is omitted. With STATIC, memory in DGROUP is allocated by the compiler for each variable, and that memory is permanently reserved for use by those variables. When STATIC is not specified, the variables in the routine are by default placed onto the system stack. This means that sufficient stack memory must be available, although that memory can then be used again later for variables in other procedures. An important side effect of using the stack for variable storage is that the memory is cleared each time the subprogram or function is entered. Therefore, all numeric variables are initialized to zero, and strings are initialized to null. Any arrays within a non-static procedure are by default dynamic, which means they are created upon entry to the routine and erased when the routine exits. STATIC also has an additional meaning in subprograms and functions; it can establish variables as being private to a procedure. If a variable has been declared as shared throughout a module by using DIM SHARED in the main
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 68 -
portion of the program, using the statement STATIC Variable inside the subroutine will override that property. Thus, Variable will be local to the procedure, and will not conflict with a global shared variable of the same name. STATIC within a subprogram or function also lets you use the same name for a variable that was already given to a named constant. Many programmers find the use of the term STATIC for two very different purposes confusing, and rightly so. It would have made more sense to use a different keyword, perhaps LOCAL, to limit a variable's scope. And to further confuse the issue, the '$STATIC metacommand is used to establish the memory storage method for arrays. None the less, STATIC always indicates that memory for a variable is permanently allocated, and it may also specify that a variable is private to a procedure. The final term I want to introduce now is *recursion*. The classic definition of a recursive procedure is that it may call itself. While this is certainly true, that doesn't really explain what recursion is all about, or how it could be useful. I will cover recursion in depth momentarily, but for now suffice it to say that recursion is often helpful when manipulating tree-structured information. For example, a program that lists all of the files on a hard disk would most likely be based on a recursive subroutine. Such a program would first change to the root directory, and then call the routine to read and display all of the file names it finds there. Then for each directory under the current one, the routine would change to that directory and call itself again to read and display the files in that directory. And if more directories were found at the next level down, the routine would call itself yet again to process all of those files too. This continues until all of the files in all directories on the hard disk have been processed. Another application for recursion is a subroutine that sorts an array on more than one key. For example, consider a TYPE array in which each element has components for a first name, a last name, and address fields. You might want to be able to sort that array first by last name, then by first name, and then by zip code. That is, all of the Smiths would be grouped together, and within that group Adam would be listed before John. All of the John Smiths would in turn be sorted in zip code order. By employing recursion, the routine would first sort the entire array based on the last name only. Next, it would identify each range of elements that contain identical last names. The routine would then call itself to sort that subgroup, and call itself again to sort the subgroup within that group based on zip code. SUBROUTINES VERSUS FUNCTIONS There is a fundamental difference between subroutines and functions. A subroutine is accessed with either a CALL or GOSUB statement, and a function is invoked by referencing its name. In general, a subroutine is used to perform an action such as opening a group of files, or perhaps updating a screen-full of information. A function, on the other hand, returns a value such as the result of a calculation. A string function also returns information, although in this case that information is a string. Notice that the type of information returned by a function is independent of the type of parameters, if any, that are passed to it. For example, BASIC's native STR$ function accepts a numeric argument but returns a string. Likewise, a numeric function such as INSTR accepts two strings and returns a single integer. This is also true for functions that you design using either DEF FN or FUNCTION. Although a function is primarily used for calculations and a subroutine for performing one or more actions, there is no hard and fast distinction between the two. You could easily design a subroutine that multiplies three numbers and returns the answer in one of the parameters. Similarly, a function could be written to clear the screen and then open a file. Which you use and when will depend on your own programming style. However, there are definite advantages to using functions where appropriate. One immediately obvious benefit of a function is that a value can be
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 69 -
returned without requiring an additional passed parameter. Each variable that is passed as a parameter requires 4 bytes of code for setup, plus an additional 5 bytes within the subroutine each time it is accessed. Another important advantage of using a function is BASIC's automatic type conversion. If you assign a single precision variable from the result of an integer function, BASIC will convert the data from one format to the other transparently. In fact, a simple assignment from a variable of one type to that of another type is also handled for you by the compiler. But if a routine is written to pass the value back as a parameter, then you must use whatever type of data the subprogram expects. Although most high-level languages require the programmer to match explicitly the types of data being assigned, Microsoft BASIC has done this automatically since its inception. When you write Var1! = Var2%, BASIC treats that as Var1! = CSNG(Var2%). Object oriented programming languages use the term *polymorphism* to describe such automatic type conversion. GOSUB ROUTINES The primary advantage a GOSUB routine holds over all of the other subroutine types is that it can be accessed very quickly. Translated to assembly language a GOSUB statement is but three bytes in length, and its speed is surpassed only by a GOTO. When the only thing that matters is how fast a subroutine can be called, GOSUB has the clear advantage. However, there are many limitations inherent in a GOSUB. The most important restriction is that arguments cannot be passed using GOSUB. Therefore, any variables must be assigned before invoking the routine, and possibly reassigned when it returns. For example, if a subroutine requires two parameters--perhaps a row and column at which to print a message--those variables must be assigned before the GOSUB can be used. And if a value is being returned, your program must know the name of the variable that was assigned within the GOSUB routine. Another important limitation is that the target line label must be in the same block of code as the GOSUB. Although a GOSUB is legal within a SUB or FUNCTION, both the GOSUB and the routine it calls must be located in the same procedure. Likewise, a GOSUB in the main body of a program cannot access a subroutine inside a procedure, or vice versa. [And of course you cannot invoke a GOSUB routine that is located in a different source module.] Both of these problems restrict your ability to reuse a subroutine in more than one program. One of the goals of modern structured programming is the ability to design a routine for one application, and also use it again later in other programs. The only way to do that using GOSUB routines is to establish a variable naming convention, and always use variables and line labels with those unique names. SUBPROGRAMS Subprograms were introduced with QuickBASIC version 2.0, and they improve greatly on GOSUB routines in many respects. The most important advantages of a subprogram are that it accepts passed parameters, and that variables used within the subprogram are local by default. Besides the obvious benefit of not having to worry about variable naming conflicts, these properties allow you to create your own toolbox of useful subroutines, and use them repeatedly in different programming projects. I will discuss this use of subprograms in detail later in this chapter. A subprogram is accessed using the CALL statement, and any number of arguments may optionally be passed to the routine. A subprogram is defined with a statement of the form SUB SubName (Param1, Param2, ...) STATIC. The parameters and surrounding parentheses are optional, as is the STATIC directive. Of course, the number of arguments passed to a subprogram must match the number of parameters it expects. As you can see, subprograms have many advantages over GOSUB routines. However, they are not a magical panacea for every programming problem.
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 70 -
Each subprogram includes a fixed amount of overhead just to enter and exit it. Because of the complexities of accessing incoming parameters, a *stack frame* must be created by the compiler upon entry. A stack frame is simply a fancy name for an area of memory that holds the addresses of the incoming parameter. However, this requirement adds a fair amount of code to each subprogram. Eight bytes of code are needed to set up and call the internal BASIC routine that creates the stack frame, and the routine itself comprises another 35 bytes. Eight more bytes are needed to call the routine that exits a subprogram, and that routine adds contains 26 bytes. Finally, all but the last subprogram in a source file needs a 3-byte jump to skip over the other subprograms that follow. Therefore, a total of 80 bytes are added to any program that uses a subprogram rather than a GOSUB routine. It is important to point out, however, that the 61 bytes used by the library routines to enter and exit a subprogram are added to the final .EXE file only once. It is also worth mentioning that BASIC PDS provides the /Ot switch, which eliminates the usual overhead incurred from calling the routines needed to enter and exit a subprogram. Although using /Ot avoids the code that is otherwise added, there is one important restriction: You may not use a GOSUB within the subprogram. When a program performs a GOSUB, the address to return to is placed onto the stack, for retrieval later when the subroutine returns. Likewise, when a subprogram is called, both a segment and address to return to are put on the stack. If a GOSUB were used inside the subprogram and an EXIT SUB was then encountered within the GOSUBed subroutine, the return addresses on the stack would be out of order. Thus, the subprogram would return to the wrong place, with undoubtedly disastrous consequences. To avoid this, BASIC by default saves the address to return to when the subprogram is first entered, and uses that when it is exited. Therefore, when the compiler sees that a GOSUB is being used, it does not use the abbreviated method even if /Ot has been specified. Although using /Ot makes a subprogram (and function) much faster by eliminating the overhead to call the entry and exit routines, there is no actual savings in code size. A series of assembler NOP (No Operation) instructions are placed where the entry and exit code would have been. However, those empty instructions are never executed. We can only hope that in future releases of BASIC PDS Microsoft will improve BC's code generation to eliminate these unnecessary instructions. [Yeah, right.] Another problem with subprograms is that programmers tend to use them to excess. For example, I have seen people create subprograms to increment and decrement integer variables even though it is far more efficient to do that with in-line code. The statement X% = X% + 1 creates only 4 bytes of code, compared to 9 for a single call to a subprogram to do the same thing! However, incrementing long integer or floating point variables does take more code than invoking a subprogram with a single parameter, so a subprogram could be useful in that case. Only by counting the number of times a subprogram will be used and comparing that to the overhead incurred can you determine whether there will be any savings. DEF FN FUNCTIONS Although a DEF FN function is designed to return a result, it is more closely related to a GOSUB subroutine in actual operation. Like a GOSUB routine it is invoked with a 3-byte assembly language "near" call, as opposed to the 5-byte "far" call that subprograms and formal functions require. And while a DEF FN function can accept incoming parameters, variables within the function definition are by default shared with the main portion of the program. As I already explained, variables used in a DEF FN function can be made private to the function only by explicitly declaring them as STATIC. However, at least it is possible to employ local variables. Further, a DEF FN function can return a result, which makes it an ideal replacement for GOSUB when speed is paramount.
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 71 -
Internally, parameters are passed to a DEF FN function very differently than to a called subprogram or formal function. Arguments are passed to a subprogram by placing their addresses on the stack. With a DEF FN function, however, a copy of each parameter is created, and the function directly manipulates those copies. Therefore, it is impossible for a DEF FN function to modify an incoming parameter directly. This behavior is neither good nor bad. Rather, it is simply different and thus important to understand. It is also important to understand that a DEF FN function can be used only in the module in which it is defined. If the same function is needed in different modules, the same code must be duplicated again and again. In the manuals that come with QuickBASIC and BASIC PDS, Microsoft advises against using DEF FN functions, in favor of the newer, more powerful formal functions. Because of this favoritism, Microsoft will probably never correct one disturbing anomaly that is present in all DEF FN functions. When a string is passed as an argument to a DEF FN function, a copy is made for the function to manipulate. Unfortunately, the copy is never deleted! Therefore, if you pass, say, a 10,000 byte string to a DEF FN function, that amount of memory is permanently taken until the function is invoked again later. The short listing below proves this behavior. DEF FnWaste (A$) FnWaste = ASC(A$) END DEF Big$ = SPACE$(10000) PRINT FRE(Big$) X = FnWaste(Big$) PRINT FRE(Big$) Notice that running this program in the QuickBASIC editing environment will not give the expected (memory-wasting) result. However, in a separately compiled program the 10000 byte loss will be evident. As with subprograms, there is a fixed amount of overhead required to enter and exit a DEF FN function. For each function that has been defined, 5 bytes are needed to call the Enter and Exit routines. Further, these routines are 14 and 24 bytes in length respectively. But again, the routines themselves are added to a program only once when it is linked. There are two final limitations of DEF FN functions worth mentioning here. The first is that arrays and TYPE variables may not be passed as parameters to them. Since by design a copy is made of every incoming parameter, there is no reasonable way to do that with an entire array. The second limitation is that the function definition must be physically positioned in the source file before any references are made to it. FORMAL FUNCTIONS A formal function is nearly identical to a called subprogram, and it requires the exact same amount of overhead to enter and exit. Also like subprograms, nearly any type of data may be passed to a function, including TYPE variables and arrays. The only limitation is that a fixed-length string may not be used directly as a parameter. If a fixed-length string is passed to a subprogram or function that expects a string, a copy is made and assigned to a conventional string. This copying was described in detail in Chapter 2. Because a formal function is invoked by referencing its name in an assignment or PRINT statement, it is essential that it be declared. After all, how else could BASIC know that the statement PRINT MyFunc means to call a function and display the result, as opposed to printing the variable named MyFunc? When a BASIC function is created in the BASIC editing environment, a corresponding DECLARE statement is generated automatically. But when a function is written in another language or kept in a Quick
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 72 -
Library, an explicit declaration is mandatory. Like subprograms, formal functions are ideally suited to modular, reusable programming methods. Furthermore, a function may be accessed from any module in an entire application, even those in other source files. Indeed, the only difference between a subprogram and a function is that a function returns a result. The assembly language code that BASIC generates is in all other respects identical. STATIC VERSUS NON-STATIC PROCEDURES As I stated earlier, when the STATIC keyword is appended to a SUB or FUNCTION declaration, all of the variables within the routine are assigned a permanent address in DGROUP. And when STATIC is omitted, the variables are instead stored on the stack and cleared to zeros or null strings each time the routine is entered. There are several important ramifications of this behavior. Non-static procedures allocate new stack memory each time they are invoked, and then release that memory when they exit. It is therefore possible to exhaust the available stack space when the subroutine calls are deeply nested. For example, if you call one subprogram that then calls another which in turns calls yet another, sufficient stack memory must be available for all of the variables in all of the subprograms. Besides the memory needed for each variable in a subprogram or function, other data is also placed onto the stack as part of the call. For each parameter that is passed, 2 bytes are taken to hold its address. Add to that 4 bytes to store the segment and address to return to in the calling program. Finally, temporary variables that BASIC creates for its own purposes are also stored on the stack in a non-static subprogram or function. Another important consideration when STATIC is omitted is that every string variable must be deleted before the subprogram exits. Because of the way BASIC's string management routines operate, memory that holds string descriptors and string data cannot simply be abandoned. Every string must be released explicitly by a called routine, at a cost of 9 bytes per string. Please understand that you do not have to delete these strings. Rather, this is another case where BASIC creates additional code without telling you. Again, I would love to be able to tell you that using STATIC is always desirable, or that never using it always makes sense. But unfortunately, it just isn't that simple. When a program becomes very large and complex, only by counting variables can you be absolutely certain how much stack space is really needed. Although the FRE(-2) function may be used to determine how much stack memory is currently available, it does not tell how much memory is actually needed by each routine. To summarize the trade-offs between static and non-static variables: Static variables are allocated permanently by the compiler, and the memory they occupy can never be used for any other purpose. Non-static variables are placed onto the stack, and exist only while the subprogram or function is in use. Remember that you can also have a mix of static and non-static variables in the same procedure. By omitting STATIC after the subroutine name, all variables will by default be non-static. You can then override that property for selected variables by using the STATIC keyword. In the section on debugging in Chapter 4, you will learn how to use CodeView to determine the stack requirements for a procedure's variables. Controlling the Stack Size There are several ways to control the amount of memory that is dedicated for use by the stack. All versions of BASIC support the CLEAR command, which takes an optional argument that sets the stack size. The statement CLEAR , , StackSize sets aside StackSize bytes for the stack. Unfortunately, CLEAR also clears all of the data in a program, closes any open files, and erases all arrays. If you know ahead of time how much stack memory will be needed, then using CLEAR as the first statement in a
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 73 -
program will not cause a problem. Even when CLEAR is used as the first statement in a program, there is still one situation where that will not be acceptable. When you use CHAIN to execute a subsequent program, a CLEAR statement in that program will clear all of the variables that have been declared COMMON. Fortunately, there are two solutions to this problem: BASIC PDS offers the STACK statement, which lets you establish the size of the stack but without the side effects of CLEAR. For example, the statement STACK 5000 sets aside 5000 bytes for the stack. The other solution is to use the /STACK: link switch, which reserves a specified number of bytes. All of the options that LINK supports are described in Chapter 5. RECURSION I have already illustrated some of the situations in which a recursive subprogram or function could be useful. Now lets look at some actual programming examples. The Evaluate function in the listing below uses recursion to reinvoke itself for each new level of parentheses it encounters. DECLARE FUNCTION Evaluate# (Formula$) INPUT "Enter an expression: ", Expr$ PRINT "That evaluates to"; Evaluate#(Expr$) FUNCTION Evaluate# (Formula$) 'Search for an operator using INSTR as a table lookup. If found, 'remember which one and its position in the string. FOR Position% = 1 TO LEN(Formula$) Operation% = INSTR("+-*/", MID$(Formula$, Position%, 1)) IF Operation% THEN EXIT FOR NEXT 'Get the value of the left part, and a tentative value for the 'right part. LeftVal# = VAL(Formula$) RightVal# = VAL(MID$(Formula$, Position% + 1)) 'See if there's another level to evaluate. Paren% = INSTR(Position%, Formula$, "(") 'There is, call ourselves for a new RightVal#. IF Paren% THEN RightVal# = Evaluate#(MID$(Formula$, Paren% + 1)) 'No more to evaluate, do SELECT CASE Operation% CASE 1 Evaluate# = LeftVal# CASE 2 Evaluate# = LeftVal# CASE 3 Evaluate# = LeftVal# CASE 4 Evaluate# = LeftVal# END SELECT END FUNCTION When you run this program, enter an expression like 15 * (12 + (100 / 8)). To keep the code to a minimum, Evaluate accepts only simple, two-number expressions. That is, it will not work with more than one math operator
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 74 -
the appropriate operation and exit. 'addition + RightVal# 'subtraction - RightVal# 'multiplication * RightVal# 'division / RightVal#
within each pair of parentheses as in 10 * (3 + 4 + 5). However, the parentheses may be nested to nearly any level. This function begins by examining each character in the incoming formula string for a math operator. If it finds one the operator number (1 through 4) is remembered, as well as its position in the formula string. Next, VAL is used to obtain the value of the digits to the left of the operator, as well as the digits to the right. Notice that it was not necessary to use LEFT$ to isolate the left-most portion of the string, because VAL stops examining the string when it encounters any non-digit character such as the "+" or "(". Once these values have been saved, the next test determines if any more parentheses follow in the formula. If so, Evaluate calls itself, passing only those characters that are beyond the next parenthesis. Thus, the same routine evaluates each new level, returning to the level above only after all levels have been examined. I encourage you to run this program in the QuickBASIC editing environment, and step through each statement one by one with the F8 Trace command. In particular, use the Watch Variable feature to view the value of Position% and LeftVal# as the function recurses into subsequent invocations. It is important to understand the need for stack variables in this program, and why STATIC must not be used in the function definition. When Evaluate walks through the incoming string and determines which math operator is specified, that operator must be remembered throughout the course of the function. If a static variable were used for Operation%, then its previous value would be destroyed when Evaluate calls itself. Likewise, LeftVal# cannot be overwritten either, or it would not hold the correct value when Evaluate returns to itself from the level below. Therefore, as you step through this program you will observe that each new invocation of Evaluate creates a new set of variables. As you can see, stack variables are necessary for the proper functioning of a subprogram or function that calls itself. They are also necessary when one procedure calls another procedure which in turn calls the first one again. The key point is that each time a non-static routine is invoked, new and unique variables must be created. Otherwise, the variable contents from a previous level above will be overwritten. Although recursion is a powerful and necessary technique, it should be used only when necessary. There is a substantial amount of overhead needed to allocate stack memory and clear it to zeros, so invoking a non-static routine is relatively slow. And as I described earlier, every non-static string variable must be deleted when the routine exits, at a cost of 9 bytes apiece. Some programmers use recursion even when there are other, more efficient ways to solve a problem. For example, the QuickBASIC manual shows a recursive function that calculates a factorial. (A factorial is derived by multiplying a number by all of the whole numbers less than itself. That is, the factorial of 4 equals 4 * 3 * 2 * 1.) However, a factorial can be calculated faster and with less code using a simple FOR/NEXT loop as shown below. This version of Factorial is 20 percent faster than the example given in the QuickBASIC manual. FUNCTION Factorial#(Number%) STATIC Seed# = 1 FOR X% = 1 TO Number% Seed# = Seed# * X% NEXT Factorial# = Seed# END FUNCTION PASSING PARAMETERS TO PROCEDURES As you have already learned, BASIC normally passes data to a subprogram or function by placing its address on the stack. And when an entire array is specified, the address of the array descriptor is sent instead. But there
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 75 -
are some cases where BASIC imposes restrictions on how variables and arrays may be passed to a procedure. Let's look now at some of the ways to get around those restrictions. When using versions of BASIC earlier than PDS 7.1, it is not legal to pass an array of fixed-length strings. In fact, it is also impossible to pass a single fixed-length string directly. As you saw in Chapter 2, BASIC copies every fixed-length string argument to a regular string, which adds a lot of code and also wastes string memory. The simplest solution for fixed-length strings is to define an equivalent TYPE that is comprised of a single string component. Since a TYPE variable or array can legally be passed, this is the easiest and most direct approach, as shown here. TYPE FLen S AS STRING * 100 END TYPE DIM MyString AS Flen CALL Subprogram(MyString) SUB Subprogram(FLString AS FLen) ... ... END SUB If the subprogram being called is in a separate module, then the TYPE definition must also be present in that file. However, the DIM statement is needed only in the program that passes the string. This also works with fixed-length string arrays, except that the DIM would have to be changed to DIM MyArray(1 TO NumElements) AS FLen, and the subprogram's definition would be changed to SUB Subprogram(FLString() AS FLen). BASIC PDS 7.1 supports passing a fixed-length string array directly, so this work-around is not needed with that version. Curiously, a single fixed-length string may not be passed as a parameter in BASIC 7.1. Since a fixed-length string is closely related to a TYPE variable, this limitation seems arbitrary at best. BASIC 7.1 also supports the use of BYVAL when passing numeric arguments to procedures. This is a particularly powerful feature, because it can greatly reduce the amount of code needed to access those values within the routine. It also eliminates the need to make copies when a constant is passed as an argument. To take advantage of this feature, you simply specify BYVAL in both the calling and receiving argument list, as shown below. DECLARE SUB Subroutine(BYVAL Arg1%, BYVAL Arg2%) CALL Subroutine(Var1%, Var2%) SUB Subroutine(BYVAL X%, BYVAL Y%) ... ... END SUB Because the actual value of the argument is being passed, there is no way to return information back to the caller. But in those situations where an assignment to the original variable from within the routine is not needed, BYVAL can eliminate a lot of compiler-generated code when dealing with integers. Of course, you may use a mix of BYVAL and non-BYVAL parameters if you need the benefits of both methods in a single call. As proof of this savings, disassemblies of a one-statement subprogram designed both ways is presented below, to show how an integer parameter is accessed when it is passed by address and by value.
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 76 -
SUB ByAddress(Param%) STATIC LocVar% = Param% MOV SI,[Param%] ;get the address of Param% MOV AX,[SI] ;then read the value there MOV LocVar%,AX ;assign that to LocVar% END SUB SUB ByValue(BYVAL Param%) STATIC LocVar% = Param% MOV AX,Param% ;read Param% directly MOV LocVar%,AX ;and assign it to LocVar% END SUB Note that the savings are only within the subroutine, and not when it is called. That is, 4 bytes are needed to pass an integer variable whether by address or by value. In fact, passing larger data types requires more code to pass by value. Any variable can be passed by address with 4 bytes of compiler-generated code, because what is sent is a single address. But to pass a double precision number by value requires 16 bytes, since 4 bytes of code are needed for each 2-byte portion of the number. In general, passing variables as parameters to a subprogram or function is preferable to sharing them. When many variables are shared throughout a program, you run the risk of introducing bugs caused by accidentally using the same variable name more than once. However, sharing has some definite advantages in at least two situations. The first is when a procedure must be accessed as quickly as possible. Since a finite amount of code is needed to pass each parameter, some amount of time is also required to execute that code. Therefore, sharing a few, carefully selected variables can improve the speed of your programs and reduce their size as well. Another important use for SHARED is to conserve data memory. Nearly all programs use at least a few temporary scratch variables, perhaps as FOR/NEXT loop counters. By dimensioning several such variables as being shared throughout a program, the same variables can be used repeatedly. I often begin programs with a DIM SHARED statement such as DIM SHARED X, Y, Z, and then use those variables as often as possible. One final trick I want to share is how to pass a large number of parameters using less code than would normally be necessary. Each argument that is passed to a procedure requires 4 bytes of code. In a complicated routine that needs many parameters, this can quickly add up. Worse, these bytes are added for every call. Therefore, a subprogram that accepts 10 parameters and is called 20 times will add 800 bytes to the final executable file just to handle the parameters! One solution is to use an array, which is ideal when all of the parameters are the same type of data. An entire array can be passed as a single parameter since only the array descriptor's address is needed. Even better, however, is to create a TYPE variable, and then assign all of the parameters to it. A TYPE variable can hold nearly any amount and type of data, and it too can be passed using only 4 bytes. Although this does require a separate assignment for each TYPE component, you simply use the TYPE where the regular variables would have been assigned. By eliminating the added code to pass many parameters, programs that use a TYPE this way will also be much faster. MODULAR PROGRAMMING QuickBASIC versions 4.0 and later let you load subprograms and functions from multiple files into the editing environment at the same time. This further enhances their reusability, since the different modules can be treated as "black boxes" whose purpose is already known. Once a routine has been developed and debugged, it can be used again and again, without further regard for the names of the variables within the routines. Indeed,
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 77 -
many of the utility routines included with this book are provided as separate modules, intended to be loaded along with your programs. Any variable name can be passed as an argument to a procedure, even if a different name is used to represent the same variable within the procedure. If you have defined a subprogram such as SUB MySub(X%, Y!, Z$), then you could call it using CALL MySub(A%, B!, C$). Of course, the variables you pass must be of the same data type as the subroutine expects. Because reusability is an important consideration in the design of any procedure, it generally makes sense to store it in its own source file. This lets you combine the same module repeatedly with any number of programs. The alternative would be to merge the file into each program that needs it. But maintaining multiple copies of the same code wastes disk space. Further, if a bug is found in the routine, you will have to identify all of the programs that contain it, and manually correct each one of them. Another important advantage of using separate files is that you can exceed the usual 64K code size barrier. Unlike the data segment which is comprised of the sum of all data in all modules, an .EXE file can contain multiple code segments. Each BASIC module has a single code segment, and each of these can be as large as 64K. In fact, dividing a program into separate files is the *only* way to exceed the usual 64K code size limitation. Although using a separate source file for each subprogram makes sense in many situations, there is one slight disadvantage. When all of the various program modules are linked together, each separate module adds approximately 100 bytes of overhead. None the less, for all but the smallest programming projects, the advantages of using separate modules will probably outweigh the slight increase in code size. INCLUDE FILES Another useful BASIC feature that can help you to create modular programs is the Include file. An Include file is a separate file that is read and processed by BASIC at a specified place in your program. The statement '$INCLUDE: 'filename' tells QB or BC to add the statements in the named file to your source code, as if that code had been entered manually. If a file extension is not given, then .BAS is assumed. Many of the files that Microsoft provides with QuickBASIC use a .BI extension, which stands for "BASIC Include". Some programmers use .INC, and you may use whatever seems appropriate to the contents of the file. Include files are ideal for storing DECLARE, CONST, TYPE, and COMMON statements. Except for COMMON, none of these statements add to the size of your program, and none of them create any executable code. Therefore, you could create a single include file that is used for an entire project, and add an appropriate '$INCLUDE directive to the beginning of each program source file. Unused DECLARE and CONST statements and TYPE definitions are ignored by BASIC if they are not referenced. However, they do impinge slightly on available memory within the QuickBASIC editor, since BASIC has no way to know that they are not being used. Similarly, BC must keep track of the information in these statements as it compiles your program. But again, there is no impact on the size of your final executable program. In general, I recommend that you avoid placing any executable statements into an include file. Because the code in an include file is normally hidden from your view, it is easy to miss a key statement that is causing a bug. Likewise, a '$DYNAMIC or '$STATIC command hidden within an include file will obscure the true type of any arrays that are subsequently dimensioned. Perhaps worst of all is placing a DEFINT or other DEFtype statement there, for the same reason. QUICK LIBRARIES Quick Libraries contribute to modular programming in two important ways. Perhaps the most important use for a Quick Library is to allow access to
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 78 -
subprograms and functions that are not written in BASIC. All DOS programs and subroutines--regardless of the language they were originally written in--end up as .OBJ files suitable for LINK to join together. But the QB and QBX editing environments manipulate BASIC source code, and interpret the commands rather than truly compile them. Therefore, the only way you can access a routine written in assembly language or C within QuickBASIC is by placing the routine into a Quick Library. Quick Libraries also let you store completed BASIC subprograms and functions out of the way from the rest of your program. If you have a large number of subroutines in one program, the list of names displayed when F2 is pressed can be very long and confusing. Since QuickBASIC does not display the routines in a Quick Library, there will be that many fewer names to deal with. Another advantage of placing pre-compiled BASIC routines into a Quick Library is that they can take less memory than when the BASIC source code is loaded as a module. This is true especially when you have many comments in the program, since comments are of course not compiled. Be aware that there are a few disadvantages to placing BASIC code into a Quick Library. One is that you cannot step and trace through the code, since it is not in its original BASIC source form. Another is that Quick Libraries are always stored in normal DOS memory, as opposed to expanded memory which QBX [and VB/DOS] can use. When a BASIC subprogram or function is less than 16K in size and EMS is present, QBX [and VB/DOS] will place its source code in expanded memory to free up as much conventional memory as possible. ERROR AND EVENT HANDLING ======================== As a BASIC programmer, there are several types of errors that you must deal with in a program. These errors fall into two general categories: compile errors and runtime errors. Compile errors are those that QB or BC issue, such as "Syntax error" or "Include file not found". Generally, these are easy to understand and correct, because the QuickBASIC editor places the cursor beneath the offending statement. In some cases, however, the error that is reported is incorrect. For example, if your program uses a function in a Quick Library that expects a string parameter and you forgot to declare it, BASIC reports a "Type mismatch" error. After all, with a statement such as X = FuncName%(Some$), how could BASIC know that FuncName% is not simply an integer array? Assuming that it is an array, BASIC rejects Some$ as being illegal for an element number. Runtime errors are those such as "File not found" which are issued when your program tries to open a file that doesn't exist, or is not in the specified directory. Other common runtime errors are "Illegal function call", "Out of string space", and "Input past end". Many of these errors can be avoided by an explicit test. If you are concerned that string space might be limited you can query the FRE("") function before dimensioning a dynamic string array. However, some errors are more difficult to anticipate. For example, to determine if a particular directory exists you must use CALL Interrupt to query a DOS service. The conventional way to handle errors is to use ON ERROR, and design an error handling subroutine. There are a number of problems with using ON ERROR, and most professional programmers try to avoid using it whenever possible. But ON ERROR does work, and it is often the simplest and most direct solution in many programs. The short listing below shows the minimum steps necessary to implement an error handler using ON ERROR. ON ERROR GOTO HandleErr FILES "*.XYZ" END HandleErr: SELECT CASE ERR
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 79 -
CASE 53: PRINT "File not found" CASE 68: PRINT "Device unavailable" CASE 71: PRINT "Disk not ready" CASE 76: PRINT "Path not found" CASE ELSE: PRINT "Error number"; ERR END SELECT RESUME NEXT The statement ON ERROR GOTO HandleErr tells BASIC that if an error occurs, the program should jump to the HandleErr label. Without ON ERROR, the program would display an error message and then end. Since it is unlikely that you have any files with an .XYZ extension, BASIC will go to the error handler when this program is run. Within the error handling routine, the program uses the ERR function to determine the number of the error that occurred. Had line numbers been used in the program, the line number in which the error occurred would also be available with the ERL function. In this brief program fragment, the most likely error numbers are filtered through a SELECT CASE block, and any others will be reported by number. Regardless of which error occurred, a RESUME NEXT statement is used to resume execution at the next program statement. RESUME can also be used with an explicit line label or number to resume there; if no argument is given BASIC resumes execution at the line that caused the error. In many cases a plain RESUME will cause the program to enter an endless loop, because the error will keep happening repeatedly. In this case, the file will not exist no matter how many times BASIC tries to find it. Therefore, a plain RESUME is not appropriate following a "File not found" or similar error. Had the error been "Disk not ready", you could prompt the user to check the drive and then press a key to try again. In that case, then, RESUME would make sense. Although BASIC's ON ERROR can be useful, it does have a number of inherent limitations. Perhaps the worst problem with ON ERROR is that it often increases the program's size. When you use RESUME NEXT, you must also use the /x compile switch. Unfortunately, /x adds internal address labels to show where each statement begins, so the RESUME statement can find the line that caused the error. These labels are included within the compiled code and therefore increases its size. Another problem with ON ERROR is that it can hide what is really happening in a program. I recommend strongly that you REM out all ON ERROR statements while working in the QuickBASIC editing environment. Otherwise, an Illegal function call or other error may cause QuickBASIC to go to your error handler, and that handler might ignore it if the error is not one you were expecting and testing for. If that happens and your program uses RESUME NEXT, you might never even know that an error occurred! Yet another problem with ON ERROR is that it's frankly a clumsy way to program. Most languages let you test for the success or failure of the most recent operation, and act on or ignore the results at your discretion. Pascal, for example, uses the IOResult function to indicate if an error occurred during the last input or output operation. Finally, BASIC generates errors for many otherwise proper circumstances, such as the FILES statement above. You might think that if no files were found that matched the .XYZ extension given, then BASIC would simply not display anything. Indeed, an important part of toolbox products such as Crescent Software's QuickPak Professional are the routines that replace BASIC's file handling statements. By providing replacement routines that let you test for errors without an explicit ON ERROR statement, an add-on library can help to improve the organization of your programs. As I mentioned earlier, some errors can be avoided by using CALL Interrupt to access DOS directly. (One important DOS service lets you see if a file exists before attempting to open it.) But critical errors such as those caused by an open drive door require assembly language. In Chapter 12 you will learn how to bypass BASIC and access DOS directly using CALL Interrupt.
- 80 -
EVENT HANDLING BASIC includes several forms of event handling, and like ON ERROR, these too are avoided when possible by many professional programmers. Event handling lets your programs perform a GOSUB automatically and without any action on your part, based on one or more conditions. Some of the more commonly used event statements are ON KEY, ON TIMER, and ON COM. With ON KEY, you can specify that a particular key or combination of keys will temporarily halt the program, and branch to a GOSUB routine designated as the ON KEY handler. ON TIMER is similar, except it performs a GOSUB at regular intervals based on BASIC's TIMER function. Likewise, ON COM performs a GOSUB whenever a character is received at the specified communications port. The concept of event handling is very powerful indeed. For example, ON COM allows your program to go about its business, and also handle characters as they arrive at the communications port. ON TIMER lets you simulate a crude form of multi-tasking, where control is transferred to a separate subroutine at one second intervals. Unfortunately, BASIC's event handling is not truly interrupt driven, and the resulting code to implement it adds considerably to a program's size. When any of the event handling methods are used, BASIC calls an interval event dispatcher periodically in your program. These calls add five bytes apiece, and one is added at either every statement, or at every labeled statement [depending on whether you compiled using /v or /w respectively]. This can increase your program's size considerably. Even worse, the repeated calls have an adverse effect on the speed of most programs. Like ON ERROR, BASIC's event handling statements provide a simple solution that is effective in many programming situations. And also like ON ERROR, they are best avoided in important programming projects. Using purely BASIC techniques, the only alternative to event trapping is polling. Polling simply means that your program manually checks for events, instead of letting BASIC do it automatically. The primary advantage of polling is that you can control when and where this checking occurs. The disadvantage is that it requires more effort by you. To see if any characters have been received from a communications port but are still waiting to be read you would use the LOF function. And to see if a given amount of time has elapsed you must query the TIMER function periodically. If true interrupt driven event handling were available in BASIC, that would clearly be preferable to either of the two available methods. However, only with Crescent's P.D.Q. product can such capability be added to a BASIC program. PROGRAMMING STYLE Programming style is a personal issue, and every programmer develops his or her own particular methods over time. Some aspects of programming style have little or no impact on the quality of the final result. For example, the number of columns you indent a FOR/NEXT loop will not affect how quickly a sort routine operates. But there are style factors that can help or harm your programs. One is that clearly commenting your code will help you to understand and improve it later. Another is when more than one programmer is working on a large project simultaneously. If neither programmer can figure out what the other is doing, the program's quality will no doubt suffer. Clearly, no one can or even should try to force a particular style or ideology upon you. However, I would like to share some of the decisions that I have made over the years, and explain why they make sense to me. Of course, you are free to use or not use these opinions as you see fit. Programmers are as unique and varied as any other discipline, and no one set of rules could possibly serve everyone equally. Whatever conventions you settle upon, be consistent above all else. The most important convention that I follow is to use DEFINT A-Z as the first statement in every program. For me, using integers verges on religion, and my fingers could type DEFINT even if I were asleep. As I
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 81 -
have stated repeatedly, integers should be used whenever possible, unless you have a compelling reason not to. Integers are much faster and smaller than any other variable type BASIC offers. Nearly all of the available third party add-on products use integers parameters wherever possible, and so should the routines you write. The only reasonable exception to this is when writing financial or scientific programs, or other math-intensive applications. Equally important is adding sufficient and appropriate comments. Some programmers like to use comment headers that identify each related block of code; others prefer to comment every line. I recommend doing both, especially if other people will be reading your programs. I also prefer using an apostrophe as a comment delimiter, rather than the more formal REM. There are only so many columns available for each comment line, and it seems a shame to waste the space REM requires. When writing a subprogram or function that you plan to use again in other projects, include a complete heading comment that shows the purpose of the routine and the parameters it expects. If each parameter is listed neatly at the beginning of the file, you can create a hardcopy index of routines by printing that section of each file. Avoid comments that are obvious or redundant, such as this: Count = Count + 1 'increment Count If Count is keeping track of the number of lines read from a file, a more appropriate comment would be 'show that another line was read. Also avoid comments that are too cute or flip. Simply state clearly what is happening so you will know what you had in mind when you come back to the program next month or next year. Selecting meaningful variable names is equally valuable in the overall design of a program. If you are keeping track of the current line in a file, use a variable name such as CurLine. Although BASIC in some cases lets you use a reserved word as a variable name, I recommend against that. Over the years, different versions of BASIC have allowed or disallowed different keywords for variables. While QuickBASIC 4.5 lets you use Name$ as a variable, there is no guarantee that the next version will. Also, be aware that variables names which begin with the letters Fn are illegal, because BASIC reserves that for user-defined functions. Using the variable FName$ to hold a file name may look legal, but it isn't. Don't be ashamed to use GOTO when it is appropriate. There are many places where GOTO is the most direct way to accomplish something. As I showed earlier in this chapter, GOTO when used correctly can sometimes produce smaller and faster code than any other method. Use line labels instead of line numbers. The statement GOSUB 1020 doesn't provide any indication as to what happens at line 1020. GOSUB OpenFile, on the other hand, reads like plain English. The only exception to this is when you are debugging a program that crashes with the message "Illegal function call at line no line number". In that case, you should *add* line numbers to your program and run it again. A program that reads a source file and prints each line to another file with sequential numbers is trivial to write. I will also discuss debugging in depth in Chapter 4. Even though using DEFINT is supposed to force all subsequent CONST, DEF FN, and FUNCTION declarations to be integer, a bug in QuickBASIC causes untyped names to occasionally assume the single precision default. Therefore, I always use an explicit percent sign (%) to establish each function's type. In fact, I use whatever type identifier is appropriate for functions and CONST statements, to make them easily distinguishable in the program listing. For example, in the statement IF CurRow > MaxRows% THEN CurRow = MaxRows%, I know that MaxRows% has been defined as a constant. Some people prefer to use all upper-case letters for constants, though I prefer to reserve upper case for BASIC keywords. Although BASIC supports the optional AS INTEGER and AS SINGLE directives when defining a subprogram or function, that wastes a lot of screen space. I greatly prefer using the variable type identifiers. That is, I will use SUB MySub(A%, B!) rather than SUB MySub(A AS INTEGER, B AS SINGLE). The same information is conveyed but with a lot less effort and screen clutter.
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 82 -
A well-behaved subroutine will restore the PC to the state it was when called. If you have subprogram that prints a string centered on the bottom line of the screen, use CSRLIN and POS(0) to read the current cursor location before you change it. Then restore the cursor before you exit. I like to indent two spaces within FOR/NEXT and IF/THEN blocks. Although some people prefer indenting four or even eight columns for each level, that can quickly get out of hand when the blocks are deeply nested. Nothing is harder to read than code that extends beyond the edge of the screen. But whatever you do, please *do not* change the tab stop settings in the QuickBASIC editor, unless you are the only one who will ever have to look at your code. Even though the program may look fine on your screen, the indentation will be completely wrong on everyone else's PC. When creating a dynamic array I prefer REDIM to a previous '$DYNAMIC statement. REDIM is clearer because it shows at the point in the source where the array is dimensioned that this is a dynamic array. Otherwise you have to scan backwards through your source code looking for the most recent '$DYNAMIC or '$STATIC, to see what type of array it really is. By the same token, using ever-changing DEFtype statements throughout your code is poor practice. Further, if a variable is a string, always include the dollar sign ($) suffix when you reference it. If you use DEFSTR S or even worse, DIM xxx AS STRING and then omit the dollar sign, nobody else will understand your program. I also prefer to explicitly dimension all arrays, and not let BC create them with the 11-element default (including element zero). If you need less than 11 elements, the memory is wasted. And if you need more, then your program will behave unpredictably. Not dimensioning every array is sloppy programming. Period. Avoid repeated calls to BASIC's internal functions if possible. In the listing below, the first example creates 61 bytes of code, while the second generates only 46 bytes. Not recommended: IF CSRLIN = 1 OR CSRLIN = 6 OR CSRLIN = 12 THEN ... END IF Much better: Temp = CSRLIN IF Temp = 1 OR Temp = 6 OR Temp = 12 THEN ... END IF As I stated earlier in this chapter, using SELECT CASE instead of IF will also eliminate this problem. Many BASIC statements are translated into calls, and each call takes a minimum of five bytes. Your programs will be easier to read if you evaluate temporary expressions separately. Even though BASIC lets you nest parentheses to nearly any level, nothing is gained by packing many expressions into a single statement. In the examples below that strip the extension from a file name, the first creates only a few bytes less code. Although this may seem counter to the other advice I have given, a slight code increase is often more than offset by a commensurate improvement in clarity. File$ = LEFT$(File$, INSTR(File$, ".") - 1) Dot = INSTR(File$, ".") File$ = LEFT$(File$, Dot - 1)
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 83 -
The last issue I want to discuss is how to pronounce BASIC keywords and variable names. Don't laugh, but many programmers have no idea how to communicate the words LEFT$ or VARSEG over the telephone. Some people say "X dollar" for X$ even though "X string" is so much easier to say. Another keyword that's hard to verbalize is VARPTR. I prefer "var pointer" since it is, after all, a pointer function. CHR$(13) is pronounced "character string thirteen", again because that's the clearest and most straight forward interpretation. Likewise, INSTR is pronounced "in string" and LEFT$ would be said as "left string". If you're not sure how to pronounce something, use the closest equivalent English wording you can think of. SUMMARY In this chapter you have learned how BASIC's control flow statements are constructed, and how the compiler-generated code is similar regardless of which statements are used. You also learned where GOSUB and GOTO should be used, and when subprograms and functions are more appropriate. The discussion on logical operations showed how AND, OR, EQV, and XOR operate, and how they can be used to advantage in your programs. I have explained in detail exactly what recursion is, and how recursive subroutines can perform services that are not possible using any other technique. You have also learned about the importance of the stack in recursive and other non-static subroutines. Passing parameters to subprograms and functions has also been described in detail, along with some of the principles of modular program and event handling. Finally, I have shared with you some of my own personal preferences regarding programming style, and when and how such conventions can make a difference. Although this is a personal issue, I firmly believe it is important to develop a consistent style and stick with it. In Chapter 4 you will learn debugging methods using both the QuickBASIC editing environment and Microsoft's CodeView debugger. The successful design of a program is but one part of its development. Once it has been written, it must also be made to work correctly and reliably. As you will learn, there are many techniques that can be used to identify and correct common programming errors.
CHAPTER 4 DEBUGGING STRATEGIES There are many individual components which contribute to a completed application. The logical flow of the program must be determined, the user interface must be designed, and appropriate algorithms must be selected. But no matter how much effort you devote to the design and implementation of a program, the bottom line is it must also work correctly. In an ideal scenario, you would begin writing a program by first jotting down some notes that describe its operation. Next, you would create an outline listing each of the program's major components. You would then determine all of the subroutines and functions that are needed, and perhaps even create a flow chart showing each of the paths that could be taken. Properly prepared for any situation that might arise, you finally write the actual code and find that it works perfectly. Now, what's wrong with this picture? Few people actually program that way! In practice, many programmers simply start coding with little forethought and no detailed plan. They begin with the first statement and continue to the last, occasionally reworking portions into subroutines as necessary. After all, planning is not nearly as much fun as programming, and everyone knows that fun is the most important part. Believe it or not, I agree. There's nothing really wrong with plodding through a program,
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 84 -
stabbing here and there until it works. Indeed, some great algorithms developed out of aimless doodling. I have personally never drawn a flow chart, and I have no plans to start now. What I will address here is how to find and correct problems when they do occur. There are more things that can go wrong with a program than can go right, and tracking down an elusive "Illegal function call" error that appears only occasionally is definitely not much fun. How quickly you can solve these problems is directly related to your understanding of programming in general, and to your familiarity with the tools available. In this chapter you will learn how to identify problems in your programs, and also how to solve them. Programming errors, or bugs, can be as simple as a misspelled variable name, and as complex and ornery as an internal flaw in BASIC itself. The BASIC editing environment provides a wealth of powerful debugging features, and understanding how to use them will help you produce programs that are reliable and error free. COMMON PROGRAMMING ERRORS ========================= There are three distinct types of programming errors: simple misspellings and other naming or syntax errors, incorrect logic such as misunderstanding or incorrectly coding an algorithm, and failing to understand some of the finer points of the BASIC language. No matter how carefully you type, no matter how much forethought you apply to a particular problem, and no matter how often you read the BASIC manuals, it is impossible to completely avoid making mistakes. The first category includes those errors caused by simple mistakes such as misspelling a variable or procedure name. Trying to call a subprogram that doesn't exist will be immediately obvious, because BASIC gives you an error message before the program can be run. But an incorrect variable name will return the wrong results with no warning. Passing the wrong number of arguments to a procedure may or may not be reported, depending on whether the routine has been declared. Assembly language routines in a Quick Library can be particularly pesky in this regard. Although BASIC automatically generates a DECLARE statement for BASIC subprograms and functions you have loaded in source form, it does not do this for routines in a Quick Library. If you call an assembly language routine incorrectly, you will probably crash the PC. However, it is also possible to corrupt string memory and not know it. Worse, a "String space corrupt" error is often not reported until much later in the program. If you run the short program below in the QuickBASIC 4.5 editor, it will appear to operate correctly. X$ = SPACE$(1000) 'create a string POKE SADD(X$) - 2, 100 'corrupt string memory PRINT "Testing" X% = 1 PRINT "More testing" X% = 2 PRINT "Yet more testing" X% = 3 Here, the POKE statement is overwriting the back pointer that belongs to X$, which is one type of string corruption that can occur. But QuickBASIC doesn't know that this has happened, because it has no reason to check the integrity of its string memory until another string assignment is made. However, adding the statement PRINT FRE("") anywhere after the POKE command causes BASIC to check string memory, and report the error. Even if your program does not use POKE, calling a procedure incorrectly can cause it to overwrite memory in this fashion. Another simple error is inadvertently using the same variable name twice, or omitting a type declaration character from a variable name. For
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 85 -
example, if you are using a variable named Bytes& to track how many bytes of a file have been read, accidentally using Bytes later on will give the wrong results. If a DEFINT statement is in effect, then Bytes will be an integer variable. Otherwise, it will be single precision which is also incorrect. Unless you use the DIM...AS statement to declare a variable explicitly, BASIC lets you have different variables with the same name. That is, Var%, Var!, and Var# can all coexist in the same program, and each is a unique variable. Similarly, using the wrong variable entirely will cause your program to operate incorrectly, and again with no error message displayed. More than once I have had a program with one FOR loop nested within another, and used the outer loop counter variable when I meant to use the inner one. Another common situation is caused by changing the name of a variable during the course of writing a program. For example, you may have a variable named BPtr that tracks where you are reading within a buffer. If you later decide to change that name to BufPointer because it is more meaningful, you must also remember to change all occurrences of the name. Of course, BASIC's search and replace feature minimizes that problem. More important, though, you must make a mental note to use the new name as you continue to develop the program. Forgetting to declare a function can also lead to incorrect results that produce no warning. If an integer function is not declared, then BASIC will dimension an array with that name if the function expects a numeric argument. When BASIC encounters the statement X = FuncName%(Y%) it assumes that FuncName% is an integer array, and create an array containing the default 11 elements. In this case X will be assigned a value of zero, or you will receive a "Subscript out of range" error if Y% is not between 0 and 11. I once observed an unexplainable "Out of string space" error that was caused by the statement Size = ScreenSize%(ULRow, ULCol, LRRow, LRCol). ScreenSize% was a function present in a Quick Library, but without a DECLARE statement BASIC created a 4-dimensional integer array. LOGIC ERRORS ============ The second cause of bugs is logic errors, and these include adding when you meant to subtract, or using the wrong variable altogether. Programs that manipulate pointers (variables that hold the addresses of other variables) are particularly prone to errors in logic. Another common logic error is forgetting to trim the leading or trailing blanks from a file or directory name before using it. If the operator enters " c:\[Link]" and you try to open that file, BASIC will report a "Bad file name" error. Another cause of logic errors is failing to consider all of the things a user may enter. An inexperienced operator is likely to enter data that you as the programmer would never consider, or select menu items in an order that makes no sense. Indeed, never underestimate the value of beta testers. After you have exhausted all of the possibilities you can think of, give the program to a 4 year old child, and ask him or her to try it while you watch. Your uncle Ernie would be a good beta tester too, and the less he knows about your program, the more valuable his contribution will be. People who know absolutely nothing about computers have an uncanny knack for creating "Illegal function call" errors in a program that you just know is perfect. Similarly, you must consider all of the possible error conditions that could happen in a program. In an error handler that has a CASE statement for each possibility you anticipate, also include a CASE ELSE clause for those you haven't thought of. The short listing that follows shows a typical error handler that incorporates this added safety measure. ON ERROR GOTO HandleErr ... ... HandleErr:
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 86 -
SELECT CASE ERR CASE 7, 14 PRINT "Out of memory" CASE 24, 25, 27 PRINT "Fix the printer" CASE 53 PRINT "File not found" CASE ELSE PRINT "Error number"; ERR END SELECT ... ... The CASE ELSE clause lets you accommodate any possibility, and your user can then at least report to you what the error number was. This simple example doesn't include all of the possibilities, but you can certainly see the general concept. Another common logic error is using the same file number twice. When a file has been opened as #1, that number remains in use until the file is closed. This can be problematical when writing reusable modules, since there is no way to know which files may be in use by the main program. Some programmers use #99 or another unlikely number in a routine that will be reused in many programs. But even that approach is flawed, because you have to remember which numbers are used by which routines. BASIC's FREEFILE function is intended to solve this problem, and it returns the next available file number. Be sure to save the results FREEFILE returns, however, since the value will change as soon as the next file is opened. The code below shows both the wrong and right ways to use FREEFILE. Wrong: OPEN "[Link]" FOR INPUT AS #FREEFILE INPUT #FREEFILE, X$ 'FREEFILE has changed! CLOSE #FREEFILE Right: FileNum = FREEFILE 'get and save the number OPEN "[Link]" FOR INPUT AS #FileNum INPUT #FileNum, X$ CLOSE #FileNum In the first example if FREEFILE returns, say, a value of 2, then it will return 3 at the INPUT statement which is of course incorrect. Therefore, you must save the value FREEFILE returns, and use that for all subsequent file accesses. This situation also occurs with INKEY$, because once a character has been returned it is no longer available unless you saved it. Two other frequent problems are attempting to use LSET to assign characters into a string that does not exist, and failing to clear a counter variable within a static subprogram or function. The second problem can be especially frustrating, because the routine will work correctly the first time it is invoked. In the function below, a counter returns the number of embedded control characters it finds in a string. FUNCTION CtrlCount%(Work$) STATIC FOR X% = 1 TO LEN(Work$) IF ASC(MID$(Work$, X%, 1)) < 32 THEN Count% = Count% + 1
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 87 -
END IF NEXT CtrlCount% = Count% END FUNCTION The problem here is that Count% retains its value between function invocations. Therefore, each time CtrlCount% is used it will return ever higher values. One solution is to add the statement Count% = 0 at the beginning of the function. Another is to omit the STATIC option from the function definition. UNDERSTANDING BASIC'S QUIRKS The third type of error is caused by not understanding some of BASIC's finer points and quirks. For example, some people do not realize that omitting the third argument from MID$ causes it to return all of the remaining characters in a string. To see if a drive letter was given as part of a file name and if so extract it, you might use a statement such as IF MID$(FileName$, 2) = ":" THEN Drive$ = LEFT$(FileName$, 1). But since the number of characters was not specified to MID$, it returned all but the first character in the string. Unless the string was a drive letter and colon only ("C:"), the test for a colon could never work. The solution, of course, is to use MID$(FileName$, 2, 1). Another instance in which an intimate knowledge of BASIC's idiosyncracies comes into play can affect the earlier example of a file name that contains leading blanks. Most programmers do not use INPUT to accept information, unless the program is very simple and it will be used only occasionally. However, asking for a file name with INPUT is one way to avoid that problem, because INPUT strips all leading and trailing blank spaces, as well as CHR$(9) tab characters. The more useful LINE INPUT, on the other hand, does not strip leading blanks and tabs. Most programmers would never be so foolish as to enter a file name with leading blanks. So this is yet another situation where it is important to consider all of the possibilities. It is also possible to crash a program by using the ASC function when the string might be null. Again, *you* would never press Enter alone in response to a prompt for a file name or other mandatory information, but someone else might. Another BASIC quirk is caused by rounding errors. As you saw in Chapter 2, adding or multiplying many numbers in succession can produce results that are not precisely correct. Instead of checking to see if a value is zero, it is often better to compare it to a very small number. That is, instead of IF Value# = 0 you would use IF Value# < .000001 or IF Value# < .000001 AND Value# > -.000001 or something similar. Also, some numbers simply cannot be represented at all. If you try to enter the statement X# = .00000000001 in the QuickBASIC 4.5 editor, the value will be converted to 9.999999999999999D-12 as soon as you press Enter. Although not technically a BASIC quirk, many programmers forget that variables within a DEF FN function are by default global. Unless you include an explicit STATIC statement listing each variable that is to be local to the function, it is likely that an unexpected change will be made to a variable in the main program. Some programming situations require that you obtain the address of a string variable using SADD. However, SADD is not legal for use with a fixed-length string or the string portion of a TYPE variable. More important, when using BASIC PDS far strings you must also remember to use SSEG to get the string's data segment. Using VARSEG will not create an error; however, the program will not work correctly. Related to that, it is important to remember that strings and dynamic arrays move around in memory--often at unexpected times. The program below appends a zero character to one string for each zero that is found in
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 88 -
another string. Since BASIC may move Work$ during the course of assigning Zero$, this code will fail eventually: Address = SADD(Work$) FOR Y = Address TO Address + LEN(Work$) - 1 IF PEEK(Y) = 48 THEN Zero$ = Zero$ + "0" NEXT Another particularly insidious bug can result if you inadvertently add parentheses around a variable that is passed to a subprogram or function. In the example below, a subprogram that intentionally modifies a parameter has been declared and is then called without the CALL keyword. DECLARE SUB Square(Param%) Square (Value%) SUB Square(Value%) STATIC Value% = Value% * Value% END SUB Because of the unnecessary and incorrect use of parentheses, a copy of the argument is sent to Square instead of the argument itself, with the result that Value% is never actually changed. The fix is to either remove the parentheses, or add the word CALL. Another, related issue is placing a DEFINT after DECLARE statements. In the example below, the parameters X, Y, and Z are assumed by BASIC to be single precision, even though this is clearly not what was intended. DECLARE SUB (X, Y, Z) DEFINT A-Z . . 'X, Y, and Z are singles!
The final issue I want to address here is potential overflow errors. The statement IF IntVar% * 14 > 1000000 can never be true, because BASIC performs integer math assuming an integer range only. Unless you compile your program using the /d debug option, the error will be unreported in a compiled program. If this statement is executed within the QB environment, BASIC will report an overflow error, even though the instruction certainly appears to be legal. But since integer math assumes an integer result, the product of IntVar% times 14 will overflow the range of integer values if IntVar% is greater than 2,340. One solution is to use a long integer for IntVar, and BASIC will then use the range of long integers for the comparison. Using a long integer wastes memory, however, and calculations on long integers are slower and require more code to implement. A much better solution is to use CLNG (Convert to Long), which tells BASIC to assume a long integer result. The statement IF CLNG(IntVar%) * 14 > 1000000 will create a long integer version of IntVar%, and then multiply the result times 14 and use that for the subsequent comparison. Unlike the copies that BASIC makes which steal DGROUP memory, the long integer conversion in this instance is handled within the CPU's registers. CLNG when used this way is really just a compiler directive, as opposed to a called library routine. Another solution is to add an ampersand after the constant 14, thus: IF IntVar% * 14& > 1000000. Again, no additional DGROUP memory is used to handle 14 as a long integer value. Another interesting use of CLNG and CINT--unrelated to debugging but worth mentioning none the less--is to reduce the size of comparison code. When you use a statement such as IF X% > VAL(Some$), a floating point
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 89 -
comparison is performed even if Some$ holds an integer value. By replacing that example with IF X% > CINT(VAL(Some$)) 6 bytes of code can be saved. The CINT tells BASIC that it will not have to perform any floating point rounding when it compares the two values. DEBUGGING AND TESTING TECHNIQUES ================================ When you are developing a large application that is comprised of many individual modules, there are several useful debugging techniques you can employ. One is to create short test-bed programs that exercise each subprogram and function. Finding an error in a complex program with many interdependencies between subroutines can be a tedious prospect at best. If you instead create a small program whose sole purpose is to test a particular subprogram, you will be better able to focus on just that routine. Another useful technique for detecting and preventing sporadic errors is to test your code on "boundary conditions". If you have a routine that reads and process a file in 4K (4096 byte) increments, test it with a file that is exactly 4096 bytes long, as well as with other test files that are 4095 and 4097 bytes long. Perhaps nothing is more frustrating than having a program fail with the message "xxx at line No line number". This message is a throw-back to the days when all BASIC programs had to use line numbers. Now that line numbers are not required in modern compiled BASIC, most programmers do not use them, opting instead for more descriptive line labels when labels are needed at all. When an error does occur and the program has been compiled with /d, BASIC reports the number of the nearest numbered line preceding the line in which the error occurred. A good solution to track down the cause of such errors is to use a variant on a hardware debugging technique known as the "cut in half" method. In a complex electronic circuit that does not work, using this technique means that the circuit is first checked at its mid-point for the correct signal. If the circuit tests correctly at that point, then the error is in the second half. Therefore, the test engineer would "cut in half" again, and test at a point halfway between the middle and the end. If the test fails there, then the problem must lie between the middle of the circuit and that point. In a purely software situation, you would add a line number to a line that falls approximately half-way through the program. If that number is reported, then the problem is occurring in the second half of the program. An enhancement to this technique that I recommend is to add, say, ten line numbers in evenly spaced increments throughout the program. This will let you quickly isolate the problem to a much smaller portion of the program. Besides the line number (or lack of line number) that BASIC reports, the segment and address at which the error occurred is also reported. This is information is frankly useless in a purely BASIC environment. You must either use CodeView to identify the line that is associated with the error, or view the assembly language output that BC can optionally generate. These will be described in the section on advanced debugging later in this chapter. Finally, it is important to point out that you should never use ON ERROR while a program is being developed. ON ERROR can hide programming errors that you need to know about. As an example, a LOCATE statement with incorrect values will generate an "Illegal function call" error. But if ON ERROR is in effect and your program uses RESUME NEXT for errors it is not expecting, you may never even know that an error occurred. If you run the complete program below you can see that there is no indication that an error occurred at the obviously illegal LOCATE statement. CLS ON ERROR GOTO HandleErr LOCATE 100, -90
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 90 -
PRINT "My program seems to work fine." END HandleErr: RESUME NEXT USING THE QB AND QBX EDITING ENVIRONMENTS The single most powerful debugging feature that is available to you is the BASIC editing environment. More than just an editor that you can use to enter program statements, the QB environment is exactly that: a complete editing environment for developing and testing BASIC programs. The BASIC editor lets you enter program statements, single-step through a program, examine variable values, and much more. Besides being able to execute commands singly and in sequence, you can also trace into subroutines and functions, and even run your program in reverse. The primary advantage of using the QB environment instead of a separate editor is the enhanced debugging capabilities. In most high-level languages, you first write a program using an editor, and then compile and run it to see if it works correctly. If an error occurs, you must start the editor again, load your program, and study the code to see what went wrong. In contrast, QB lets you run your program at the same time it is being edited. You can even modify the program while it is running and then resume execution, view and change variable values, and change the order in which statements are executed. Further, BASIC can be instructed to stop and return to the edit mode when the program reaches a certain statement, or when a particular logical condition becomes true. For example, you can tell BASIC to halt the program when a variable takes on a specified value. These are extremely powerful debugging tools which have no equal in any other language. In the sections that follow, I will describe each of these capabilities in detail. STEP AND TRACE DEBUGGING Early versions of Microsoft BASIC offered a very primitive trace capability that displayed the line numbers of the currently executing statements. Although this was better than nothing, interpreting a blur of line numbers flashing by on the screen required a lot of mental effort. When Microsoft introduced QuickBASIC version 3.0 they added greatly improved debugging in the form of a step and trace feature. To activate step and trace you would enter a STOP statement at a selected point in the source code. When the program reached that point you could then execute each statement in sequence by pressing a function key. QuickBASIC 3 also provided the ability to display continuously the value of a single variable in a window at the top of the screen. QuickBASIC 4.0 offered an improved version of this feature, using additional function keys to control how a program proceeds. This method has been continued with little change through current versions of QuickBASIC and BASIC PDS. Of course, the primary reason you would want to step through a program one statement at a time is to determine why it is not working. For example, if you have code that opens a file for output but the file is never created, you would step through that portion of the code to see which statements are being executed and which are not. In particular, stepping through a program lets you see which path an IF or CASE test is taking. Two function keys are used to single-step through a program, and four additional options are available to assist program debugging. Each time the F10 key is pressed, the current statement is executed and the program advances to the next statement. If you have just loaded the program being tested, you will press F10 once to get to the first instruction. Pressing F10 again executes that statement, and continues to the next one. If the current statement is related to screen activity, the screen is switched momentarily to display the program's output rather than the source code.
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 91 -
The screen is also switched during a CALL statement or function invocation, in case that routine performs screen output. You can optionally toggle between viewing the output and edit screens manually by pressing F4. In some cases you may want to treat a subroutine as a single statement, which is what F10 does. That is, CALL MySub is handled as single statement, and all of the statements within the routine are executed as one operation. In other cases, however, you may need to trace into a subprogram, GOSUB routine, DEF FN, or function, to step through its statements as well. This is what F8 is for. When F8 is pressed at a CALL or GOSUB statement or function invocation, BASIC traces into the procedure and lets you watch as it executes each statement individually. Two additional capabilities let you navigate a program more quickly. Pressing F7 tells BASIC to execute all of the statements up to the current cursor location. This way, you are spared from having to watch a long sequences of commands that you know are working correctly. For example, stepping through a FOR/NEXT loop that initializes 1000 elements in an array is usually pointless. Therefore, when you reach that spot in the program you would manually move the cursor to the statement following the NEXT, and press F7. It is also possible to force execution to a particular point in the program using the "Set next statement" option of the Debug menu. Unlike F7, though, the statements that precede the selected line will not be executed. Therefore, this option is equivalent to adding a temporary GOTO to the program, causing it to jump to the specified line. One of the most powerful features of the BASIC editor is that you can actually modify your program, then resume execution. In earlier versions of QuickBASIC, making even the slightest change to a program--even if only to a single comment--the entire program would have to be recompiled. BASIC can now preserve variable values and indeed the entire program state during most types of editing operations. The last important step operation I want to mention now is the History feature. This too must be selected from a menu, and using it will slow your program's operation considerably. When the History option is selected from the Debug menu, BASIC remembers the last 25 program statements, and lets you step through your program in reverse. For example, if a variable has taken on an incorrect value, you can walk backwards through the program to see what statements caused that to happen. Where F8 steps forward through your program, Shift-F8 instead steps backward. WATCH VARIABLES AND BREAK POINTS As powerful as BASIC's single-step feature is, it is only half of the story. Equally important is the Watch capability that lets you view a program's variables in real time. One or more variables may be placed into a special Watch window at the top of the editing screen, and their values will be displayed and updated after each statement is executed. Between the Step and Watch features, you can observe all aspects of your program's operation as it is executing. Besides watching variable values, you can also monitor complex expressions and function results. For example, you could watch the value of X% * Y% + Z%, ASC(Work$), or the result of a function such as StrFunction$(Array$(), Count%). Because each variable or expression is updated after every program statement, your program will run more slowly when many items are displayed in the watch window. However, this is seldom a problem in a debugging situation, and the ability to see precisely what is happening far outweighs the minor speed penalty. Being able to watch the results of expressions as well as simple variables offers some useful and interesting techniques. As an example, suppose you are watching a string variable named Buffer$. If Buffer$ is very long, you can use LEFT$ or MID$ to watch just a portion of the string: MID$(Buffer$, CurPointer%, 70). This expression displays the 70-character portion of Buffer$ that is currently pointed to by CurPointer% (assuming, of course, you are using variables with those names). Likewise, if you are observing a string but nothing is showing in the
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 92 -
watch window, you could watch "{" + Work$ + "}". This displays "{}" if the string is null, and shows if there are leading or trailing blanks or CHR$(0) bytes. Adding braces also lets you see if the string contains characters that begin past the edge of the visible window. One particularly powerful use of BASIC's Watch capability is related to the fact that all of the expressions are evaluated anew at each statement. Earlier I mentioned how insidious "String space corrupt" errors can be, because BASIC checks the integrity of its string memory only when a string is being assigned. Therefore, watching the expression FRE(Any$) tells BASIC to evaluate string memory after every source line. Thus, as soon as string memory is corrupted it will be immediately reported. This technique can be extended to identify a "Far heap corrupt" error as well, by watching the expression FRE(-1). Besides the Step and Watch capabilities, there are two additional features you should understand: Break Points and Watch Points. When a program is very large and complex, it becomes impractical to step and trace through every statement. Also, in some cases you may not know at which statement an error is occurring. Pressing F9 sets up a Break Point which tells BASIC to halt when it reaches that point in the program, regardless of how it arrived there. You can have multiple break points, and the program will run normally until the specified statement is about to be executed. Simply place the cursor on the line at which the program is to stop, and press F9. That line will be highlighted to show that it is currently a Break Point. Pressing F9 again removes the Break Point. A Watch Point tells BASIC to execute the program, until a certain condition becomes true. Some examples of Watch Points are X% = 100, ABS(Total#) > 1000, and FRE("") < 1000. In the first example you are telling BASIC to stop the program and return to the editor when X% equals 100. The second example will stop the program when the absolute value of Total# exceeds 1000, and the third halts it when there are less than 1000 bytes of string space remaining. Considered together, these debugging features are extremely powerful. You can tell BASIC, in effect, "Run until the value of Count% hits 14; then stop the program, and let me walk backwards through the program to see how that happened." USING /D TO DETECT ERRORS Another very powerful debugging solution at your disposal is to compile your program with the /d debug option. When creating an .EXE file in the BASIC environment from the Run menu, you would select the "Produce debug code" option. Compiling with /d tells BC to add three important safeguards to the code it generates. Some of these debugging issues were described in Chapter 1, but they deserve elaboration here. The first code addition is a call to a central event handler prior to every BASIC program statement, to detect if Ctrl-Break was pressed. Normally, a compiled BASIC program is immune from pressing Ctrl-Break and Ctrl-C, unless the program is processing an INPUT statement. BASIC adds break checking to let you get out of an endless loop or other similar situation, without having to reboot your computer. The second addition is an overflow test following each integer and long integer addition, subtraction, and multiplication, to detect results that exceed the range of legal values. If you have a statement such as X% = Y% * Z% and the result after multiplying is greater than 32767, the overflow test will detect that and produce an error message. Otherwise, X% would be assigned an erroneous value and your program would have no way to detect it. Floating point operations do not need any additional testing, because overflows are detected and reported whether or not /d is used. The last additional code that BASIC adds when /d is used is array element bounds checking. If you have dimensioned an array and attempt to assign an element that doesn't exist, a compiled BASIC program will normally ignore the error. For example, if an array has been dimensioned using DIM Array%(1 TO 100) and you then have the statement Array%(200) =
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 93 -
12, BASIC will store the value 12 at what would have been the 200th element. This can lead to disastrous consequences such as overwriting an element in another array, or corrupting string memory. When /d is used BASIC adds additional code to check every array element referenced, and reports an error if that element does not exist. Because of the added checking for overflow errors and illegal element numbers, a program compiled with /d will be larger and run more slowly than one in which /d is not used. Therefore, you should not release a program for general use that has been compiled with the debug option. One exception worth noting is that QuickBASIC versions 4.0 and 4.5 contain a bug that generates incorrect code for certain long integer array operations. The only solution when that happens is to use /d. This way, the routine that calculates element addresses and checks for illegal element numbers is used, rather than the incorrect in-line code that BC produces directly. You could also compile with the /ah (huge array) switch, which uses the same routine to calculate and check array element addresses. Using /ah has an advantage over /d in this case, because your program will not be halted if Ctrl-Break is pressed. Using /ah also avoids the extra code and time to check for overflow errors. However, /ah affects dynamic arrays only, and errors with static arrays will not be prevented. When a program is run in the BASIC editor, the same protection that /d provides is employed. This added debug testing within the editor is one more contributor to its slowness when compared to a fully compiled program. ADVANCED DEBUGGING Although being able to step through your program and watch its variables in the BASIC editing environment is very powerful, there are still some limitations inherent in that process. For example, it is possible that a program will work perfectly in the editor, but not when it has been compiled to an .EXE program. Microsoft has tried to make the BASIC editor as compatible with BC as possible, but the editor is an interpreter and not a true compiler. There are bound to be some differences in how the program runs. Another limitation is that some programs are just too large to be run within the editor. Finally, if you receive an error message from an executable program that lists only a segment and address, there is no way to determine where the error occurred using the editor. In these cases you will need to work with the actual compiled program. To relate an error address to the original BASIC source statement you must be able to see the assembly language code that BC generates, along with the original BASIC source. One way to do this is with the Microsoft CodeView debugger. CodeView comes with BASIC PDS [and VB/DOS Professional Edition] as well as with Microsoft's Macro Assembler. CodeView provides a debugging environment that is similar to the QB editor, except it is intended for tracing through a program that has already been compiled. Another way is to instruct BC to generate an assembly language source listing as it compiles your program. This listing shows a mix of BASIC source statements and the resultant assembly language code and addresses. However, the listing is not as clear or easy to follow as the display that CodeView presents. But if you do not have CodeView, this is your only choice. I will describe this method first. CREATING AN ASSEMBLY LANGUAGE SOURCE LISTING To create an assembly language list file you use the compiler's /a switch, and then specify a list file name. The syntax is shown below, followed by a sample list file that is generated.
- 94 -
You enter this: bc program /a [/other options] , , listfile; [Link] contains this: PAGE 1 25 June 91 [Link] Microsoft (R) QuickBASIC Compiler Version 4.50 Offset Data 0030 0030 0030 0033 0034 0039 003C 003D 0040 0041 0044 0045 0046 004B 004D 004F 0050 0051 0054 0055 0056 0057 0058 005D 005D 005D 005D 0062 0067 0069 006C 0072 0072 0072 0072 0072 0076 007B 0080 0085 0006 0006 ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** 0008 0008 0008 ** ** ** ** ** 0008 0008 0008 0008 ** ** ** ** 0008 Source Line CLS INPUT Count% I00002: mov push call mov push call pop add push push call jmp dw db db mov push pop push push call IF Count% < 100 Count% = 100 END IF call cmp jl jmp mov PRINT Count% END I00003: push call call call
ax,0FFFFh ax B$SCLS ax,offset <const> ax 0000h ax ax,000Dh cs ax B$INPP $+04h 0002h 00h 02h bx,offset COUNT% ds es es bx B$RDI2 THEN B$PEOS word ptr COUNT%,64h $+03h I00003 COUNT%,0064h
43981 Bytes Available 43643 Bytes Free 0 Warning Error(s) 0 Severe Error(s) Here, the list file shows the original BASIC source code, as well as the generated assembly language instructions. The column at the left holds the code addresses, and these correspond to the addresses that BASIC displays
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 95 -
when a program crashes with an error message. Unfortunately, several BASIC statements are grouped together, so it is not immediately apparent which address goes with which source statement. For example, after the BASIC statement INPUT Count%, the earlier assembly language instructions that clear the screen are shown. Similarly, the call to B$PEOS is actually part of the INPUT code, although it is listed following the IF test. When BASIC displays an error message and ends your program by displaying a segmented address, only the address portion is meaningful. The segment in which a program is running will depend on many factors, including the DOS version (and thus its size), the FILES= and BUFFERS= values specified in [Link], and whether TSR programs and device drivers are loaded. Each of these factors cause the program to be loaded at a higher segment, although the addresses within that segment never change. Also, in a multi-module program, a different segment is used for each module's source file. Therefore, if the message is "Illegal function call in module XYZ at address 3456:1234", you would compile [Link] to create a list file instead of the main program. The code in the vicinity of address 1234 will be where the error occurred. USING MICROSOFT CODEVIEW Although compiling with the /a switch lets you view the assembly language code that BASIC creates, there is little you can actually do with that information. CodeView is a much more powerful debugging tool, and it lets you step through an .EXE file as it is running. This lets you follow the compiled program's execution path, and also view its assembly language instructions. Further, CodeView can trace into BASIC's library routines, as well as calls to C or assembly language routines that you have written. CodeView can also be used to see how many bytes of code are generated for each BASIC statement. This is a good way to compare the relative efficiency of different programming methods, to see which ones produce less code. It is important to understand that the size of the assembly language code generated for a given BASIC statement is a combination of two factors: the number of bytes the compiler generates for each occurrence of the statement, and the size of the called routine within BASIC's runtime library. Of course, the called routine is added to your program only once. However, the code that sets up and calls the routine is added each time the statement is encountered. Compiling a program for use with CodeView is very simple, and merely requires the addition of special compiler and linker option switches. Note that you cannot compile a program for CodeView from within the QuickBASIC editor; you must compile and link manually from the DOS command line, as shown below. Also notice that the BASIC program must be saved as ASCII text, and not with the special "Fast Load" method that QB optionally uses. bc program /zi [/other options]; link program /co [/other options]; cv program The /zi option tells BC to write additional information into the object file, which is used by LINK and CodeView to relate each line of BASIC source code to its resultant assembly code. The more meaningfully named /co switch is required so LINK will know to do likewise. You may be interested to know that /zi is named after Microsoft legend Mark Zibikowski, whose initials (MZ) also appear as the first two bytes in every DOS .EXE file. Once the program has been compiled and linked, start CodeView by entering CV followed by the file's first name (that is, without the .BAS or .EXE extension). You will then be presented with a screen very similar to that of the QB editor. Most versions of CodeView initially show the BASIC source code. In other versions, you must press Alt-R-R to "restart" the program and bring it to the first source line. I should point out that
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 96 -
CodeView is a quirky program, and it is often referred to as the program that people "love to hate". It has some glaring omissions, many aspects of its interface are inconsistent and downright obnoxious, and I personally would be lost without it. When the BASIC source is displayed, you may press F4, F7, F8, and F10, which perform the same functions as their BASIC editor counterparts. One important difference, however, is that you may also press F3 to show a mix of BASIC and assembly language code. Stepping through the program with F8 and F10 will execute either a single BASIC statement or a single assembler command, depending on the context. That is, if you are in the BASIC view mode, then you will step through the BASIC code. If the assembly language code is being displayed, then you will step through that instead. Figure 4-1 [not available here, sorry] shows a screen snapshot of a short sample program as displayed by CodeView when it is first started in the BASIC view mode. Figure 4-2 [also unavailable] shows the same program after pressing F10 to execute up to the first statement, followed by F3 to view a mix of BASIC and assembly language. This screen is in a 50-line mode to allow the entire program to be displayed. Although it is not shown here, CodeView can continuously display the processor's registers in a small window at the right side of the screen. The register display is alternately activated and deactivated by pressing F2. FIG4-1: The CodeView display when using the BASIC view mode. FIG4-2: The CodeView display for the same program, but using the assembly language view mode. Notice in Figure 4-2 that CodeView displays each BASIC statement indented and with a line number. This lets you identify where each BASIC command starts, and also which block of assembly language code it is associated with. The numbers at the left edge of the display show the segment and address of each instruction in hexadecimal notation. The segment value never changes within a single program module, although the addresses increase based on the number of bytes in each assembly language instruction. As you can see, some assembly language commands are as short as one byte, and others are as long as six. In the first instruction, CLS, a value of -1 (FFFF hex) is passed to the CLS routine as a flag to show that no argument was given. Had the BASIC statement been CLS 2, then a value of 2 would have been moved into AX instead. Nine bytes of code are generated each time CLS is used, not counting the code within B$SCLS. Besides showing the B$SCLS routine name, CodeView also shows the segment and address at which B$SCLS resides. Knowing the routine's address is of little practical use in this situation, and it is displayed solely for informational purposes. The INPUT statement is fairly complicated to set up, and I won't belabor what every assembly language instruction does. But several items are worth discussing. The first is that CodeView attempts to relate every number it encounters to a variable or procedure address. In many cases this is confusing, because some numbers are simply that, and have no relationship to a variable or procedure address. For example, at address 39 the assembly language command MOV AX,40 is shown as MOV AX,b$STRTAB_END+10 (0040), as if there was some significance to the fact that the value 40 is an address ten bytes past the end of an internal string table. Likewise, two instructions later the value 40 is represented as being 31 bytes past the beginning of the B$LENDRW procedure. Two instructions past that the value 13 (0D hex) is added to AX, and again CodeView tries to establish a significance where none exists. In not one of these cases are the values shown related to the named address, and you should therefore treat those named labels with skepticism. The only symbolic names that are meaningful in most cases are variable and procedure names that do not have an extra value added to them. In the instruction MOV Word Ptr [COUNT% (0036)],b$HEAP_FIRST (0064) at address 6C,
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 97 -
the address for Count% (36) is valid, while the value 64 named b$HEAP_FIRST is meaningless. In this case, 64 hex represents the value 100 in the BASIC statement Count% = 100. Whatever b$HEAP_FIRST may represent, it has no meaning here. I suggest that you enter this short program and then step through it one statement at a time, just to get a feel for how CodeView operates. You should also try tracing into some of the BASIC library calls, as well as into a simple subprogram or two of your own. Again, you may use either F10 or F8 to step through the code, but only F8 will trace into code that is being called. You can also use F8 to trace into some BIOS interrupts, but you should never try to trace through a DOS interrupt (21 hex). Many DOS services never return, or return in a non-standard manner, and a locked-up PC is the likely result. You will not hurt anything if you do trace into a DOS interrupt, but be prepared to press Ctrl-Alt-Del. Besides being able to view and step through the assembly language code that BASIC creates, you can also view and modify your program's data directly. If you have pressed F2 to display the CPU's registers, CodeView will show the value currently in every memory address that is about to be accessed. For example, if the next statement to be executed is MOV Word Ptr [COUNT%],10, CodeView will show the current contents of the variable COUNT%. A range of memory addresses may be displayed by entering commands into the immediate window at the bottom of the screen. When CodeView is first started, the cursor is placed at the bottom line in that window. As with the BASIC editor, the F6 key is used to toggle between the code output and immediate windows. Unlike the BASIC editor, however, you may type commands regardless of which window is active. The three primary commands you will find useful are D, U, and R. The D (Dump) command tells CodeView to display a range of memory, starting at a given address. For example, D 0 means to show the 32 bytes that start at address 0 in the default data segment. Likewise, D ES:100 means to start at address 100 in the segment held in the ES register. Unfortunately, CodeView is particularly obtuse in this regard, because in some cases the numbers you enter are assumed to be decimal while in others it assumes hexadecimal. Which is which depends on your view perspective (selected with F3), and I won't even begin to offer a reason or explain the confusing rules. If you don't get what you expect, try adding an "&H" prefix to the number. And if you start by using &H and CodeView reports a syntax error, then try it without the &H. When the contents of memory are displayed, they are shown as individual bytes, rather than as integer words which is generally more useful. In the listing below, two string constants have been displayed in response to the command D &H40. For space reasons, the segment and address which CodeView adds to the left of each row of values are instead shown above the rows. >D &H40 5676:0040 02 00 44 00 48 69 23 00 4A 00 41 42 43 44 45 46 5676:0050 47 48 49 4A 4B 4C 4D 4E 4F 50 51 52 53 54 55 56 As you learned in Chapter 2, BASIC near strings have a 4-byte descriptor, with the first two bytes holding the string's current length, and the second two bytes its current address. Beginning with the first two numbers displayed, the 02 00 represents the length of a 2-character string, and the 44 00 indicates the address which is 44. The data itself is a CHR$(&H48) followed by a CHR$(&H61) ("Hi"), and it immediately follows the string descriptor. When two bytes are used to store an integer word, the least significant byte is kept in the lower memory address. Therefore, the value 0002 is actually listed as 02 00 (CodeView adds an extra blank between bytes for clarity).
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 98 -
Immediately following the six bytes for the string "Hi" and its descriptor is another descriptor. This one shows that the string has a length of 23 Hex bytes, and its data starts at address 4A Hex. Again, the value 0023 is shown as 23 00, and the address 004A is displayed as 4A 00. This string contains the data "ABCDEFGHIJKLMNOPQRSTUV". The U (Unassemble) command can be used to show the assembly language source code at any arbitrary segment and address. The command U 2000:1000 will unassemble the code at address 2000:1000, though again you may need to use U &H2000:&H1000 in some view modes. The U command is not used that frequently, since CodeView is used most often to step through code in sequence, rather than to examine an arbitrary block of instructions. The R command lets you change the contents of a register, and this might be useful when debugging your own assembly language subroutines. When you type, for example, RCX and press Enter, the current value of the CX register is displayed and you are prompted for a new value. Pressing Enter alone cancels the command and leaves the current register contents intact. Otherwise, the value you enter will be assigned to CX. This is similar to BASIC's immediate window, in which you can assign new values to a variable. The last CodeView features worth describing here are Watch Variables and Watch Points, which are similar to the same features in QB. Unlike QB, though, you cannot use an expression as the target of a Watch; it must be a simple variable name, array element, or address. Watch Variables may be added using the pull-down menu, or by pressing Alt-W and then typing the variable name. If you are in the BASIC view mode you may add only BASIC variables; in the assembly language view mode you can add only assembly language variables. To monitor the contents of a memory address requires the W command. For example, W 40 will set up address 40 as the target of a Watch. Although CodeView does support Watch points, whereby the program will run continuously until a given expression is true, you won't want to use that feature. Asking CodeView to stop when, say, CX becomes greater than 100 will cause your program to run at less than one thousandth its normal speed. Therefore, I have never found using Watch Points effective in any situation--it is always too slow. I have avoided discussing the latest versions of CodeView, in favor of focusing on those features which are common to all versions. CodeView 3.10 which is included with BASIC 7.1 has several new convenience features, and a few new bugs as well. Many of the commands that in earlier versions have to be entered manually are now available by simply typing new values onto the display. For instance, where older versions of CodeView required you to enter Dump commands repeatedly, the new version updates the displayed values in a range of addresses constantly. And to change the address range, you may now simply move the cursor to the segment and address numbers and type new ones. An option to display memory values as words or even single and double precision values is also present in version 3.10. Now that you have seen what CodeView is all about and how to use it, I want to conclude this chapter with a practical example. As I mentioned in Chapter 3, the amount of stack memory that is needed in a non-static subprogram or function can be difficult to determine. The calculation itself is trivial: simply add up the number of bytes needed by every variable in the routine. Each integer requires two bytes, single precision, long integer, and string variables need four bytes, and so forth. The problem, of course, is who wants to do all that counting, especially when there may be hundreds of variables. Counting is what computers are for, no? The solution is that BASIC knows how many bytes are needed for the subprogram, and the very first thing a subprogram does when it is invoked is to call another routine that allocates the necessary stack space. So rather than use trial and error methods to increase the stack in small increments, you can use CodeView to directly see how many bytes of stack space are being requested. Here's how that's done, using the example program shown below.
- 99 -
DEFINT A-Z DECLARE SUB StackTest (Dummy) Test = 10 CALL StackTest(Test) END SUB X Y Z END StackTest(AnyVar) = 100 = 10 = AnyVar SUB
Save this program as an ASCII file using the name [Link], and then compile it with the /o and /zi options. Next, link [Link] for CodeView using the /co option. Then start CodeView by entering CV TEST. Once you are in CodeView and viewing the BASIC source, press F10 to skip past BASIC's start-up code. At this point the cursor should be on the first statement, Test = 10. Finally, press F3 to show a mix of BASIC and assembly language source code. The display should look similar to that shown in Figure 4-3 [unavailable]. FIG4-3: How to determine the amount of stack memory needed for a non-static procedure. Notice the first statement within the TestStack subprogram at line 7, where the value 6 (erroneously labeled b$STRTAB+6) is assigned to the CX register. This is the number of bytes of stack space being requested from the B$ENRA routine which is called in the next instruction. B$ENRA is the routine that actually allocates the stack memory, and it uses the value BASIC sends in CX to know how many bytes are needed. TestStack has three local variables and each is a two-byte integer, hence six bytes are required to store them on the stack. For a very large program, the value assigned to CX will of course be much larger. Further, if one subprogram calls another, it will be up to you to add up all of the CX values to determine the total stack memory requirements. But this is very much easier than counting variables. SUMMARY In this chapter you have learned how to identify and correct common programming errors. You have also learned the importance of understanding BASIC's various quirks, and how some statements do not always do exactly what you thought they would. I have shown several debugging strategies, including a software adaptation of the "cut in half" hardware technique. Perhaps your most powerful debugging ally is the QuickBASIC and QBX editing environments. These powerful editors let you single step through a program, monitor variable values and function results, and halt your program when a specified condition occurs. When BASIC terminates a program prematurely with an error message and a segmented address, you can either use the BC compiler's /a option to generate a source listing, or use CodeView to see where the error occurred. CodeView can also be used to step and trace through a program at the assembly language source level, and to determine the number of bytes of stack memory a non-static procedure requires. In Chapter 5 you will learn about compiling and linking BASIC programs. I will present a complete overview of the many BC and LINK options that are available, and discuss the relative merits of each.
- 100 -
CHAPTER 5 COMPILING AND LINKING The final step in the creation of any program is compiling and linking, to produce a stand-alone .EXE file. Although you can run a program in the BASIC editing environment, it cannot be used by others unless they also have their own copy of BASIC. In preceding chapters I explained the fundamental role of the BASIC compiler, and how it translates BASIC source statements to assembly language. However, that is only an intermediate action. Before a final executable program can be created, the compiled code in the object file must be joined to routines in the BASIC language library. This process is called linking, and it is performed by the LINK program that comes with BASIC. In this chapter you will learn about the many options and features available with the BASIC compiler and LINK. By thoroughly understanding all of the capabilities these programs offer, you will be able to create applications that are as small and fast as possible. Many programmers are content to let the BASIC editor create the final program using the pulldown menu selections. And indeed, it is possible to create a program without invoking BC and LINK manually--many programmers never advance beyond BASIC's "Make .EXE" menu. But only by understanding fully the many options that are available will you achieve the highest performance possible from your programs. I'll begin with a brief summary of the compiling and linking process, and explain how the two processes interact. I will then move on to more advanced aspects of compiling and linking. BC and LINK are very complex programs which possess many features and capabilities, and all of their many options will be described throughout this chapter. You may also refer back to Chapter 1, which describes compiling in more detail. AN OVERVIEW OF COMPILING AND LINKING ==================================== When you run the [Link] compiler, it reads your BASIC source code and translates some statements directly into the equivalent assembly language commands. In particular, integer math and comparisons are converted directly, as well as integer-controlled DO, WHILE, and FOR loops. Floating point arithmetic and comparisons, and string operations and comparisons are instead translated to calls to existing routines written by the programmers at Microsoft. These routines are in the BCOM and BRUN libraries that come with BASIC. As BC compiles your program, it creates an object file (having an .OBJ extension) that contains both the translated code as well as header information that LINK needs to create a final executable program. Some examples of the information in an object file header are the name of the original source file, copyright notices, offsets within the file that specify external procedures whose addresses are not known at compile time, and code and data segment names. In truth, most of this header information is of little or no relevance to the BASIC programmer; however, it is useful to know that it exists. All Microsoft-compatible object files use the same header structure, regardless of the original source language they were written in. The LINK program is responsible for combining the object code that BC produces with the routines in the BASIC libraries. A library (any file with a .LIB extension) is merely a collection of individual object files, combined one after the other in an organized manner. A header portion of the .LIB file holds the name of each object file and the procedure names contained therein, as well as the offset within the library where each
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 101 -
object module is located. Therefore, LINK identifies which routines are being accessed by the BASIC program, and searches the library file for the procedures with those names. Once found, a copy of that portion of the library is then appended to the .EXE file being created. LINK can also join multiple object files compiled by BC to create a single executable program, and it can produce a Quick Library comprised of one or more object files. Quick Libraries are used only in the editing environment, primarily to let BASIC access non-BASIC procedures. Because the BASIC editor is really an interpreter and not a true compiler, Quick Libraries were devised as a way to let you call compiled (or assembled) subroutines during the development of a program. When LINK is invoked it reads the header information in each object file compiled by BC, and uses that to know which routines in the specified library or libraries must be added to your program. Since every external routine is listed by name, LINK simply examines the library header for the same name. It is worth mentioning that BASIC places the name of the default library in the object file, so you don't have to specify it when linking. For example, when you compile a stand-alone program (with the /o) switch) using BC version 4.5, it places the name [Link] in the header. BASIC is not responsible for determining where external routines are located. If your program uses a PRINT statement, the compiler generates the instruction CALL 0000:0000, and identifies where in the object file that instruction is located. BASIC knows that the print routine will be located in another segment, and so leaves room for both a segment and address in the Call instruction. But it doesn't know where in the final executable file the print routine will end up. The absolute address depends on how many other modules will be linked with the current object file, and the size of the main program. In fact, LINK does not even know in which segment a given routine will ultimately reside. While it can resolve all of the code and data addresses among modules, the absolute segment in which the program will be loaded depends on whether there are TSR programs in memory, the version of DOS (and thus its size), and the number of buffers specified in the host PC's [Link] file, among other factors. Therefore, all .EXE files also have a header portion to identify segment references. DOS actually modifies the program, assigning the final segment values as it loads the program into memory. Figure 5.1 shows how DOS, file buffers, and device drivers are loaded in memory, before any executable programs.
- 102 -
+---------------------+ ROM BIOS routines +--------------------- Video memory --------------------- <-- top of DOS memory (640K boundary) Far heap storage for dynamic arrays
+--------------------- String memory +--------------------- The stack +--------------------- Variable data +--------------------- Compiled BASIC code --------------------- TSR programs +--------------------- Device drivers +--------------------- File control blocks +--------------------- File buffers +--------------------- DOS program +--------------------- BIOS work area +--------------------- Interrupt vectors +---------------------+
- 103 -
It is important to understand that library routines are added to your program only once, regardless of how many times they are called. Even if you use PRINT three hundred times in a program, only one instance of the PRINT routine is included in the final .EXE file. LINK simply modifies each use of PRINT to call the same memory address. Further, LINK is generally smart enough to not add all of the routines in the library. Rather, it just includes those that are actually called. However, LINK can extract only entire object files from a library. If a single object module contains, say, four routines, all of them will be added, even if only one is called. For BASIC modules that you write, you can control which procedures are in which object files, and thus how they are combined. But you have no control over how the object modules provided with BASIC were written. If the routines that handle POS(0), CSRLIN, and SCREEN are contained in a single assembly language source file (and they are), all of them are added to your program even if you use only one of those BASIC statements. Now that you understand what compiling and linking are all about, you may wonder why it is necessary to know this, or why you would ever want to compile manually from the DOS command line. The most important reason is to control fully the many available compile and link options. For example, when you let the BASIC editor compile for you, there is no way to override BC's default size for the communications receive buffer. Likewise, the QuickBASIC editor does not let you specify the /s (string) option that in many cases will reduce the size of your programs. LINK offers many powerful options as well, such as the ability to combine code segments to achieve faster performance during procedure calls. Another important LINK option lets you create an .EXE file that can be run under CodeView. Again, these options are not selectable from within the QuickBASIC environment [but PDS and VB/DOS Pro Edition let you select more options than QuickBASIC], and they can be specified only by compiling and linking manually. All of these options are established via command line switches, and each will be discussed in turn momentarily. Finally, BASIC PDS includes a number of *stub files* which reduce the size of your programs, although at the expense of decreased functionality. For example, if your program does not use the SCREEN statement to enable graphics mode, a stub file is provided to eliminate graphics support for the PRINT statement. BASIC PDS [and the VB/DOS Pro Edition] also support program overlays, and to use those requires linking manually from DOS. COMPILING ========= To compile a program you run [Link] specifying the name of the BASIC program source file. BC accepts several optional parameters, as well as many optional command line switches. The general syntax for BC is as follows, with brackets used to indicate optional information. bc program [/options] [, object] [, listfile] [;] In most cases you will simply give the name of the BASIC source file, any option switches, and a terminating semicolon. A typical BC command is as follows: bc program /o; Here, a BASIC source file named [Link] is being compiled, and the output object file will be called [Link]. The /o option indicates that the program will be a stand-alone .EXE file that does not require the BRUN library to be present at runtime. If the semicolon is omitted, the compiler will prompt for each of the file name parameters it needs. For example, entering bc program /o invokes the compiler, which then prompts you for the output and listing file names. Pressing Enter in response to
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 104 -
any prompt tells BC to use the source file's first name. You may also start BC with no source file name, and let it prompt for that as well. In most cases the default file names are acceptable; however, it is not uncommon to want the output file placed into a different directory. This is done as follows: bc program, \objdir\ /o; [Note that if the trailing backslash were omitted from \objdir\ above, BC would create an output file named [Link] in the root directory. Of course, that is not what is intended. Therefore, a trailing backslash is added to tell BC to use the default name of [Link], and to place that file in the directory named \OBJDIR.] If you are letting BC prompt you for the file names, you would enter the output path name at that prompt position. You may also include a drive letter as part of the path, or a drive letter only to use the default directory on the specified drive. The listing that follows shows a typical BC session that uses prompting. C>bc program /o Microsoft (R) QuickBASIC Compiler Version 4.50 (C) Copyright Microsoft Corporation 1982-1988. All rights reserved. Simultaneously published in the U.S. and Canada. Object Filename [[Link]]: d:\objects\ <Enter> Source Listing [[Link]]: <Enter> 43965 Bytes Free 43751 Bytes Available 0 Warning Error(s) 0 Severe Error(s) C> Although you can override the default file extensions, this is not common and you shouldn't do that unless you have a good reason to. For example, the command BC [Link] , [Link]; will compile a BASIC source file named [Link] and create an object module named [Link]. Since there are already standard default file extension conventions, I recommend against using any others you devise. The optional list file contains a source listing of the BASIC program showing the addresses of each program statement, and uses a .LST extension by default. There are a number of undocumented options you can specify to control how the list file is formatted, and these are described later in this chapter in the section *Compiler Metacommands*. A list file may also include the compiler-generated assembly language instructions, and you specify that with the /a option switch. All of the various command options will be discussed in the section following. Notice that the positioning of the file name delimiting commas must be maintained when the object file name is omitted. If you plan to accept the default file name but also want to specify a listing file, you must use two commas like this: bc source , , listfile; The Bytes Available and Bytes Free messages indicate how much working memory the compiler has at its disposal, and how much of it remained free while compiling your program. BC must keep track of many different kind of information as it processes your source code, and it uses its own internal DGROUP memory for that. For example, every variable that you use must be remembered, as well as its address.
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 105 -
When BASIC sees a statement such as X = 100, it must look in its *symbol table* to see if it has already encountered that variable. If so, it creates an assembly language instruction to store the value 100 at the corresponding address. Otherwise, it adds the variable X to the table, assigns a new address for it, and then adds code to assign the value 100 to that address. When you use PRINT X later on, BASIC will again search its table, find the address, and use that when it creates the code that calls the PRINT routine. Other data that BASIC must remember as it works includes the number and type of arguments for each SUB or FUNCTION that is declared, line label names and their corresponding addresses, and quoted string constants. As you may recall, in Chapter 2 I explained that BC maintains a table of string constants, and stores each in the final program only once. Even when the same quoted string is used in different places in a program, BC remembers that they are the same and stores only a single copy. Therefore, an array is used by BC to store these strings while your program is being compiled. In most cases you can simply ignore the Bytes Available and Bytes Free messages, since how much memory BASIC used or had available is of no consequence. The only exception, of course, is when your program is so large that BC needed more than was available. But again, you will receive an error message when that occurs. However, if you notice that the Bytes Free value is approaching zero, you should consider splitting your program into separate modules. The error message display indicates any errors that occurred during compilation, and if so how many. This display is mostly a throw-back to the earlier versions of the BASIC compiler, because they had no development environment. These days, most people get their program working correctly in the BASIC editor, before attempting to compile it. Of course, there must still be a facility for reporting errors. In most cases, any errors that BC reports will be severe errors. These include a mismatched number of parentheses, using a reserved word as a variable name (for example, PRINT = 12), and so forth. One example of a warning error is referencing an array that has not been dimensioned. When this happens, BASIC creates the array with a default 11 elements (0 through 10), and then reports that it did this as a warning. One interesting quirk worth mentioning is that BASIC will not let you compile a program named [Link]. If you enter BC USER, BC assumes that you intend to enter the entire program manually, statement by statement! This too must be a holdover from earlier versions of the compiler; however, when [Link] is specified it will appear that the compiler has crashed, because nothing happens and no prompt is displayed. In my testing with BASIC 7.1, any statements I entered were also ignored, and no object file was created. COMPILER OPTIONS All of the options available for use with the BASIC compiler are described in this section in alphabetical order. Some options pertain only to BASIC 7 PDS, and these are noted in the accompanying discussion. Each option is specified by listing it on the BC command line, along with a preceding forward slash (/). Also, these options apply to the BC compiler only, and not necessarily to the QB and QBX editing environments. /A The /a (assembly) switch tells BC to include the assembly language source code it creates in the listing file. The format of the file was described in detail in Chapter 4, so I won't belabor that here. Note, however, that a file name must be given in the list file position of the BC command line. Otherwise, a list file will not be written.
- 106 -
/Ah Using /ah (array huge) tells BASIC that you plan to create dynamic arrays that may exceed 64K in total data size. This option affects numeric, TYPE, and fixed-length string arrays only, and not conventional string arrays. Normally, BASIC calculates the element addresses for array references directly, based on the segment and other information in the array descriptor. This is the most direct method, and thus provides the fastest performance and smallest code. When /ah is used, all access to non-string dynamic arrays is instead made through a called routine. This called routine calculates the segment and address of a single array element, and because it must also manipulate segment values, increases the size of your programs. Therefore, /ah should be avoided unless you truly need the ability to create huge arrays. Even if a particular array does not currently exceed the 64K segment limit, BASIC has no way to know that when it compiles your program. To minimize the size and speed penalty /ah imposes, it may be used selectively on only some of the source modules in a program. If you have one subprogram that needs to manipulate huge arrays but the rest of program does not, you should create a separate file containing only that subprogram and compile it using /ah. When the program is linked, only that module's array accesses will be slower. Note that the /ah switch is also needed if you plan to create huge arrays when running programs in the BASIC editor. However, with the BASIC editor, using /ah does not impinge on available memory or make the program run slower. Rather, it merely tells BASIC not to display an error message when an array is dimensioned to a size greater than 64K. [The BASIC editor always uses the slower code that checks for illegal array elements anyway, so it can report an error rather than lock up your computer.] One limitation that /ah will not overcome is BASIC's limit of 32,767 elements in a single dimension. That is, the statement REDIM Array%(1 to 32768) will fail, regardless of whether /ah is used. There are two ways to exceed this limit: one is to create a TYPE array in which each element is comprised of two or more variables. The other is to create an array that has more than one dimension. The brief program below shows how to access a 2-dimensional array as if it had only a single dimension. DEFINT A-Z '----- pick an arbitrary group size, and number of groups (in this ' case 100,000 elements) GroupSize = 1000: NumGroups = 100 '----- dimension the array REDIM Array(1 TO GroupSize, 1 TO NumGroups) '----- pick an element number to assign (note use of a long integer) Element& = 50000 '----- calculate the first and second subscripts First = ((Element& - 1) MOD GroupSize) + 1 Second = (Element& - 1) \ GroupSize + 1 '----- assign the appropriate array element Array(First, Second) = 123 '----- show how to derive the original element based on First and ' Second (CLNG is needed to prevent an Overflow error) CalcEl& = First + (Second - 1) * CLNG(GroupSize)
/C
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 107 -
The /c (communications) option lets you specify the size of the receive buffer when writing programs that open the COM port. The value specified represents the total buffer size in bytes, and is shared when two ports are open at once. For example, if two ports are open and the total buffer size is 4096 bytes, then each port has 2048 bytes available for itself. A receive buffer is needed when performing communications, and it accumulates the incoming characters as they are received. Each time a character is accepted by the serial port, it is placed into the receive buffer automatically. When your program subsequently uses INPUT or INPUT$ or GET to read the data, it is actually reading the characters from the buffer and not from the hardware port. Without this buffering, your program would have to wait in a loop constantly looking for each character, which would preclude it from doing anything else! Communications data is received in a continuous stream, and each byte must be processed before the next one arrives, otherwise the data will be lost. The communications port hardware generates an interrupt as each character is received, and the communications routines within BASIC act on that interrupt. The byte is retrieved from the hardware port using an assembly language IN instruction, which is equivalent to BASIC's INP function. This allows the characters to accumulate in the background, without any additional effort on your part. As each byte is received it is placed into the buffer, and a pointer is updated showing the current ending address within the buffer. As your program reads those bytes, another pointer is updated to show the new starting address within the buffer. This type of buffer is called a *circular buffer*, because the starting and ending buffer addresses are constantly changing. That is, the buffer's end point "wraps" around to the beginning when it becomes full. The receive buffer whose size is specified with /c is located in far memory. However, BASIC also maintains a second buffer in near memory, and its size is dictated by the optional LEN= argument used with the OPEN statement. Because near memory can be accessed more quickly than far memory, it is sensible for BASIC to copy a group of characters from the far receive buffer to the near buffer all at once, rather than individually each time you use GET or INPUT$. When /c is not specified, the buffer size defaults to 512 bytes. This means that up to 512 characters can be received with no intervention on your part. If more than 512 bytes arrive and your program still hasn't removed them using INPUT$ or GET, new characters that come later will be lost. It is also possible to stipulate hardware handshaking when you open the communications port. This means that the sender and receiver use physical control wires to indicate when the buffer is full, and when it is okay to resume transmitting. In many programming situations, the 512 byte default will be more than adequate. However, if many characters are being received at a high baud rate (9600 or greater) and your program is unable to accept and process those characters quickly enough, you should consider using a larger buffer. Fortunately, the buffer is located in far memory, so increasing its size will not impinge on available string and data stored in DGROUP. /D The /d (debug) option switch is intended solely to help you find problems in a program while it is being developed. Because /d causes BC to generate additional code and thus bloat your executable program, it should be used only during development. When /d is specified, four different types of tests are added to your program. The first is a call to a routine that checks if Ctrl-Break has been pressed. One call is added for every BASIC source statement, and each adds five bytes of code to your final executable program. The second addition is a one-byte assembly language INTO instruction following each integer and long integer math operation, to detect overflow errors. The third is a call to a routine that calculates array element addresses, to ensure that the element number is in fact legal. Normally,
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 108 -
element addresses are computed directly without checking the upper and lower bounds, unless you are using huge (greater than 64K) arrays. Without /d, it is therefore possible to corrupt memory by assigning an element that doesn't exist. The final code addition implements GOSUB and RETURN statements using a library routine, rather than calling and returning from the target line directly. Normally, a GOSUB statement is translated into a three-byte assembly language *near call* instruction, and a RETURN is implemented using a one-byte *near return*. But when /d is used, the library routines ensure that each RETURN did in fact result from a corresponding GOSUB, to detect RETURN without GOSUB errors. This is accomplished by incrementing an internal variable each time GOSUB is used, and decrementing it at each RETURN. If that variable is decremented below 0 during a RETURN statement, then BASIC knows that there was no corresponding GOSUB. These library routines are added to your program only once by LINK, and comprise only a few bytes of code. However, a separate five-byte call is generated for each GOSUB and RETURN statement. Many aspects of the /d option were described in detail in Chapters 1 and 4, and there is no need to repeat that information here. But it is important to remember that /d always makes your programs larger and run more slowly. Therefore, it should be avoided once a program is running correctly. /E The /e (error) option is necessary for any program that uses ON ERROR or RESUME with a line label or number. In most cases using /e adds little or no extra code to your final .EXE program, unless ON ERROR and RESUME are actually used, or unless you are using line numbers. For each line number, four bytes are added to remember the number itself as well as its position in the file [two bytes each]. As with /d, every GOSUB and RETURN statement is implemented through a far call to a library routine, rather than by calling the target line directly. Without this added protection it would not be possible to trap "RETURN without GOSUB" errors correctly, or recover from them in an ON ERROR handler. Also see the /x option which is needed when RESUME is used alone, or with a 0 or NEXT argument. The /x switch is closely related to /e, and is described separately below. /Fpa and /Fpi (BASIC PDS and later) When Microsoft introduced their BASIC compiler version 6.0, they included an alternate method for performing floating point math. This Floating Point Alternate library (hence the /fpa) offered a meaningful speed improvement over the IEEE standard, though at a cost of slightly reduced accuracy. This optional math library has been continued with BASIC 7 PDS, and is specified using the /fpa command switch. By default, two parallel sets of floating point math routines are added to every program. When the program runs, code in BASIC's runtime startup module detects the presence of a math coprocessor chip, and selects which set of math routines will be used. The coprocessor version is called the Inline Library, and it merely serves as an interface to the 80x87 math coprocessor that does the real work in its hardware. (Note that inline is really a misnomer, because that term implies that the compiler generates coprocessor instructions directly. It doesn't.) The second version is called the Emulator Library, because it imitates the behavior of the coprocessor using assembly language subroutines. Although the ability to take advantage of a coprocessor automatically is certainly beneficial, there are two problems with this dual approach: code size and execution speed. The coprocessor version is much smaller than the routines that perform the calculations manually, since it serves only as an interface to the coprocessor chip itself. When a coprocessor is in fact present, the entire emulator library is still loaded into memory.
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 109 -
And when a coprocessor is not installed in the host PC, the library code to support it is still loaded. The real issue, however, is that each BASIC math operation requires additional time to route execution to the appropriate routines. Since BC has no way to know if a coprocessor will be present when the program eventually runs, it cannot know which routine names to call. Therefore, BASIC uses a system of software interrupts that route execution to one library or the other. That is, instead of using, say, CALL MultSingle, it instead creates code such as INT 39h. The Interrupt 39h vector is set when the program starts to point to the correct library routine. Unfortunately, the extra level of indirection to first read the interrupt address and then call that address impacts the program's speed. Recall that Chapter 1 explained how the library routines in a BRUNstyle program modify the caller's code the first time they are invoked. The compiler creates code that uses an interrupt to access the library routines, and those routines actually rewrite that code to produce a direct call. Although this code modification increases the time needed to call a library routine initially, subsequent calls will be noticeably faster. BASIC statements executed many times within a FOR or DO loop will show the greatest improvement, but statements executed only once will be much slower than usual. In a similar fashion, the coprocessor routines that are in BASIC's runtime library alter the caller's code, replacing the interrupt commands with equivalent coprocessor instructions. Each floating point interrupt that BC generates includes the necessary variable addresses and other arguments within the caller's code. These arguments are in the same format as a coprocessor instruction. The first time an interrupt is invoked, it subtracts the "magic value" &H5C32 from the bytes that comprise the interrupt instruction, thus converting the instruction into a coprocessor command. This will be covered in Chapter 12 and I won't belabor it here. Since the alternate floating point math routines do not use a coprocessor even if one is present, the interrupt method is not necessary. BC simply hardcodes the library subroutine names into the generated code, and the program is linked with the alternate math library. Besides the speed improvement achieved by avoiding the indirection of interrupts, the alternate math library is also inherently faster than the emulator library when a coprocessor is not present. The /fpi switch tells BASIC to use its normal method of including both the coprocessor and emulator math libraries in the program, and determining which to use at runtime. (See the discussion of /fpa above.) Using /fpi is actually redundant and unnecessary, because this is the default that is used if no math option is specified. /Fs (BASIC PDS only)
BASIC PDS offers an option to use far strings, and this is specified with the /fs (far strings) switch. Without /fs, all conventional (not fixedlength) string variables and string arrays are stored in the same 64K DGROUP memory that holds numeric variables, DATA items, file buffers, and static numeric and TYPE arrays. Using the /fs option tells BASIC to instead store strings and file buffers in a separate segment in far memory. Although a program using far strings can subsequently hold more data, the capability comes at the expense of speed and code size. Obviously, more code is required to access strings that are stored in a separate data segment. Furthermore, the string descriptors are more complex than when near strings are used, and the code that acts on those descriptors requires more steps. Therefore, you should use /fs only when truly necessary, for example when BASIC reports an Out of string space error. Far versus near strings were discussed in depth in Chapter 2, and you should refer to that chapter for additional information. [One very unfortunate limitation of VB/DOS is that only far strings are supported. The decision makers at Microsoft apparently decided it was too much work to also write a near-strings version of the forms library. So users of VB/DOS are stuck with the additional size and speed overhead of
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 110 -
far strings, even for small programs that would have been better served with near strings.] /G2 (BASIC PDS and later)
The /g2 option tells BASIC to create code that takes advantage of an 80286 or later CPU. Each new generation of Intel microprocessors has offered additional instructions, as well as performance optimizations to the internal microcode that interprets and executes the original instructions. When an existing instruction is recoded and improved within the CPU, anyone who owns a PC using the newer CPU will benefit from the performance increase. For example, the original 8086/8088 had several instructions that performed poorly. These include Push and Pop, and Mul and Div. When Intel released the 80186, they rewrote the microcode that performs those instructions, increasing their speed noticeably. The 80286 is an offshoot of the 80186, and of course includes the same optimizations. The 80386 and 80486 offer even more improvements and additions to the original 8086 instruction set. Besides the enhancements to existing instructions, newer CPU types also include additional instructions not present in the original 8086. For example, the 80286 offers the Enter and Leave commands, each of which can replace a lengthy sequence of instructions on the earlier microprocessors. Another useful enhancement offered in the 80286 is the ability to push numbers directly onto the stack. Where the 8086 can use only registers as arguments to Push, the instructions Push 1234 and Push Offset Variable are legal with 80186 and later CPUs. Likewise, the 80386 offers several new commands to directly perform long integer operations. For example, adding two long integer values using the 8086 instruction set requires a number of separate steps. The 80386 and later CPUs can do this using only one instruction. If you are absolutely certain that your program will be run only on PCs with an 80286 or later microprocessor, the /g2 option can provide a modest improvement in code size and performance. In particular, programs that use /g2 can save one byte each time a variable address is passed to a routine. When /g2 is not used, the command PRINT Work$ results in the code shown below. PRINT Work$ Mov AX,Offset Work$ Push AX Call B$PESD
'this requires 3 bytes 'this requires 1 byte 'a far call is 5 bytes
When /g2 is used, the address is pushed directly rather than first being loaded into AX, as shown following. PRINT Work$ Push Offset Work$ Call B$PESD
With the rapid proliferation of 80386 and 80486 [and Pentium] computers, Microsoft should certainly consider adding a /g3 switch. Taking advantage of 80386 instructions could provide substantially more improvement over 80286 instructions than the 80286 provides beyond the 8086. [In fact, Microsoft has added a /g3 switch to VB/DOS. Unfortunately, it does little more than the /g2 switch. Most of a program's execution is spent running code inside the Microsoft-supplied runtime libraries. But those libraries contain only 8088 code! Using /g2 and /g3 affect only the compiler-generated code, which has little impact on a program's overall performance. Until Microsoft writes additional versions of their runtime
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 111 -
libraries using 80386 instructions (yeah, right), using /g2 or /g3 will offer very little practical improvement.] /Ix (BASIC PDS and later)
Another important addition to BASIC 7 PDS is its integral ISAM data file handler. Microsoft's ISAM (Indexed Sequential Access Method) offers three key features: The first is indexing, which lets you search a data file very quickly. A simple sequential search reads each record from the disk in order until the desired information is found. That is, to find the record for customer David Eagle you would start at the beginning of the file, and read each record until you found the one containing that name. An index system, on the other hand, keeps as many names in memory as will fit, and searches memory instead of the disk. This is many time faster than reading the disk repeatedly. If Mr. Eagle is found in, say, the 1200th position, the index manager can go directly to the corresponding record on disk and return the data it contains. The second ISAM feature is its ability to maintain the data file in sorted order. In most situations, records are stored in a data file in the order they were originally entered. For example, with a sales database, each time a customer purchases a product a new record is added holding the item and price for the item. When you subsequently step through the data file, the entries will most likely be ordered by the date and time they were entered. ISAM lets you access records in sorted order--for example, alphabetically by the customer's last name--regardless of the order in which the data was actually entered. The last important ISAM feature is its ability to establish relationships between files, based on the information they contain. Many business applications require at least two data files: one to hold names and addresses of each customer which rarely changes, and another to hold the products or other items that are ordered periodically. It would be impractical and wasteful to duplicate the name and address information repeatedly in each product detail record. Instead, many database programs store a unique customer number in each record. Then, it is possible to determine which sales record goes with which customer based on the matching numbers in both files. A program that uses this technique is called a *relational database*. To help the BASIC ISAM routines operate efficiently, you are required to provide some information when compiling your program. Each of the /i switches requires a letter indicating which option is being specified, and a numeric value. For each field in the file that requires fast (indexed) access, ISAM must reserve a block of memory for file buffers. This is the purpose of the /ii: switch. Notice that /ii: is needed only if more than 30 indexes will be active at one time. The /ie: option tells ISAM how much EMS memory to reserve for buffers, and is specified in kilobytes. This allows other applications to use the remaining EMS for their own use. The /ib: option switch tells ISAM how many 2K (2048-byte) *page buffers* to create in memory. In general, the more memory that is reserved for buffers, the faster the ISAM program can work. Of course, each buffer that you specify reduces the amount of memory that is available for other uses in your program. An entire chapter in the BASIC PDS manual is devoted to explaining the ISAM file system, and there is little point in duplicating that information here. Please refer to your BASIC documentation for more examples and tutorial information on using ISAM. In particular, advice and formulas are given that show how to calculate the numeric values these options require. In Chapter 6 I will cover file handling and indexing techniques in detail, with accompanying code examples showing how you can create your own indexing methods. /Lp And /Lr (BASIC PDS only)
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 112 -
BASIC 7 PDS includes an option to write programs that operate under OS/2, as well as MS-DOS. Although OS/2 has yet to be accepted by most PC users, many programmers agree that it offers a number of interesting and powerful capabilities. By default, BC compiles a program for the operating system that is currently running. If you are using DOS when the program is compiled and linked, the resultant program will also be for use with DOS. Similarly, if you are currently running OS/2, then the program will be compiled and linked for use with that operating system. The /lp (protected) switch lets you override the assumption that BC makes, and tell it to create OS/2 instructions that will run in protected mode. The /lr (real) option tells BC that even though you are currently running under OS/2, the program will really be run with DOS. Again, these switches are needed only when you need to compile for the operating system that is not currently in use. /Mbf With the introduction of QuickBASIC 4.0, Microsoft standardized on the IEEE format for floating point data storage. Earlier versions of QuickBASIC and GW-BASIC used a faster, but non-standard proprietary numeric format that is incompatible with other compilers and languages. In many cases, the internal numeric format a compiler uses is of little consequence to the programmer. After all, the whole point of a high-level language is to shield the programmer from machine-specific details. One important exception is when numeric data is stored in a disk file. While it is certainly possible to store numbers as a string of ASCII characters, this is not efficient. As I described in Chapter 2, converting between binary and decimal formats is time consuming, and also wastes disk space. Therefore, BASIC (and most other languages) write numeric data to a file using its native fixed-length format. That is, integers are stored in two bytes, and double-precision data in eight. Although QuickBASIC 4 and later compilers use the IEEE format for numeric data storage, earlier version of the compiler do not. This means that values written to disk by programs compiled using earlier version of QuickBASIC or even GW-BASIC cannot be read correctly by programs built using the newer compilers. The /mbf option tells BASIC that it is to convert to the original Microsoft Binary Format (hence the MBF) prior to writing those values to disk. Likewise, floating point numbers read from disk will be converted from MBF to IEEE before being stored in memory. [Even when /mbf is used, all floating point numbers are still stored in memory and manipulated using the IEEE method. It is only when numbers are read from or written to disk that a conversion between MBF and IEEE format is performed.] Notice that current versions of Microsoft BASIC also include functions to convert between the MBF and IEEE formats manually. For example, the statement Value# = CVDMBF(Fielded$) converts the MBF-format number held in Fielded$, and assigns an IEEE-format result to Value#. When /mbf is used, however, you do not have to perform this conversion explicitly, and using Value# = CVD(Fielded$) provides the identical result. Also see the data format discussion in Chapter 2, that compares the IEEE and MBF storage methods in detail. /O BASIC can create two fundamentally different types of .EXE programs: One type is a stand-alone program that is completely self-contained. The other type requires the presence of a special runtime .EXE library file when it runs, which contains the routines that handle all of BASIC's commands. By default, BASIC creates a program that requires the runtime .EXE library, which produces smaller program files. However, the runtime library is also needed, and is loaded along with the program into memory. The differences between the BRUN and BCOM programs were described in detail in Chapter 1. The /o switch tells BASIC to create a stand-alone program that does
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 113 -
not require the BRUN library to be present. Notice that when /o is used, the CHAIN command is treated as if you had used RUN, and COMMON variables may not be passed to a subsequently executed program. /Ot (BASIC PDS and later)
Each time you invoke a BASIC subprogram, function, or DEF FN function, code BC adds to the subprogram or function creates a stack frame that remembers the caller's segment and address. Normally, Call and Return statements in assembly language are handled directly by the microprocessor. DEF FN functions and GOSUB statements are translated by the compiler into near calls, which means that the target address is located in the same segment. Invoking a formal function or subprogram is instead treated as a far call, to support multiple segments and thus larger programs. Therefore, a RETURN or EXIT DEF statement assumes that a single address word is on the stack, where EXIT SUB or EXIT FUNCTION expect both a segment and address to be present (two words). A problem can arise if you invoke a GOSUB routine within a SUB or FUNCTION procedure, and then attempt to exit the procedure from inside that subroutine with EXIT SUB or EXIT FUNCTION. If a GOSUB is active, EXIT SUB will incorrectly return to the segment and address that are currently on the stack. Unfortunately, the address is that of the statement following the GOSUB, and the "segment" is in fact the address portion of the original caller's return location. This is shown in Figure 5-2.
- 114 -
+- +-> +->
+------------------------- Caller's return segment +------------------------- Caller's return address <-+ +------------------------- GOSUB's return address <- +------------------------- (next available location) +------------------------- These addresses will incorrectly -+ be used as a segment and address. Figure 5.2: The stack frame within a procedure while a GOSUB is pending.
- 115 -
To avoid this potential problem, the original caller's segment and address are saved when a subprogram or function is first invoked. The current stack pointer is also saved, so it can be restored to the correct value, no matter how deeply nested GOSUB calls may become. Then when the procedure is exited, another library routine is called that forces the originally saved segment and address to be on the stack in the correct position. Because this process reduces the speed of procedure calls and adds to the resultant code size, the /ot option was introduced with BASIC 7 PDS. Using /ot tells BASIC not to employ the larger and slower method, unless you are in fact using a GOSUB statement within a procedure. Since this optimization is disabled automatically anyway in that case, it is curious that Microsoft requires a switch at all. That is, BC should simply optimize procedure calls where it can, and use the older method only when it has to. /R The /r switch tells BASIC to store multi-dimensioned arrays in row, rather than column order. All arrays, regardless of their type, are stored in a contiguous block of memory. Even though string data can be scattered in different places, the table of descriptors that comprise a string array is contiguous. When you dimension an array using two or more subscripts, each group of rows and columns is placed immediately after the preceding one. By default, BASIC stores multi-dimensioned arrays in column order, as shown in Figure 5-3.
- 116 -
+-------------+ Array(5, 2) +------------- Array(4, 2) +------------- Array(3, 2) +------------- Array(2, 2) +------------- Array(1, 2) +------------- Array(5, 1) +------------- Array(4, 1) +------------- Array(3, 1) +------------- Array(2, 1) +------------- Array(1, 1) +-------------+
Figure 5.3: How BASIC stores a 2-dimensional array dimensioned created using DIM Array(1 TO 5, 1 TO 2).
As you can see, each of the elements in the first subscript are stored in successive memory locations, followed each of the elements in the second subscript. In some situations it may be necessary to maintain arrays in row order, for example when interfacing with another language that expects array data to be organized that way [notably FORTRAN]. When an array is stored in row order, the elements are arranged such that Array(1, 1) is followed by Array(1, 2), which is then followed by Array(2, 1), Array(2, 2), Array(3, 1), and so forth. Although many of the BC option switches described here are also available for use with the QB editing environment, /r is not one of them. /S The /s switch has been included with BASIC since the first BASCOM 1.0 compiler, and it remains perhaps the least understood of all the BC options. Using /s affects your programs in two ways. The first is partially described in the BASIC manuals, which is to tell BC not to combine like string constants as it compiles your program. As you learned in Chapter 2, BASIC makes available as much string memory as possible in your programs, by consolidating identical constant string data. For example, if you have the statement PRINT "Insert disk in drive A" seven times in your program, the message is stored only once, and used for each instance of PRINT. In order to combine like data the BC compiler examines each string as it is encountered, and then searches its own memory to see if that string is already present. Having to store all of the strings your program uses just to check for duplicates impinges on BC's own working memory. At some point it will run out of memory, since it also has to remember variable and procedure names, line labels and their corresponding addresses, and so on. When this happens, BC has no recourse but to give up and display an "Out of memory" error message. The /s switch is intended to overcome this problem, because it tells the compiler not to store your program's string constants. Instead of retaining the strings in memory for comparison, each is simply added to the
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 117 -
object file as it is encountered. However, strings four characters long or shorter are always combined, since short strings are very common and doing that does not require much of BC's memory. The second [undocumented] thing /s does is to add two short (eight bytes each) assembly language subroutines to the very beginning of your program. Two of the most common string operations are assignments and concatenations, which are handled by routines in the runtime library. Normally, a call to either of these routines generates thirteen bytes of code, including the statements that pass the appropriate string addresses. The subroutines that /s adds are accessed using a near rather than a far call, and they receive the string addresses in CPU registers rather than through the stack. Therefore, they can be called using between three and nine bytes, depending on whether the necessary addresses are already in the correct registers at the time. The inevitable trade-off, however, is that calling one subroutine that in turn calls another reduces the speed of your programs slightly. In many cases--especially when there are few or no duplicated string constants--using /s will reduce the size of your programs. This is contrary to the Microsoft documentation which implies that /s will make your programs larger because the duplicate strings are not combined. I would like to see Microsoft include this second feature of /s as a separate option, perhaps using /ss (string subroutine) as a designator. /T The /t (terse) switch tells BC not to display its copyright notice or any warning (non-fatal) error messages. This option was not documented until BASIC PDS, even though it has been available since at least QuickBASIC 4.0. The only practical use I can see for /t is to reduce screen clutter, which is probably why QB and QBX use it when they shell to DOS to create an .EXE program. /V and /W Any programs that use event handling such as ON KEY, ON COM, ON PLAY, or the like [but not ON GOTO or ON GOSUB] require that you compile using either the /v or /w option switches. These options do similar things, adding extra code to call a central handler that determines if action is needed to process an event. However, the /v switch checks for events at every program statement while /w checks only at numbered or labeled lines. In Chapter 1 I described how event handling works in BASIC, using polling rather than true interrupt handling. There you saw how a five-byte call is required each time BASIC needs to see if an event has occurred. Because of this added overhead, many programmers prefer to avoid BASIC's event trapping statements in favor of manually polling when needed. However, it is important to point out that by using line numbers and labels sparingly in conjunction with /w, you can reduce the amount of extra code BASIC creates thus controlling where such checking is performed. /X Like the /e switch, /x is used with ON ERROR and RESUME; however, /x increases substantially the size of your final .EXE program file. When RESUME, RESUME 0, or RESUME NEXT are used, BASIC needs a way to find where execution is to resume in your program. Unfortunately, this is not a simple task. Since a single BASIC source statement can create a long series of assembly language commands, there is no direct correlation between the two. When an error occurs and you use RESUME with no argument telling BASIC to execute the same statement again, it can't know directly how many bytes earlier that statement begins. Therefore, when /x is specified, a numbered line marker is added in the object code to identify the start of every BASIC source statement.
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 118 -
These markers comprise a linked list of statement addresses, and the RESUME statement walks through this list looking for the address that most closely precedes the offending BASIC statement. Because of the overhead to store these addresses--four bytes for each BASIC source statement--many professional programmers avoid using /x unless absolutely necessary. However, the table of addresses is stored within the code segment, and does not take away from DGROUP memory. /Z (BASIC PDS and later)
The /z switch is meant to be used in conjunction with the Microsoft editor. This editor is included with BASIC PDS, and allows editing programs that are too large to be contained within the QB and QBX editing environments. When a program is compiled with /z, BASIC includes line number information in the object file. The Microsoft editor can then read these numbers after an unsuccessful compile, to help you identify which lines were in error. Because the addition of these line number identifiers increases a program's size, /z should be used only for debugging and not in a final production. In general, the Microsoft editor has not been widely accepted by BASIC programmers, primarily because it is large, slow, and complicated to use. Microsoft also includes a newer editing environment called the Programmer's Workbench with BASIC PDS; however, that too is generally shunned by serious developers for the same reasons. /Zd Like /z, the /zd switch tells BC to include line number information in the object file it creates. Unlike /zi which works with CodeView (see the /zi switch below), /zd is intended for use with the earlier SYMDEB debugger included with MASM 4.0. It is extremely unlikely that you will ever need to use /zd in your programming. /Zi The /zi option is used when you will execute your program in the Microsoft CodeView debugger. CodeView was described in Chapter 4, and there is no reason to repeat that information here. Like /z and /zd, /zi tells BC to include additional information about your program in the object file. Besides indicating which assembler statements correspond to which BASIC source lines, /zi also adds variable and procedure names and addresses to the file. This allows CodeView to display meaningful names as you step through the assembly language compiled code, instead of addresses only. In order to create a CodeView-compatible program, you must also link with the /co LINK option. All of the options that LINK supports are listed elsewhere in this chapter, along with a complete explanation of what each does. Note that CodeView cannot process a BASIC source file that has been saved in the Fast Load format. This type of file is created by default in QuickBASIC, when you save a newly created program. Therefore, you must be sure to select the ASCII option button manually from the Save File dialog box. In fact, there are so many bugs in the Fast Load method that you should never use it. Problems range from QuickBASIC hanging during the loading process to completely destroying your source file! If a program that has been saved as ASCII is accidentally damaged, it is at least possible to reconstruct it or salvage most of it using a DOS tool such as the Norton Utilities. But a Fast Load file is compressed and encrypted; if even a single byte is corrupted, QB will refuse to load it. Since a Fast Load file doesn't really load that much faster than a plain ASCII file anyway, there is no compelling reason to use it. [Rather than fix the Fast Load bug, which Microsoft claims they cannot reproduce, beginning with PDS version 7 BASIC now defaults to storing programs as plain ASCII files.]
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 119 -
COMPILER METACOMMANDS There are a number of compiler metacommands that you can use to control how your program is formatted in the listing file that BC optionally creates. Although these list file formatting options have been available since the original IBM BASCOM 1.0 compiler [which Microsoft wrote], they are not documented in the current versions. As with '$INCLUDE and '$DYNAMIC and the other documented metacommands, each list formatting option is preceded by a REM or apostrophe, and a dollar sign. The requirement to imbed metacommands within remarks was originally to let programs run under the GW-BASIC interpreter without error. Each of the available options is listed below, along with an explanation and range of acceptable values. Many options require a numeric parameter as well; in those cases the number is preceded by a colon. For example, a line width of 132 columns is specified using '$LINESIZE: 132. Other options such as '$PAGE do not require or accept parameters. Notice that variables may not be used for metacommand parameters, and you must use numbers. CONST values are also not allowed. Understand that the list file that BASIC creates is of dubious value, except when debugging a program to determine the address at which a runtime error occurred. While a list file could be considered as part of the documentation for a finished program, it conveys no useful information. These formatting options are given here in the interest of completeness, and because they are not documented anywhere else. [In order to use any of these list options you must specify a list file name when compiling.] '$LINESIZE The '$LINESIZE option lets you control the width of the list file, to prevent or force line wrapping at a given column. The default list width is 80 columns, and any text that would have extended beyond that is instead continued on the next line. Many printers offer a 132-column mode, which you can take advantage of by using '$LINESIZE: 132. [Of course, it's up to you to send the correct codes to your printer before printing such a wide listing.] Note that the minimum legal width is 40, and the maximum is 255. '$LIST The '$LIST metacommand accepts either a minus (-) or plus (+) argument, to indicate that the listing should be turned off and on respectively. That is, using '$LIST - suspends the listing at that point in the program, and '$LIST + turns it back on. This option is useful to reduce the size of the list file and to save paper when a listing is not needed for the entire program. '$PAGE To afford control over the list file format, the '$PAGE metacommand forces subsequent printing to begin on the next page. Typically '$PAGE would be used prior to the start of a new section of code; for example, just before each new SUB or FUNCTION procedure. This tells BC to begin the procedure listing on a new page, to avoid starting it near the bottom of a page. 'PAGEIF '$PAGEIF is related to '$PAGE, except it lets you specify that a new page is to be started only if a certain minimum number of lines remain on the current page. For example, '$PAGEIF: 6 tells BC to advance to the next page only if there are six or less printable lines remaining.
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 120 -
'$PAGESIZE You can specify the length of each page with the '$PAGESIZE metacommand, to override the 66-line default. This would be useful with laser printers, if you are using a small font that supports more than that many lines on each page. Notice that a 6-line bottom margin is added automatically, so specifying a page size of 66 results in only 60 actual lines of text on each page. The largest value that can be used with '$PAGESIZE is 255, and the smallest is 15. To set the page length to 100 lines you would use '$PAGESIZE: 100. There is no way to disable the page numbering altogether, and using values outside this range result in a warning error message. '$OCODE Using '$OCODE (object code) allows you to turn the assembly language source listing on or off, using "+" or "-" arguments. Normally, the /a switch is needed to tell BC to include the assembly language code in the list file. But you can optionally begin a listing at any place in the program with '$OCODE +, and then turn it off again using '$OCODE -. '$SKIP Like '$PAGE and '$PAGEIF, the '$SKIP option lets you control the appearance of the source listing. '$SKIP accepts a colon and a numeric argument that tells BC to print that many blank lines in the list file or skip to the end of the page, whichever comes first. '$TITLE and '$SUBTITLE By default, each page of the list file has a header that shows the current page number, and date and time of compilation. The '$TITLE and '$SUBTITLE metacommands let you also specify one or two additional strings, which are listed at the start of each page. Using '$TITLE: 'My program' tells BASIC to print the text between the single quotes on the first line of each page. If a subtitle is also specified, it will be printed on the second line. Note that the title will be printed on the first page of the list file only if the '$TITLE metacommand is the very first line in the BASIC source file. LINKING ======= Once a program has been compiled to an object file, it must be linked with the routines in the BASIC library before it can be run. LINK combines one or more object files with routines in a library, and produces an executable program file having an .EXE extension. LINK is also used to create Quick Libraries for use in the QB editing environment, and that is discussed later in this chapter. LINK can combine multiple BASIC object files, as well as object files created with other Microsoft-compatible languages. In the section that follows you will learn how the LINK command line is structured, what each parameter is for, and how the many available options may be used. Using the various LINK options can reduce the size of your programs, and help them run faster as well. I should mention here it is imperative that you use the correct version of LINK. DOS comes with an old version of [Link] that is not suitable for use with QuickBASIC or BASIC PDS. Therefore, you should always use the [Link] program that came with your compiler. I also suggest that you remove or rename the copy of LINK that came with DOS if it is still on your hard disk. More than once I have seen programmers receive
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 121 -
inexplicable LINK error messages because their PATH setting included the \DOS directory. In particular, many of the switches that current versions of LINK support cause an "Unrecognized option" message from older versions. If the correct version of LINK is not in the current directory, then DOS will use its PATH variable to see where else to look, possibly running an older version. The LINK command line is structured as follows, using brackets to indicate optional information. The example below is intended to be entered all on one line. link [/options] objfile [objfile] [[Link]], [exefile], [mapfile], [libfile] [libfile] [;] As with the BC compiler, you may either enter all of the information on a single command, let LINK prompt you for the file names, or use a combination of the two. That is, you could enter LINK [filename] and let LINK prompt you for the remaining information. Default choices are displayed by LINK, and these are used if Enter alone is pressed. Typing a semicolon on a prompt line by itself or after a file name tells LINK to assume the default responses for the remaining fields. LINK also lets you use a *response file* to hold the file names and options. When there are dozens or even hundreds of files being specified, this is the only practical method. Response files are described later in this section. Also like BC, the separating commas are required as place holders when successive fields are omitted. For example, the command: link program , , mapfile; links [Link] to produce [Link], and creates a map file with the name [Link]. If the second comma had not been included, the output file would be named [Link] and a map file would not be written at all. The first LINK argument is one or more optional command switches, which let you control some of the ways in which link works. For example, the /co switch tells LINK to add line number and other information needed when debugging the resultant EXE program with CodeView. Another option, /ex, tells LINK to reduce the size of the program using a primitive form of data compression. Each LINK option will be discussed in the section that follows, and we won't belabor them here. The second argument is the name of the main program object module, which contains the code that will be executed when the program is run from the DOS command line. Many programs use only a single object file; however, in a multi-module program you must list the main module first. That is then followed by the other modules that contain additional subprograms and functions. Of course, you can precede any file name with a drive letter and/or directory name as necessary. You may also specify that all of the object modules in an entire library be included in the executable program by entering the library name where the object name would be given. Since LINK assumes an .OBJ file extension, you must explicitly include the .LIB extension when linking an entire library. For example, the command link mainprog [Link]; creates a program named [Link] which is comprised of the code in [Link] and all of the routines in [Link]. Normally, a library is specified at the end of the LINK command line. However, in that case only the routines that are actually called will be added to the program. Placing a library name in the object name field tells LINK to add all of the routines it contains, regardless of whether they are actually needed. Normally you do not want LINK to include unused routines, but that is often needed when creating Quick Libraries which will be discussed in a moment. Notice that when more than one object file is given, the first listed is the one that is run initially. Its name is also used for the executable
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 122 -
file name if an output file name is not otherwise given. Like the BC compiler, LINK assumes that you are using certain file naming conventions but lets you override those assumptions with explicit extensions. I recommend that you use the standard extensions, and avoid any unnecessary heartache and confusion. In particular, using non-standard names is a poor practice when more than one programmer is working on a project. Also notice that either spaces or plus signs (+) may be used to separate each object and library file name. Which you use is a matter of personal preference. The third LINK field is the optional executable output file name. If omitted, the program will use the base name of the first object file listed. Otherwise, the specified name will be used, and given an .EXE extension. Again, you can override the .EXE extension, but this is not recommended. Following the output file name field is the map file entry. A map file contains information about the executable program, such as segment names and sizes, the size of the stack, and so forth. The /map option, which is described later, tells LINK to include additional information in the map file. In general, a map file is not useful in high-level language programming. One interesting LINK quirk is that it will create a map file if empty commas are used, but not if a semicolon is used prior to that field. You can specify the reserved DOS device name nul to avoid creating a map file. For example, the command link program, , nul, library; links [Link] to create [Link], but not does not create the file [Link]. I use a similar line in the batch files I use for compiling and linking, to avoid cluttering my hard disk with these useless files. The last field specifies one or more libraries that hold additional routines needed for the program. In purely BASIC programming you do not need to specify a library name, because the compiler specifies a default library in the object file header. If you are linking with assembly or other language subroutines that are in a library, you would list the library names here. You can list any number of library names, and LINK will search each of them in turn looking for any routines it does not find in the object files. The version of LINK that comes with BASIC 7 also accepts a definitions file as an optional last argument. But that is used only for OS/2 and Windows programming, and is not otherwise needed with BASIC. LINK OPTIONS All of the available LINK options that are useful with BASIC running under DOS are shown following in alphabetical order. As with the switches supported by BC, each is specified on the LINK command line by preceding it forward slash (/). Many of the options may be abbreviated by entering just the first few letters of their name. For example, what I refer to as the /co option is actually named /codeview; however, the first two letters are sufficient for LINK to know what you mean. Each option is described using only enough letters to understand the meaning of its name. You can see the full name for those options in the section headers below, or run LINK with the /help switch. Any switch may be specified using only as many characters as needed to distinguish it from other options. That is, /e is sufficient to indicate /exepack because it is the only one that starts with that letter. But you must use at least the first three characters of the /nologo switch, since /no could mean either /nologo or /nodefaultlibrary. The details section for each option shows the minimum letters that are actually needed. /BATCH
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 123 -
Using /ba tells LINK that you are running it from a batch file, and that it is not to pause and prompt for library names it is unable to find. When /ba is used and external routines are not found, a warning message is issued rather than the usual prompt. The /ba option is not generally very useful--even if you are linking with a batch file--since it offers no chance to fix an incorrect file or directory name. One interesting LINK quirk worth noting is when it is unable to find a library you must include a trailing backslash (\) after the path name when reentering it manually. If LINK displays the prompt "Enter new file spec:" and you type \pathname, you are telling LINK to use the library named [Link] and look for it in the root directory. What is really needed is to enter \pathname\, which tells it to look in that directory for the library. Furthermore, if you initially enter the directory incorrectly, you must then specify both the directory and library name. If you are not sure of the default library name it is often easier to simply press Ctrl-C and start again. /CODEVIEW The /co switch is necessary when preparing a program for debugging with CodeView. Because of the extra information that LINK adds to the resultant executable file, /co should be used only for debugging purposes. However, the added data is stored at the end of the file, and is not actually loaded into memory if the program is run from the DOS command line. The program will therefore have the same amount of memory available to it as if /co had not been used. /EXEPACK When /e is used, LINK compresses repeated character strings to reduce the executable file size. Because variables and static arrays are initialized to zero by the compiler, they are normally stored in the file as a group of CHR$(0) zero bytes. The /e switch tells LINK to replace these groups of zero bytes with a group count. Then when the program is run, the first code that actually executes is the unpacking code that LINK adds to your program. This is not unlike the various self-extracting archive utilities that are available commercially and as shareware. Notice that the compression algorithm LINK employs is not particularly sophisticated. For example, SLR System's OptLink is an alternate linker that reduces a program to a much smaller file size than Microsoft's LINK. PKWare and SEA Associates are two other third-party companies that produce utilities to create smaller executable files that unpack and run themselves automatically. /FARCALLTRANSLATE By default, all calls from BASIC to its runtime library routines are far calls, which means that both a segment and address are needed to specify the location of the routine being accessed. Assembly language and C routines meant to be used with BASIC are also designed as far calls, as are BASIC subprograms and functions. This affords the most flexibility, and also lets you create programs larger than could fit into a single 64K segment. Within the BASIC runtime library there are both near and far calls to other library routines. Which is used depends on the routines involved, and how the various segments were named by the programmers at Microsoft. Because a far call is a five-byte instruction compared to a near call which is only three, a near call requires less code and can execute more quickly. In many cases, separate code segments that are less than 64K in size can be combined by LINK to form a single segment. The routines in those segments could then be accessed using near calls. However, BASIC always generates far calls as it compiles your programs.
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 124 -
The /f option tells LINK to replace the far calls it encounters with near calls, if the target address is indeed close enough to be accessed with a near call. The improvement /f affords is further increased by also using the /packcode switch (see below). Although the far call is replaced with a near call, LINK can't actually reduce the size of the original instruction. Instead it inserts a Nop (no operation) assembly language command where part of the far call had been. But since a near call does not require segment relocation information in the .EXE file header, the file size may be reduced slightly. See the text that accompanies Figure 51 earlier in this chapter for an explanation of DOS' loading and relocation process. There is one condition under which the /f option can cause your program to fail. The machine code for a far call is a byte with the value of &H9A, which is what LINK searches for as it converts the far calls to near ones. Most high-level languages, store all data in a separate segment, which is ignored by LINK when servicing /f. BASIC, however, stores line label addresses in the program's code segment when ON GOTO and the other ON commands are used. If one of those addresses happens to be &H9A, then LINK may incorrectly change it. In my personal experience, I have never seen this happen. I recommend that you try /f in conjunction with /packc, and then test your program thoroughly. You could also examine any ON statements with CodeView if you are using them, to determine if an address happens to contain the byte &H9A. /HELP Starting LINK with the /he option tells it to display a list of all the command options it recognizes. This is useful both as a reminder, and to see what new features may have been added when upgrading to a newer compiler. In many cases, new compilers also include a new version of LINK. /INFO The /inf switch tells LINK to display a log of its activity on the screen as it processes your file. The name of each object file being linked is displayed, as are the routines being read from the libraries. It is extremely unlikely that you will find /inf very informative. /LINENUM If you have compiled with the /zd switch to create SYMDEB information, you will also need to specify the /li LINK switch. This tells LINK to read the line number information in the object file, and include it in the resultant executable program. SYMDEB is an awkward predecessor to CodeView that is also hard to use, and you are not likely to find /li useful. /MAP If you give a map file name when linking, LINK creates a file showing the names of every segment in your program. The /m switch tells LINK to also include all of the public symbol names. A public symbol is any procedure or data in the object file whose address must be determined by LINK. This information is not particularly useful in purely BASIC programming, but it is occasionally helpful when writing subroutines in assembly language. Segment naming and grouping will be discussed in Chapter 13. /NODEFAULTLIB When BC compiles your program, it places the default runtime library name into the created object file's header. This way you can simply run LINK,
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 125 -
without having to specify the correct library manually. Before BASIC PDS there were only two runtime library names you had to deal with--QuickBASIC 4.5 uses [Link] and [Link]. But PDS version 7.1 comes with 16 different libraries, each intended for a different use. For example, there are BRUN and BCOM libraries for every combination of near and far strings, IEEE and /fpa (alternate) math, and DOS and OS/2. That is, [Link] stands for BASIC Runtime 7.1 Emulator Far strings Real mode. Likewise, BCL71ANP is for use with a BCOM stand-along program using Alternate math and Near strings under OS/2 Protected mode. Using /nod tells LINK not to use the library name imbedded within the object file, which of course means that you must specify a library name manually. The /nod switch also accepts an optional colon and explicit library name to exclude. That is, /nod:libname means use all of the default libraries listed in the object file except libname. In general, /nod is not useful with BASIC, unless you are using an alternate library such as Crescent Software's P.D.Q. Another possible use for /nod is if you have renamed the BASIC libraries. /NOEXTDICT As LINK combines the various object files that comprise your program with routines in the runtime library, it maintains a table of all the procedure and data names it encounters. Some of these names are in the object modules, such as the names of your BASIC subprograms and functions. Other procedure names are those in the library. In some situations the same procedure or data name may be encountered more than once. For example, when you are linking with a stub file it will contain a routine with the same name as the one it replaces in BASIC's library. Usually, LINK will issue an error message when it finds more than one occurrence of a public name. If you use /noe (No Extended Dictionary) LINK knows to use the routine or data item it finds first, and not to issue an error message. The /noe option should be used only when necessary, because it causes LINK to run more slowly. Linking with stub files is described separately later in this chapter. /NOFARCALL The /nof switch is usually not needed, since by default LINK does not translate far calls to near ones (see /farcalltranslate earlier in this section). But since you can set an environment variable to tell LINK to assume /far automatically, /nof would be used to override that behavior. Setting LINK options through the use of environment variables is described later in this chapter. /NOLOGO The /nol switch tells LINK not to display its copyright notice, and, like the /t BC switch may be used to minimize screen clutter. /NOPACKCODE As with the /nof switch, /nop is not necessary unless you have established /packc as the default behavior using an environment variable. /OVERLAYINT When you have written a program that uses overlays, BASIC uses an *overlay manager* to handle loading subprograms and functions in pieces as they are needed. Instead of simply calling the overlay manager directly, it uses an
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 126 -
interrupt. This is similar to how the routines in a BRUN library are accessed. BASIC by default uses Interrupt &H3F, which normally will not conflict with the interrupts used by DOS, the BIOS, or network adapter cards. If an interrupt conflict is occurring, you can use the /o switch to specify that a different interrupt number be used to invoke the overlay manager. This might be necessary in certain situations, perhaps when data acquisition or other special hardware is installed in the host PC. /PACKCODE The /packc switch is meant to be used with /far, and it combines multiple adjacent code segments into as few larger ones as possible. This enable the routines within those segments to call each other using near, rather than far calls. When combined with /f, /packc will make your programs slightly faster and possibly reduce their size. /PAUSE Using /pau tells link to pause after reading and processing the object and library files, but before writing the final executable program to disk. This is useful only when no hard drive is available, and all of the files will not fit onto a single floppy disk. /QUICKLIB The /q switch tells LINK that you are creating a Quick Library having a .QLB extension, rather than an .EXE program file. A Quick Library is a special file comprised of one or more object modules, that is loaded into the QB editing environment. Although BASIC can call routines written in non-BASIC languages, they must already be compiled or assembled. Since the BASIC editor can interpret only BASIC source code, Quick Libraries provide a way to access routines written in other languages. Creating and using Quick Libraries is discussed separately later in this chapter. /SEGMENTS The /seg: switch tells LINK to reserve memory for the specified number of segment names. When LINK begins, it allocates enough memory to hold 128 different segment names. This is not unlike using DIM in a BASIC program you might write to create a 128-element string array. If LINK encounters more than 128 names as it processes your program, it will terminate with a "Too many segments" error. When that happens, you must start LINK again using the /seg switch. All of the segments in an object module that contain code or data are named according to a convention developed by Microsoft. Segment naming allows routines in separate files to ultimately reside in the same memory segment. Routines in the same segment can access each other using near calls instead of far calls, which results in smaller and faster programs. Also, all data in a BASIC program is combined into a single segment, even when the data is brought in from different modules. LINK knows which segments are to be combined by looking for identical names. The routines in BASIC's runtime library use only a few different names, and it is not likely that you will need to use /seg in most situations. But when writing a large program that also incorporates many non-BASIC routines, it is possible to exceed the 128-name limit. It is also possible to exceed 128 segments when creating a very large Quick Library comprised of many individual routines. The /seg switch requires a trailing colon, followed by a number that indicates the number of segment names to reserve memory for. For example, to specify 250 segments you would use this command line:
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 127 -
link /seg:250 program, , nul, library; In most cases, there is no harm in specifying a number that is too large, unless that takes memory LINK needs for other purposes. Besides the segment names, LINK must also remember object file names, procedure names, data variables that are shared among programs, and so forth. But if LINK runs out of memory while it is processing your program, it simply creates a temporary work file to hold the additional information. /STACK The /st: option lets you control the size of BASIC's stack. One situation where you might need to do this is if your program has deeply nested calls to non-static procedures. Likewise, calling a recursive subprogram or function that requires many levels of invocation will quickly consume stack space. You can increase the stack size in a QuickBASIC program by using the CLEAR command: CLEAR , , stacksize where stacksize specifies the number of bytes needed. However, CLEAR also clears all of your variables, closes all open files, and erases any arrays. Therefore, CLEAR is suitable only when used at the very beginning of a program. Unfortunately, this precludes you from using it in a chained-to program, since any variables being passed are destroyed. Using /stack: avoids this by letting you specify how much memory is to be set aside for the stack when you link the chained-to program. The /stack: option accepts a numeric argument, and can be used to specify the stack size selectively for each program module. For example, /stack:4096 specifies that a 4K block be set aside in DGROUP for use as a stack. Furthermore, you do not need to use the same value for each module. Since setting aside more stack memory than necessary impinges on available string space, you can override BASIC's default for only those modules that actually need it. Note that this switch is not needed or recommended if you have BASIC PDS, since that version includes the STACK statement for this purpose. STUB FILES (PDS and later) A stub file is an object module that contains an alternate version of a BASIC language statement. A stub file could also be an alternate library containing multiple object files. The primary purpose of a stub file is to let you replace one or more BASIC statements with an alternate version having reduced capability and hence smaller code. Some stub files completely remove a particular feature or language statement. Others offer increased functionality at the expense of additional code. Several stub files are included with BASIC PDS, to reduce the size of your programs. For example, [Link] removes the routines that handle serial communications, replacing them with code that prints the message "Feature stubbed out" in case you attempt to open a communications port. When BASIC compiles your program and sees a statement such as OPEN Some$ FOR OUTPUT AS #1, it has no way to know what the contents of Some$ will be when the program runs. That is, Some$ could hold a file name, a device name such as "CON" or "LPT1:", or a communications argument like "COM1:2400,N,8,1,RS,DS". Therefore, BASIC instructs LINK to include code to support all of those possibilities. It does this by placing all of the library routine names in the object file header. When the program runs, the code that handles OPEN examines Some$ and determines which routine to actually call. Within BASIC's runtime library are a number of individual object modules, each of which contains code to handle one or more BASIC statements. In chapter 1 you learned that how finely LINK can extract
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 128 -
individual routines from BASIC's libraries depends on how the routines were combined in the original assembly language source files. In BASIC 7.1, using the SCREEN function in a program also causes LINK to add the routines that handle CSRLIN and POS(0), even if those statements are not used. This is because all three routines are in the same object module. The manner in which these routines are combined is called *granularity*, and a library's granularity dictates which routines can be replaced by a stub file. That is, a stub file that eliminated the code to support SCREEN would also remove CSRLIN and POS(0). Some of the stub files included with BASIC 7 PDS are [Link], [Link], and [Link]. [Link] removes all support for graphics, [Link] eliminates the code needed to send data to a printer, and [Link] contains a small subset of the many runtime error messages that a BASIC program normally contains. Other stub files selectively eliminate VGA or CGA graphics support, and another, [Link], adds the extra code necessary for the BASIC overlay manager to operate with DOS 2.1. When linking with a stub file, it is essential that you use the /noe LINK switch, so LINK will not be confused by the presence of two routines with the same name. The general syntax for linking with a stub file is as follows: link /noe basfile stubfile; Of course, you could add other LINK options, such as /ex and /packc, and specify other object and library files that are needed as well. You can also create your own BASIC stub files, perhaps to produce a demo version of a program that has all features except the ability to save data to disk. In order for this to work, you must organize your subprograms and functions such that all of the routines that are to be stubbed out are in separate source files, or combined together in one file. In the example above, you would place the routines that save the data in a separate file. Then, simply create an empty subprogram that has the same name and the same number and type of parameters, and compile that separately. Finally, you would link the BASIC stub file with the rest of the program. Note that such a replacement file is not technically a stub, unless the BASIC routines being replaced have been compiled and placed into a library. But the idea is generally the same. QUICK LIBRARIES For many programmers, one of the most confusing aspects of Microsoft BASIC is creating and managing Quick Libraries. The concept is quite simple, however, and there are only a few rules you must follow. The primary purpose of a Quick Library is to let you access non-BASIC procedures from within the BASIC editor. For example, BASIC comes with a Quick Library that contains the Interrupt routine, to let you call DOS and BIOS system services. A Quick Library can contain routines written in any language, including BASIC. Although the BASIC editor provides a menu option to create a Quick Library, that will not be addressed here. Rather, I will show the steps necessary to invoke LINK manually from the DOS command line. There are several problems and limitations imposed by BASIC's automated menus, which can be overcome only by creating the library manually. One limitation is that the automated method adds all of the programs currently loaded into memory into the Quick Library, including the main program. Unfortunately, only subprograms and functions should be included. Code in the main module will never be executed, and its presence merely wastes the memory it occupies. Another, more serious problem is there's no way to specify a /seg parameter, which is needed when many routines are to be included in the library. [Actually, you can set a DOS environment variable that tells LINK to default to a given number of segments. But that too has problems when using VB/DOS, because the VB/DOS editor specifies a /seg: value manually, and incorrectly. Unfortunately, LINK honors the value passed to it by
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 129 -
VB/DOS, rather than the value you assigned to the environment variable.] Quick Libraries are built from one or more object files using LINK with the /q switch, and once created may not be altered. Unlike the [Link] library manager that lets you add and remove object files from an existing .LIB library, there is no way to modify a Quick Library. When LINK combines the various components of an executable file, it resolves the data and procedure addresses in each object module header. The header contains relocation information that shows the names of all external routines being called, as well as where in the object file the final address is to be placed. Since the address of an external routine is not known when the source file is compiled or assembled, the actual CALL instruction is left blank. This was described earlier in this chapter in the section *Overview of Compiling and Linking*. Resolving these data and procedure addresses is one of the jobs that LINK performs. Because the external names that had been in each object file are removed by LINK and replaced with numeric addresses, there is no way to reconstruct them later. Similarly, when LINK creates a Quick Library it resolves all incomplete addresses, and removes the information that shows where in the object module they were located. Thus, it is impossible to extract an object module from a Quick Library, or to modify it by adding or removing modules. Understand that the names of the procedures within the Quick Library are still present, so QuickBASIC can find them and know the addresses to call. But if a routine in a Quick Library in turn calls another routine in the library, the name of the called routine is lost. Creating a Quick Library Quick Libraries are created using the version of LINK that came with your compiler, and the general syntax is as follows: link /q obj1 [obj2] [[Link]] , , nul , support; The support library file shown above is included with BASIC, and its name will vary depending on your compiler version. The library that comes with QuickBASIC version 4.5 is named [Link]; BASIC 7 instead includes [Link] for the same purpose. You must specify the appropriate support library name when creating a Quick Library. Notice that LINK also lets you include all of the routines in one or more conventional (.LIB) libraries. Simply list the library names where the object file names would go. The .LIB extension must be given, because .OBJ is the default extension that LINK assumes. You can also combine object files and multiple libraries in the same Quick Library like this: link /q obj1 obj2 [Link] [Link] , , nul , support; Although Quick Libraries are necessary for accessing non-BASIC subroutines, you can include compiled BASIC object files. In general, I recommend against doing that; however, there are some advantages. One advantage is that a compiled subprogram or function will usually require less memory, because comments are not included in the compiled code and long variable names are replaced with equivalent 2-byte addresses. Another advantage is that compiled code in a Quick Library can be loaded very quickly, thus avoiding the loading and parsing process needed when BASIC source code is loaded. But there are several disadvantages to storing BASIC procedures in a Quick Library. One problem is that you cannot trace into them to determine the cause of an error. Another is that all of the routines in a Quick Library must be loaded together. If the files are retained in their original BASIC source form, you can selectively load and unload them as necessary. The last disadvantage affects BASIC 7 [and VB/DOS] users only. The QBX [and VB/DOS] editors places certain subprogram and function procedures into expanded memory if any is available. Understand that all procedures are not placed there; only those whose BASIC source code size is
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 130 -
between 1K and 16K. But Quick Libraries are always stored in conventional DOS memory. Therefore, more memory will be available to your programs if the procedures are still in source form, because they can be placed into EMS memory. Note that when compiling BASIC PDS programs for placement in a Quick Library, it is essential that you compile using the /fs (far strings) option. Near strings are not supported within the QBX editor, and failing to use /fs will cause your program to fail spectacularly. RESPONSE FILES A response file contains information that LINK requires, and it can completely or partially replace the commands that would normally be given from the DOS command line. The most common use for a LINK response file is to specify a large number of object files. If you are creating a Quick Library that contains dozens or even hundreds of separate object files, it is far easier to maintain the names in a file than to enter them each time manually. To tell LINK that it is to read its input from a response file enter an at sign (@) followed by the response file name, as shown below. link /q @[Link] Since the /q switch was already given, the response file need only contain the remaining information. A typical response is shown in the listing below. object1 object2 object3 object4 object5 qlbname nul support + + + +
Even though this example lists only five object files, there could be as many as necessary. Each object file name except the last one is followed by a plus sign (+), so LINK will know that another object file name input line follows. The qlbname line indicates the output file name. If it is omitted and replaced with a blank line, the library will assume the name of the first object file but with a .QLB extension. In this case, the name would be [Link]. The nul entry could also be replaced with a blank line, in which case LINK would create a map file named [Link]. As shown in the earlier examples, the support library will actually be named BQLB45 or QBXQLB, depending on which version of BASIC you are using. LINK recognizes several variations on the structure of a response file. For example, several object names could be placed on each line, up to the 126-character line length limit imposed by DOS. That is, you could have a response file like this: object1 object2 object3 + object4 object5 object6 + ... I have found that placing only one name on each line makes it easier to maintain a large response file. That also lends itself to keeping the names in alphabetical order. You may also place the various option switches in a response file, by listing them on the first line with the object files: /ex /seg:250 object1 +
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 131 -
object2 + ... Response files can be used for conventional linking, and not just for creating Quick Libraries. This is useful when you are developing a very large project comprised of many different modules. Regardless of what you are linking, however, understanding how response files are used is a valuable skill. LINKING WITH BATCH FILES Because so many options are needed to fully control the compiling and linking process, many programmers use a batch file to create their programs. The [Link] batch file below compiles and links a single BASIC program module, and exploits DOS' replaceable batch parameter feature. bc /o /s /t %1; link /e /packc /far /seg:250 %1, , nul, mylib; Like many programs, a batch file can also accept command line arguments. The first argument is known within the batch file as %1, the second is %2, and so forth, up to the ninth parameter. Therefore, when this file is started using this command: c myprog the compiler is actually invoked with the command bc /o /s /t myprog; The second line becomes link /e /far /packc /seg:250 myprog, , nul, mylib; That is, every occurrence of the replaceable parameter %1 is replaced by the first (and in this case only) argument: myprog. I often create a separate batch file for each new project I begin, to avoid having to type even the file name. I generally use the name [Link] because its purpose is obvious, and it requires typing only one letter! Once the project is complete, I rename the batch file to have the same first name as the main BASIC program. This lets me see exactly how the program was created if I have to come back to it again months later. An example of a batch file that compiles and links three BASIC source files is shown below. bc /o /s /t mainprog; bc /o /s /t module1; bc /o /s /t module2; link /e /packc /far mainprog module1 module2, , nul, mylib; Of course, you'd use the compiler and link switches that are appropriate to your particular project. You could also specify a LINK response file within a batch file. In the example above you would replace the last line with a command such as this: link @[Link]; LINKING WITH OVERLAYS (PDS and VB/DOS PRO EDITION ONLY) At one time or another, most programmers face the problem of having an executable program become too large to fit into memory when run. With QuickBASIC your only recourse is to divide the program into separate .EXE files, and use CHAIN to go back and forth between them. This method
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 132 -
requires a lot of planning, and doesn't lend itself to structured programming methods. Each program is a stand-alone main module, rather than a subprogram or function. Worse, chaining often requires the same subroutine code to be duplicated in each program, since only one program can be loaded into memory at a time. If both [Link] and [Link] make calls to the same subprogram, that subprogram will have to be added to each program. Obviously, this wastes disk space. BASIC 6.0 included the BUILDRTM program to create custom runtime program files that combines common subroutine code with the BASIC runtime library. But that program is complicated to use and often buggy in operation. Therefore, one of the most useful features introduced with BASIC 7 is support for program overlays. An overlay is a module that contains one or more subprograms or functions that is loaded into memory only when needed. All overlaid modules are contained in a single .EXE file along with the main program, as opposed to the separate files needed when programs use CHAIN. The loading and unloading of modules is handled for you automatically by the overlay manager contained in the BASIC runtime library. Consider, as an example, a large accounting program comprised of three modules. The main module would consist of a menu that controls the remaining modules, and perhaps also contains some ancillary subprograms and functions. The second module would handle data entry, and the third would print all of the reports. In this case, the data entry and reporting modules are not both required at the same time; only the module currently selected from the menu is necessary. Therefore, you would link those modules as overlays, and let BASIC's overlay manager load and unload them automatically when they are called. The overall structure of an overlaid program is shown in Figure 5-4.
- 133 -
+----------------------------+ '**** [Link] CALL Menu(Choice) IF Choice = 1 THEN CALL EnterData ELSEIF Choice = 2 THEN CALL DoReports END IF +---------------------------- SUB Menu(Choice) ... CALL GetChoice(Choice) ... END SUB +---------------------------- SUB GetChoice(ChoiceNum) ... ... END SUB +----------------------------+ +----------------------------+ '*** [Link] SUB EnterData ... CALL GetChoice(Choice) ... END SUB +----------------------------+ +----------------------------+ '*** [Link] SUB DoReports PRINT "Which report? "; CALL GetChoice(Choice) ... ... END SUB +----------------------------+ Figure 5-4: The structure of a program that uses overlays.
Here, the main program is loaded into memory when the program is first run. Since the main program also contains the Menu and GetChoice subprograms, they too are initially loaded into memory. Understand that the main program is always present in memory, and only the overlaid modules are swapped in and out. Thus, EnterData and DoReports can both freely call the GetChoice subprogram which is always in memory, without incurring any delay to load it into memory from disk. If the host computer has expanded memory, BASIC will use that to hold the overlaid modules. Since EMS can be accessed much more quickly than a disk, this reduces the load time to virtually instantaneous. You should be aware, however, that BASIC PDS contains a bug in the EMS portion of its overlay manager. If EMS is present but less than 64K is available, your program will terminate with the error message "Insufficient EMS to load overlay." If no expanded memory is available, BASIC simply reads the overlaid modules from the original disk file each time they are called. It should also use the disk if it determines that there isn't enough EMS to handle the overlay requirements, but it doesn't. Therefore, it is up to your users to determine how much expanded memory is present, and disable the EMS driver in their PC if there isn't at least 64K. To specify that a module is to be overlaid, simply surround its name
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 134 -
with parentheses when linking. Using the earlier example shown in Figure 5-4, you would link [Link] with [Link] and [Link] as follows: link mainprog (enterdat) (reports); Of course, you may include any link switches that are needed, and also include any non-overlaid object files. Any object file names that are not surrounded by parentheses will be kept in memory at all times. Therefore, you should organize your programs such that subprograms and functions that are common to the entire application are always loaded. Otherwise, the program could become very slow if those procedures are swapped in and out of memory each time they are called. OTHER LINK DETAILS The BASIC PDS documentation lists no less than 143 different LINK error messages, and at one time or another you are bound to see at least some of those. LINK errors are divided into two general categories: warning errors and fatal errors. Warning errors can sometimes be ignored. For example, failing to use the /noe switch when linking with a stub file produces the message "Symbol multiply defined", because LINK encountered the same procedure name in the stub file and in the runtime library. In this case LINK simply uses the first procedure it encountered. In general, however, you should not run a program whose linking resulted in any error messages. Fatal errors are exactly that--an indication that LINK was unable to create the program successfully. Even if an .EXE file is produced, running it is almost certain to cause your PC to lock up. One example of a fatal error is "Unresolved external." This means that your program made a call to a procedure, but LINK wasn't able to find its name in the list of object and library files you gave it. Another fatal error is "Too many segments." You might think that LINK would be smart enough to finish reading the files, count the number of segment names it needs, and then restart itself again reserving enough memory. Unfortunately, it isn't. Regardless of the type of error messages you receive, it is impossible to read all of them if there are so many that they scroll off the screen. Although you can press Ctrl-P to tell DOS to echo the messages to your printer, there is an even better method. You can use the DOS redirection feature to send the message to a disk file. This lets you load the file into a text editor for later perusal. To send all of LINK's output to a file simply use the "greater than" symbol (>) specifying a file name as follows: link [/options] [object files]; > [Link] Instead of displaying the messages on the screen, DOS intercepts and routes them to the [Link] file. It is important to understand that this is a DOS issue, and has nothing to do with LINK. Therefore, you can use this same general technique to redirect the output of most programs to a file. Note that using redirection causes *all* of the program's output to go to the file, not just the error messages. Therefore, nothing will appear to happen on the screen, since the copyright and sign-on notices are also redirected. Another LINK detail you should be aware of is that numeric arguments may be given in either decimal or hexadecimal form. Any LINK option that expects a number--for example, the /seg: switch--may be given as a Hexadecimal value by preceding the digits with 0x. That is, /seg:0x100 is equivalent to /seg:256. The use of 0x is a C notation convention, and the "x" character is used because it sounds like "hex". Finally, if you are using QuickBASIC 4.0 there is a nasty bug you should be aware of. All versions of QuickBASIC let you create an executable program from within the editing environment. And if a Quick Library is currently loaded, QB knows to link your program with a parallel .LIB library having the same name. But instead of specifying that library
Ethan Winer: PC Magazine's BASIC Techniques and Utilities Book - 135 -
in the proper LINK field, QB 4.0 puts its name in the object file position. This causes LINK to add every routine in the library to your program, rather than only those routines that are actually called. There is no way to avoid this bug, and QB 4.0 users must compile and link manually from DOS. MAINTAINING LIBRARIES ===================== As you already know, multiple object files may be stored in a single library. A library has a .LIB extension, and LINK can extract from it only those object modules actually needed as it creates an executable file. All current versions of Microsoft compiled BASIC include the [Link] program, which lets you manage a library file. With [Link] you can add and remove objects, extract a copy of a single object without actually deleting it from the library, and create a cross-referenced list of all the procedures contained therein. It is important to understand that a .LIB library is very different from a Quick Library. A .LIB library is simply a collection of individual object files, with a header portion that tells which objects are present, and where in the library they are located. A Quick Library, on the other hand, contains the raw code and data only. The routines in a Quick Library do not contain any of the relocation and address information that was present in the original object module. The runtime libraries that Microsoft includes with BASIC are .LIB libraries, as are third-party support libraries you might purchase. You can also create your own libraries from both compiled BASIC code and assembly language subroutines. The primary purpose of using a library is to avoid having to list every object file needed manually. Another important use is to let LINK add only those routines actually necessary to your final .EXE program. Like BC and LINK, you can invoke LIB giving all of the necessary parameters on a single command line, or wait for it to prompt you for the information. LIB can also read file names and options from a response file, which avoids having to enter many object names manually. A LIB response file is similar--but not identical--to a LINK response file. Using LIB response files will be described later in this section. The general syntax of the LIB command line is shown below, with brackets indicating optional information. lib [/options] libname [commands] , [listfile] , [newlib] [;] After any optional switches, the first parameter is the name of the library being manipulated, and that is followed by one or more commands that tell LIB what you want to do. A list file can also be created, and it contains the names of every object file in the library along with the procedure names each object contains. The last argument indicates an optional new library; if present LIB will leave the original library intact, and copy it to a new one applying the changes you have asked for. There are three commands that can be used with LIB, and each is represented using a punctuation character. However, LIB lets you combine some of these commands, for a total of five separate actions. This is shown in Table 5-1. Command ======= + * -+ -* Action ========================================= Add an object module or entire library. Remove an object module from the library. Extract a copy of an object module. Replace an object module with a new one. Extract and then remove an object module.
To add the file [Link] to the existing library [Link] you would use the plus sign (+) as follows: lib mylib +newobj; And to update the library using a newer version of an object already present in the library you would instead use this: lib mylib -+d:\newstuff\anyobj; As you can see, the combination operators use a sensible syntax. Here, you are instructing LIB to first remove [Link] from [Link], and then add a newer version in its place. A drive and directory are given just to show that it is possible, and how that would be specified. To extract a copy of an object file from a library, use the asterisk (*) command. Again, you can specify a directory in which the extracted file is to be placed, as follows: lib mylib *\objdir\thisobj; You should understand that LIB never actually modifies an existing library. Rather, it first renames the original library to have a .BAK extension, and then creates and modifies a new file using the original name. It is up to you to delete the backup copy once you are certain that the new library is correct. [But this backup is made only if you do not specify a new output library name--NEWLIB in the earlier syntax example.] If the named library does not exist, LIB asks if you want to create it. This gives you a chance to abort the process if you accidentally typed the wrong name. If you really do want to create a new library, simply answer Y (Yes) at the prompt. Of course, the only thing you can do to a non-existent library is add new objects to it with the plus (+) command. One important LIB feature is its