Issue #10 March, 1992 ======================================================================== The Pascal Newsletter (C) Copyright 1992 by Alex Boisvert ALL RIGHTS RESERVED [][][][][] [] [] [] [] [] [][] [] [] [] [] [] [] [] [] [][][][][] [] [] [] [] [] [] [] [] [] [] [] [] [] [] [] [] [][] [] [] [] [] [][][][][] T H E I N T E R N A T I O N A L P A S C A L N E W S L E T T E R ======================================================================== Table of Contents (*) Introduction ................................................. 2 by Alex Boisvert - Editor, Writer (*) BigFiles - A Large File Viewer................................ 3 by Mich Davis - Contributing Writer (*) BigArrays - You've Never Seen Arrays This BIG!................ 10 by Mich Davis - Contributing Writer (*) High-Quality Sound with Turbo Pascal.......................... 13 by Alex Boisvert - Editor, Writer (*) Overlays - How to fit your programs in memory................. 15 by Alex Boisvert - Editor, Writer (*) Turbo Vision Without Getting GUI.............................. 19 by Richard Morris - Editor Over-the-Pond (*) Answers to Frequently Asked Questions ........................ 26 by Trevor Carlsen - Contributing Writer (*) Running 80286 programmes on 8088/8086 Computers............... 37 by David J. N. Begley - Contributing Writer (*) Conclusion ................................................... 41 by Alex Boisvert - Editor, Writer The Pascal Newsletter is published by Alex Boisvert as a medium for all Pascal programmers to share ideas, comments, techniques or experiences concerning any subject related to the Pascal programming language. DataMAX BBS is the home of PNL. It can be reached in Sherbrooke, Quebec, Canada at (819) 563-6327 or FidoNet @ 1:167/405.0 between 23:00 and 8:00 Eastern Standard Time. Articles and source code submitted by others are the property of the authors and are used with permission. They may not be treated separately from this newsletter without the author's permission and thus maintain all distribution rules of the newsletter as a whole. It may be freely distributed in un-modified form, but no charge whatsoever may be incurred on the recipient. All code is provided 'as-is' with no guarantees whatsoever. ======================================================================== PNL #10 Page 2 March, 1992 ================================================================= Introduction - Editor's Note ================================================================= After more than a year of silence, the Myth becomes a reality. The Pascal NewsLetter has revived. Many people will ask themselves why this is issue #10 and why wasn't PNL published during the last year. The facts. Pete Davis, founder and ex-editor of PNL, had personal problems and was forced to sell his computer. He was unable to continue his work. I must thank him for the outstanding work he has done with PNL. He worked very hard to keep the issues out and each of them was a success. For the sake of the Pascal community, I am taking up the baton. I will be publishing the Pascal NewsLetter starting with issue #10 and up. I expect to be publishing one issue bimontly, if I have sufficient material (articles) to fill it. Since I am starting from scratch, everything has to be done a second time. I must find new columnists, contributing writers and people to distribute the newsletter all around the world. Richard Morris will be distributing it in Australia but, I have yet to find a distributor in Europe and, possibly, in Asia. As you may have noticed, I have changed a bit the format of the newsletter. The table of contents now shows on the first page. This will be handy if you are keeping the newletters on print. I hope you all enjoy the "New PNL" and I wish that more people get interested in it. Alex Boisvert PNL's New Chief Editor. PNL #10 Page 3 March, 1992 ================================================================= BigFiles - a large file viewer ================================================================= G'Day! Well, it sure is good to see the Pascal NewsLetter alive and well again. In this article and the followup in the next edition, we'll be working on a very useful tool - a technique for accessing large text files. On the way we'll be exploring a LOT of really neat techniques all rolled into the one program. Unless you're a closet guru, I'm SURE you'll find something you haven't come across yet. We'll be dabbling in: o Take-only-what-you-need dynamic memory allocation. o Vertical Scrolling. o Turbo Pascal's Built-in Assembler. o Using DOS to allocate memory. o Treating TEXT files as TYPED files. o Buffering Text files to improve access speed. o A technique for quick access to any part of a text file. o Objects. o Very large arrays (bigger than 64k in size, or more than 64k elements). o A standalone program for viewing text files, a la Buerg's LIST. o i286-capable programs. o Horizontal scrolling. o Multitasking. o Turbo Vision. o Optimization using Turbo Profiler. o Optimization using the built-in assembler. o DesqView-aware programs. This is all done with Turbo Pascal 6.0. I apologise to those who don't have 6.0, but that's progress. Part 1: [Note: I have a 286, so I compile my programs with $G+ on. If you don't, remove the $G+ from compiler options, and the "Test186" from the uses-block of each program it appears in.] What we aim to have by the end of this article is a tool which will allow us to view arbitrarily large text files. Each of the included programs builds on the one before it, starting with MDP1.PAS. The way we'll first explore is to read the lines of the file into an array of strings. MDP1.PAS is about as simple as you can get! It has two sections - the first section opens the file, and the second prints it forwards and backwards. Note that this series of programs get the name of the file to show from the command line, ie MDP1 c:\autoexec.bat. If you forget to give it a filename, the program will try to read from the keyboard. You can exit by pressing ^Z and Enter. Note that IDE users can set the command-line parameters by selecting [R]un, p[a]rameters inside the IDE, and [R]un, [A]rguments inside Turbo Debugger. Note that this program is extremely limited: PNL #10 Page 4 March, 1992 o It is wasteful of resources to declare an array larger than we'll need, but for many files, 100 lines won't be enough. o The maximum number of lines we can possibly extend this approach to, is about 250 lines. For these reasons, MDP1.PAS is a bit of an evolutionary dead-end. MDP2.PAS is a little better. This program works by reading the file line-by- line, and storing it IN ONLY AS MUCH MEMORY AS IT NEEDS to store it. Thus, files with (on average) shorter lines will be able to have more lines loaded before the computer runs out of memory. This program introduces the idea of a table to point to the start of the lines in the file. Once we have this table, it's a simple matter to look up the line, get the address of that line in memory and print it out. Note the table is made of records, with a 4-byte pointer field for the start of the line, and a word for the size of the line. We need to keep the size so that later we can feed the size to FreeMem - TP doesn't keep track of how big the memory chunk it allocates you with GetMem is, so you have to remind it at FreeMem time. MDP2.PAS is limited by two things - It stores the lines from the file on the heap, so can only handle files of about half a megabyte or less. Secondly, the table of line pointers is limited to about 10000 entries, since the address table can't be larger than 64k (ie, a little larger than 6*10000). Now let's turn our attention to something different - scrolling. Take a look at MDP3.PAS. Perhaps the best way to see what it does is to run it. When you do, you'll see a line of numbers, which you can move along by using the + and - keys. This program has two parts. The first draws the initial screen. Note the call to min (minimum) in the For - this roughly translates to "draw until you hit the end of the screen, or until you run out of numbers". The second part of the program is the standard repeat-until/readkey/case input loop. WinTop by-the-way is the line number which is currently displayed at the top of the screen. Note that the - and + sections scroll the screen by using Crt's DelLine and InsLine. An unfortunate consequence of this is that the status line at the bottom of the screen flickers quite badly when scrolling takes place. To stop this, you'd either have to put the status line at the top of the screen, or declare a Crt.Window. I've done neither since I didn't find it all that annoying. The followup article in the next PNL WILL address this problem. It's a good idea to step through this program using F7 to get an idea of just how it performs the scrolling. Note that after the initial redraw, only the lines that are just appearing are rewritten - this is faster, and reduces the inevitable screen flash (not CGA snow) that occurs when you update a screen line-by-line. PNL #10 Page 5 March, 1992 Now let's have a look at MDP4.PAS. This is a hybrid - it combines the file reading section from MDP1.PAS with the scroller from MDP3.PAS, so it shouldn't contain too many surprises. Note that in the scrolling section, the writeln's which outputted numbers have been replaced with a reference to the data at that line position (eg, line [WinTop]). This is then passed to procedure PrLn, which acts like a writeln, except that it chops off lines that are longer than 79 characters. If you don't do this, a long line will overflow onto the next, spoiling the display. Until now, the programs we've looked at have been fairly elementary. Well, you've been warned! Programs after this point get quite a bit harder to understand, for many reasons: o They're more sophisticated. o They use techniques not seen in everyday Turbo Pascal programs. o I wrote them, and you're having my less-than-perfect programming style inflicted on you. Sorry! :-D Let's look at three interesting areas before we proceed to MDP5.PAS. The first is a device which dramatically speeds up TEXT file access - SetTextBuf. SetTextBuf is part of Turbo Pascal's Run-Time Library [RTL]. It lets you tell Turbo Pascal to use a given area of memory as a cache or buffer when dealing with a text file. Let's say we had the following fragment of code: var TBuff:array [0..8191] of byte; f:text; line:string; begin assign (f,'whatever.doc'); SetTextBuf (f,TBuff); reset (f); readln (f,line); readln (f,line); close (f); end; Without the SetTextBuf, the two readlns would probably have cause two disk accesses. Instead, the RTL will now try to fill TBuff with as much data from f as it can. Then, requests to access the file come out of TBuff, instead of an access to the disk. If you don't already, try it in your next program which deals with lots of text files - you'll be pleasantly surprised at the speed increase! The second concept overcomes an annoying limitation with Turbo Pascal - when you're reading TEXT files, you have no way of using the FilePos function to find out where you are, or the Seek procedure to reposition yourself. To get around this, let me introduce to you ("How do you do?") a unit written by a fellow Australian - TextUtl2. This unit lets you perform FilePos, FileSize and Seek on text files, and is a real life saver! Third, the idea of going to DOS for memory, rather than Pascal's GetMem/FreeMem. Why? Because Turbo Pascal is limited to handing you blocks of 64k at a time, whereas DOS will happily give you a couple of PNL #10 Page 6 March, 1992 hundred k's of consecutive memory, which we'll be using for our mega arrays. Take a look at MDP5.PAS. The first thing to note is that now we don't read in the actual data from the file - all we do is store the file OFFSET of each line. Later when we want to go to a certain line, we'll look up its offset, Seek to that position, and happily read away. This lets us view up to 16000 lines, which is the limit of Vern Buerg's LIST program. Observe how the text buffer is set up: TBuffSize is set to how many K we want the buffer to be (I've picked 20k). Then TBuffType is declared to be an array of that size in K's. Note that the buffer differs from the code fragment we saw before, because it'll come off the heap, instead of being a static variable. Note the call to TextFilePos on line 47. This shows just how easy it is to use the TEXTUTL2 unit. Before each line is read, we store the offset for that line for future reference. If you're wide awake, you'll see that line 48 we use a readln without telling it where to put the data it reads. This is because we're not interested in what's in the line, just where they start. The reading of the data will be done later. So, the program reads in the file, and asks you for the starting line number. When you've typed a valid number in, line 64 looks up the offset of that line, and moves the file pointer to that position. The lines following that read the file to cover the screen, either until the bottom of the screen is hit, or the end of the file occurs. Note line 71 - this is a shady trick! If you use Crt.KeyPressed, then even when it detects a key it leaves it in the keyboard buffer. So, the program pauses at line 71 until you press a key. When you do, it loops back to the readln in line 61, which gets its first key free - the one you pressed. This trick lets you prompt for information in a way which isn't going to spoil your nice screen layout until the user is ready to type something. Note that you only have to perform ONE TextSeek for a whole screen. You COULD perform a TextSeek for EVERY line on the screen, but why bother? Reading a line leaves the file pointer (and cursor!) sitting fair and square on the next line, all ready for you to use. Note in line 75 we Dispose of TBuff AFTER the file has been closed. Now we're going to sidetrack for a moment. Just for an experiment, we're going to ask DOS for the memory for TBuff, instead of TP, which uses the heap. This program will be called MDP5A.PAS ("A" because it's an experiment). But wait! Do I hear you say there's no MDP5A.PAS included? You're right. Instead, I've put the DIFFERENCES between MDP5.PAS and MDP5A.PAS into a file called MDP5A.EDL. This file is really a script for EDLIN (and you thought you'd never use EDLIN again eh?!). To get MDP5A.PAS, type this at the command line: PNL #10 Page 7 March, 1992 C:\PNL010> edlin MDP5A.PAS < MDP5A.EDL ... and this will create MDP5A.PAS. In fact, I've built a Turbo Vision revision control system which uses purely DOS commands to do the nitty gritty work - it compares two files with FC, and generates an Edlin script for the differences. I'm telling you this because it'll be in a future edition of the Pascal Newsletter, so keep reading! (And keep writing, else there won't BE a future edition!) Note that MDP5A.PAS uses a unit called DosMem. I've written this as an easy-to-use interface to the DOS memory functions. There are three modules: function Alloc(paras:word): word; procedure Free(p:word); function Largest: word; Note that each of these works with "paragraphs", which on PCs means "16-bytes". Alloc is like TP's GetMem - it'll give you "paras" paragraphs of memory, returning with the segment of the block. If the request can't be met, it will return a segment of zero. Free is like TP's FreeMem - it'll hand the block at "p" back to DOS when you've finished with it. Note that you don't have to tell DOS how large the block was - now if only FreeMem would do that! Largest is like TP's MaxAvail - it'll tell you the largest size block you can ask for. The result is in paragraphs. Note that the DosMem unit is written using the Built-in assembler. Sorry, pre-6.0 people. Needle me enough and I'll write you a .OBJ or some inline code which does it. Ok, back to business. Line 39 of MDP5A.PAS performs the same function as line 39 in MDP5.PAS, except that it asks DosMem.Alloc for the memory. Because DosMem uses paragraphs, we have to convert the buffer size in K's (20) to paragraphs. We do this by multiplying by 64. (20k * 64 = 1280 paras = 1280 * 16 = 20480 bytes = 20k). Note that TBuff is a pointer type, with two halves - the segment, and the offset. The segment comes from the result of Alloc, and the offset will always be 0. The two are joined into a standard TP pointer by the Ptr function. The other lines that have changed (57 and 75) pass the segment of TBuff back to DOS - note that you do this AFTER the file is closed. The reason we change a perfectly good program (MDP5.PAS) into one that has to rely on DOS (!) is to give you a feel for working with DOS memory, which is what we'll be doing in the next program. MDP6.PAS is the first of our programs to use the BigArray unit, which is covered in another PNL article. Basically, the BigArray unit has an object type called BigDOSArray, which makes use of the >64k ability of DOS's memory management to implement a VERY large array. We'll be using one mega-array, and our data type is the longint, which takes 4 bytes. So, line 44 tells the array object all about it. Line 45 PNL #10 Page 8 March, 1992 asks the object to say how many elements it could possibly hold, of the type you've told it. In this case, we'll be using an element for every line, so line 46 tells us how many lines our program is capable of accessing. Line 47 asks the object to set up the array as LARGE as it can possibly be made. Note that this will consume ALL DOS memory. If you were using more than one of these objects, you'd probably not pass it the maximum. Line 55 is where things start to get interesting. One of BigDOSArray's methods is called Elem. If you pass it the element number, it will return with a pointer to where that element is stored in memory. So Elem gets passed the line number, and LinePtr then points to where we put the offset. Line 56 shows how the array gets its value. Because LinePtr holds the element's address, we could just as easily read from LinePtr^ as written to it, and that's what happens in lines 73 and 74. We find the offset of the line we want, we seek to the position, and we read the file, just like we did in MDP5.PAS. One last point - note the calls to the Done method of LineBank in lines 66 and 86. This hands back the memory the array consumed back to DOS. We now have the shell of a fairly powerful program. On my machine, it says it's happy to handle over 130,000 lines (60,000 in the TP IDE). The fidonet nodelist (052) is 16302 lines long, and in a few weeks, Vern Buerg's LIST program isn't going to be able to handle that. At least you've now got something that can! If you've understood everything so far, then relax - from now on, things are all down hill. MDP7.PAS is just MDP6.PAS with the scrolling stuff we worked out in MDP4.PAS. It covers nothing new, just merges the two techniques we developed before. MDP8.PAS is as far as we're going to take things this issue. Still, it's quite a program! It's been split up into separate procedures (as it should have right from the beginning!), and a number of features have been added. First, it shows you a percentage as you load the file. This is useful when you're loading the FidoNet nodelist, or an online version of the Bible [one of the best reasons to download it - it gives you a great exerciser for your text-processing programs! ;-D] Also, it now responds to extended keys, such as the arrows, and the PgUp/ PgDn/Home/End keys. ESC has replaced 'q' for exitting, and you can press # to jump to a certain line. This is accomplished by having TWO CASEs - one handles extended keys, and the other, normal files. I'm the first to admit that it's not the fastest thing around. That's why in the next issue of PNL we'll discuss optimising it. We'll use the profiler to investigate where the program is spending the most time (everywhere, ie, equally slow!). We'll rewrite a large chunk of it in assembler, and we'll use multitasking to remove the annoying delay at the start of the program. Also, we'll discuss how we'd port the program to TurboVision, and how to have it run RIGHT under DesqView. Finally, we'll see what we can do about viewing lines that are longer than the screen by horizontal scrolling. PNL #10 Page 9 March, 1992 Until next issue, Make merry with your Pascal! +-------------------------------------------+ | Mitch Davis | | 10/1072 Whitehorse Rd., | | Box Hill, Victoria, Australia. 3128. | | | | Ph. +61-3-890-2062. Fidonet: 3:634/384.6 | +-------------------------------------------+ PNL #10 Page 10 March, 1992 ================================================================= BigArray - you've never seen arrays this BIG! ================================================================= *** Error 22: Structure too large. *** Error 49: Data segment too large. *** Error 96: Too many variables. *** Runtime error 201 at XXXX:YYYY. {Range check} *** Runtime error 203 at XXXX:YYYY. {Heap full} How many times have you been frustrated by these little beauties? How many times has it been because you had an array that just wouldn't stay down? There are two ways to fix this - you could put your thinking on a diet and work out some other way that doesn't consume quite so much memory (recommended!) or you could do it the lazy way, and slot in a unit called the (drum roll!) *** BigArray ***. BigArray will give you arrays which can be as big as will fit in conventional memory, ie, they aren't limited to 64k. The arrays can be of any type, and are of a single dimension. (A version being written now lets you have arrays that spill over to disk or EMS, as well as multi dimensionals.) The large arrays are implemented as objects. You use the .SetElemSize method to tell it how big your elements are, and you can subsequently find out via .GetMaxSize how many elements the largest array can be. Then you call the Init method with the number of elements you'd like, and voila, your array is done! When you're finished with the array, call the .Done method. How are elements accessed? You pass the element subscript to the .Elem function, and it returns a pointer to that element. The idea is that you then assign that pointer to a pointer variable of the type you're storing. Then by deferencing the pointer (following it by a caret [^]) you can then access any fields, bits, WHATEVER that you would with a plain-jane variable. Let's look at a sample of how you'd use them: program BigArrayTest; {Program to accompany article in issue #10 of the Pascal NewsLetter. } {Author: Mitch Davis, (3:634/384.6) +61-3-890-2062. } {$G+,M 16384,0,0} {Make SURE you tell TP not to steal all the DOS memory } {for a heap! If you're not using dynamic variables, } {set both numbers to 0. } {This program provides a nonsense demonstration of how you use the tools } {in the BigArray unit. } uses Test186, BigArray; {I run my programs on a i286. If you have an XT,} {Remove the "G+," from above and the "Test186" } {from the uses section. } PNL #10 Page 11 March, 1992 type PigeonType = record value:real; changed:boolean; end; var PigeonHole:BigDosArray; PigeonPtr:^PigeonType; PHnum, MaxSize:longint; GlobalChanged:boolean; begin writeln ('Welcome to the pigeon-hole.'); with PigeonHole do begin {This sets up the big array.} SetElemSize (sizeof (PigeonType)); {Tells it how big each element will be} MaxSize := GetMaxSize; Init (MaxSize); {Make it as big as possible. This is not compulsory. } end; writeln ('There are ',MaxSize,' pigeon-holes, numbered from 1 to ' ,MaxSize,'.'); write ('Please wait while I clear them... '); for PHnum := 1 to MaxSize do begin PigeonPtr := PigeonHole.Elem (PHnum); {Get the address of the element} PigeonPtr^.Changed := false; {Reference the changed field within that} end; {element. } writeln ('Done.'); GlobalChanged := false; {This will save us search time later if no changes} repeat write ('Which pigeon hole? (1-',MaxSize,', 0 to quit): '); readln (PHnum); if (PHnum > 0) and (PHnum <= MaxSize) then begin PigeonPtr := PigeonHole.Elem (PHnum); with PigeonPtr^ do begin case Changed of true :writeln ('That pigeon-hole has the value of ',Value:3:2); false:writeln ('That is a new pigeon hole.'); end; write ('Change it to? '); readln (Value); Changed := true; {means this ph will be shown in the end summary} end; writeln ('Pigeon-hole changed.'); GlobalChanged := true; end; until PHnum = 0; writeln ('-------------------------------------------------'); case GlobalChanged of false:writeln ('You didn''t change any pigeon holes.'); true :begin writeln ('The pigeon holes you changed were:'); write ('Wait..',#13); for PHnum := 1 to MaxSize do begin {scan thru the ph's} PigeonPtr := PigeonHole.Elem (PHnum); with PigeonPtr^ do if changed then writeln (PHnum,': ',value:3:5); PNL #10 Page 12 March, 1992 end; end; end; writeln ('Thanks for using the pigeon-holes!'); PigeonHole.Done; end. If you understand this (in conjunction with the "interface" section of the BigArray unit), then you should have no problem working out how to use it. A few usage notes: o The Big Arrays are 1-based, that is if you call BigDOSArray.Init(10), then the element subscripts run from 1 up to 10. o There is NO checking to ensure that the element you pass to BigDOSArray.Elem is within the defined range. If it isn't, then you'll get garbage results for the pointer result. Beware! o The code for BigDOSArray hasn't been extensively tested. Caveat Emptor! o BigDOSArray needs optimising BADLY! I plan to write it in optimised assembler throughout; in the meantime, you'll have to sacrifice speed for convenience. o I am aware that Turbo Power markets code which implements large arrays (and probably MUCH better than I've managed here!). I haven't looked at their code. Trevor Carlsen's OverSize unit also performs a similar function. I looked at his code, and couldn't understand it. That's not meant as a slur on Trevor (3:690/644), who writes code so good it should be framed. I just thought I'd take a different approach. Upshot: If any of the other large-array products fit your bill better, use them! o The BigDOSArray unit currently compiles in i286 mode. If you have an XT or similar, chop out the G+ and Test186 bits, in the same way as the comments in the demo program mention. Well that about wraps it up. I'd be more than happy to answer any queries you have about the unit. Next issue, time and space permitting, I'm planning on having a followup article which goes into optimisation, multi-dimensional arrays, etc. Till then? Make merry with your Pascal! +-------------------------------------------+ | Mitch Davis | | 10/1072 Whitehorse Rd., | | Box Hill, Victoria, Australia. 3128. | | | | Ph. +61-3-890-2062. Fidonet: 3:634/384.6 | +-------------------------------------------+ PNL #10 Page 13 March, 1992 ================================================================= High Quality Sound in Turbo Pascal... Not Really. ================================================================= Many Turbo Pascal programmers find that the SOUND procedure does not produce anything interesting as far as music goes. This is not due to Turbo Pascal's limitation to use the speaker but due to the speaker itself. The original PC speaker is almost useless... Unless you happen to have the public-domain utility called RESPLAY, written by Mark J. Cox and (of course) Turbo Pascal. --- From RESPLAY v1.0 documentation: --- "RESPLAY is a memory resident program designed to help high-level language programmers make the most of the PCs useless speaker. By use of some simple procedures, you can playback digital samples from within your programs and sample yourself. Playback can either be to the PC speaker (no too bad) or to some external hardware [...] " Possible uses of RESPLAY: o Arcade Games ("He's Dead Jim!" or realistic shooting noises) o Sound Analysis (Fast Fourier Transforms/ analysing spoken words) o Spelling Games for kids. o Sampling your friends and making them sound silly (by changing their speed, or by FFTs and altering their pitch!) o The next 'Jive Bunny' single ------------------------------------------ Resplay is fairly easy to use from Turbo Pascal because it uses interrupt calls. Simply fill the register set and call interrupt 2F. If you have any knowledge of assembler, RESPLAY will work for you! The original archive file of RESPLAY contains a C source code that I have "ported" to Turbo Pascal. I say "ported" because the two programs work differently with memory (loading & storing digital samples) but they both do the same thing: Play a digital sample file. I have done much of the work for you. I have created a Pascal unit which can read and play any sample. I have used TP's INTR procedure to call the interrupt instead of direct BASM instructions for compatibility purposes. Turbo Pascal v5.5 users will be able to use this code without changing one line of code. The unit is name DIGISND.PAS and the program is PLAYDIGI.PAS. I must admit that the program is not fully optimized. It reads the sample file by allocating 64k memory segments and plays these segments one after the other... I have played samples up to 350k with it. If the sample is bigger than 64k, you will notice some delay (under 10 ms.) between each 64k "chunck". It is possible to tell RESPLAY to play the whole sample if the sample is stored continuously in memory. I have decided to play the game safely and play the samples by 64k segments. The code is object-oriented so that anyone wishing to modify it may do so by creating a descendant object type. Someone might want to add a method to play only part of the sample read (eg. from time x to time y), play PNL #10 Page 14 March, 1992 the sample on a COVOX card instead of the PC speaker, etc... Please refer to RESPLAY1.ZIP before modifying the unit. [Editor's Note: RESPLAY1.ZIP has been included in the distribution archive of PNL#10. It contains the original documentation and a digital sample.] By the way, I have tested RESPLAY with my program on a COVOX card and the resulting "music" is better than with a SoundBlaster card. Also, try getting Macintosh (Yes, Macintosh!) digital samples, they work just fine with RESPLAY. Hope you have fun with your PC speaker now! Alex Boisvert DataMAX Communications Enr. FidoNet @ 1:167/405 PNL #10 Page 15 March, 1992 ================================================================= Overlays - How to fit your programs in memory ================================================================= Overlays in Turbo Pascal help you, the programmer, to write programs that otherwise would not fit into the 640k limitation of DOS. They can also, in special cases, reduce your code's memory requierement in order to get more memory for dynamic allocations. To accomplish this, overlays are "parts" of your programs that are stored on disk and which are controlled by the program, meaning that they are loaded in memory only when they are needed. The principle is to allocate the same (limited) memory space to several sections of program... at different time obviously. The major drawback of using overlays is the time involved in loading the useful sections at run time, or "on-the-fly". You will therefore use this method only when available memory is less than your program's size. Your programs will then become independent of RAM memory available, to which the computer has access to. The developpers' of Turbo Pascal have kept to a minimum the rules to obey when using the overlays. Management of overlays is handled by the routines of the OVERLAY unit, which you will have to add to your USES statement at the beginning of your main program. The smallest portion of code which can be declared as "overlayed" is a unit. With Turbo Pascal, it is not possible to overlay specific routines. Each unit is loaded in the heap memory zone, which is the reserved memory between the stack and the memory of dynamically allocated variables. When using overlays, you must declare all your program's routines as FAR. The easiest way to do this is to add the following compiler directive {$F+}, just after your program's header. Any unit compied with the directive {$O+}, can then be compiled as an overlayed unit, but they do not have to. This directive will tell the compiler to produce the needed overlay control code. The following compiler directive {$O UNIT_NAME} will indicate if a unit specified in the USES statement will, in fact, be overlayed. It is therefore possible to use this unit without "overlaying" it simply by using the USES statement and ommiting the {$O UNIT_NAME} directive. This unit would then be treated "normally", as any other unit declared by the USES statement. This represents an alternative. You can overlay the unit or not... depending on the program using it. PNL #10 Page 16 March, 1992 When you use the overlay technique, you have to specify the OVERLAY unit in the USES statement and, initialize overlay management by calling the OVRINIT('filename.OVR'), routine. It is best if this is done right at the beginning of the program. Here is an example: PROGRAM myprogram; {$F+} {forces FAR calls} USES OVERLAY, CRT, {TP 6 units} myunit1, myunit2, myunit3; {your own units} {$O myunit1} {$O myunit2} {Overlay these two units ... } {Note that "myunit3" is overlayed.} {...} {declaration goes here} {...} BEGIN {main program} OVRINIT('MYPROG.OVR'); {...} {other commands here} {...} END. In this case, the compiler creates two files: MYPROG.EXE, the executable file, and MYPROG.OVR, the overlay file. Now, the following example will demonstrate the basics of overlay management. The program will integrate two units, OVER1 and OVER2, and call two routines that they contain: HELLO1 and HELLO2. These two procedures will simply display a message. Here are the two overlays: Unit OVER1; {OVER1.PAS} {$F+,O+} Interface procedure HELLO1; Implementation uses crt; {TP unit} procedure HELLO1; begin writeln('Hello from unit OVER1.'); delay(1000); end; end. {end of unit OVER1} PNL #10 Page 17 March, 1992 Unit OVER2; {OVER2.PAS} {$F+,O+} Interface procedure HELLO2; Implementation uses crt; {TP unit} procedure HELLO2; begin writeln('Hello from unit OVER2.'); delay(1000); end; end. {end of unit OVER2} Now, compile the sources of the two overlays. Once this is done, your disk will contain two source files OVER1.PAS and OVER2.PAS and also their corresponding compiled TPU unit OVER1.TPU and OVER2.TPU. Do not forget to select the option "Compile/Destination" to Disk, otherwise, the two TPU files will not be created. The main program looks like this: program OverlayDemo; {OVRDEMO.PAS} {$F+} uses overlay, crt, {TP units} over1, over2; {overlays} {$O over1} {$O over2} var i: integer; {loop counter} begin {main program} ovrinit('ovrdemo.ovr'); {initialization} for i:= 1 to 10 do begin clrscr; Hello1; {call routine in unit OVER1} Hello2; {call routine in unit OVER2} end; write('Program is finished. Please press [ENTER].'); readln; end. {program} The two units merged together to form the overlay OVRDEMO.OVR when compiling the program. Compiling will also (obviously) create the executable OVRDEMO.EXE. The overlay file must always reside in the same directory as the executable file but it is also possible to merge the overlay file to the .EXE file with the command PNL #10 Page 18 March, 1992 COPY /B ovrdemo.EXE + ovrdemo.ovr which, in fact, appends the overlay at the end of the .EXE file. In this case, the command OvrInit('ovrdemo.EXE') will initialize the overlays integrated in the .EXE file. As you have seen by now, the overlays are not difficult to implement. Just a few modifications around the USES statements and the addition of the compiler directives {$O ...} and {$F+} can lead to a program which is less RAM memory dependent. You may wish to consult your Turbo Pascal manuals for further informations on using EMS memory with overlays, clearing the overlay buffer (increasing dynamically-"allocatable" memory) and modifying its size. I have used overlays largely with the Object Professional Library from Turbo Power because object-oriented programs tend to create larger programs because the compiler cannot easily "strip" unused virtual objects out of the .EXE file. For obviously reasons, another drawback of using overlays is that your program cannot be compressed by a program like PKLITE or LZEXE, which dynamically uncompress your program in memory when executed. The overlay file has to stay in its original uncompressed form on disk because this is how your .EXE file expects to read it. Alex Boisvert DataMAX Communications Enr. FidoNet @ 167/405 PNL #10 Page 19 March, 1992 ================================================================= Turbo Vision without getting GUI. Copyright (c)1991 Richard A. Morris ================================================================= INTRODUCTION Around November of 1990, as most of us know, Borland released Turbo Pascal 6.0, and with their add-in toolbox Turbo Vision unveiled a new Programming Methodology for Turbo Pascal called Event Driven Object Oriented Programming. Well, did the Fewmets hit the Windmill or What !?! Erstwhile conservative commercial programmers ranted that TV was every thing from a Goblin to a toy toolbox destined to be unsupported by Borland, they complained that it consisted merely of cute saccharine windows, of little or no practical use in professional projects. The silence from a lot of Third-Party Code Shops, who had always provided outstanding and timely Developer support packages, was deafening as they waited to see how the programming community would react (Blaise being the only exception). In a classic Catch-22, Programmers have been warily watching Borland to see if they will support Turbo Vision, Meanwhile Borland, and third-party Code shops, quietly had their eyes on the Turbo Pascal community to see if TV was to become popular enough to support, potentially making the bitching of disgruntled users a self-fulfilling prophesy. It's now a year on, slowly but surely Programming journals, and Reference Books are starting to constructively cover the complexities of TV (Most notably Messrs. Rubenking and Duntemann, thank you sirs). OK so why do so many good Pascal programmers have so much dirt to throw at TV, surely they must know what they're talking about? Well TV is a not merely another toolbox full of useful functions, but rather an application framework that impertinently confronts the programmer to completely rethink his/her programming design, from scratch. It's a big ask for a lot of us who have several years of training invested in our trade. Most of us looked at the Demos, and thought 'Well I'm certainly not going to put my name on such a "Mickey-Mouse" program. Wonder if I can make it look a bit more professional?', had a look at the code and found it too difficult to personalize the interface, and so gave up any attempt at using it, and concentrated on our own custom interfaces that we'd invested so much skull sweat upon. Two programmers I have much respect for, convinced me by their comments to take a second look at TV. Is it worth worrying about then, well in a my native vernacular, 'Bloody Oath'! I must add that it took me several months of painful re-education to bend my Object Oriented programing abilities around TV. Turbo Vision is certainly no beginners toolbox as it would appear from Borland press releases and manuals, this may have something to do with the sluggish move of programmers to using it. Firstly any prospective programmer must firmly understand the usage of Pointers and heap memory, Next Object Oriented principles must be almost second nature, Finally the Event Driven paradigm should be well understood. Them's three steep learning curves! The next problem as outlined by Jeff Duntemann in Dr Dobbs Journal Nov, 1991 in his PNL #10 Page 20 March, 1992 "Structured Programming" column (Read it!), is that there no "Front Door" to start off your Research, that you must digest Turbo Vision in a whole gestalt. I plan to do some articles on my experience of coming to grips with Turbo Vision in the next few issues of PNL, however today I'll start with a few nice little goodies that come with TV, which can be used independently of the Application Framework. This allows us to at least deal with some of the TV topics without haveing to see the "Big Picture" at once. Let's discuss TV's Objects Unit, which contains among other goodies Collections, and Stream. You should have a fair grasp of Object oriented programming, and naturally a good concept of dynamic memory and heap usage. INTERESTING OBJECTS Lurking in the set of Turbo Vision Units that comes free with every copy of Turbo Pascal 6.0 is the Unit "Objects.TPU". It's well worth looking into this Unit to see the usefull routines and objects that the Turbo Vision User interface uses as it's glue to bind itself together. I'll leave Collections and Streams for last. Open the file Objects.Int (In your DOC subdirectory of a standard turbo pascal setup), and open your Turbo Vision Guide to page 190. Refer often to the Global reference from page 327 as we talk of Types, procedures, and functions, and the Object Reference from page 205 as we talk of Objects. TYPE CONVERSION TYPES One of the main advantages and disadvantages of Pascal has always been the strong typing. This means that if you have a variable of a certain type you can't usually assign it's contents to a variable of another type. The advantage is that it saves the unwary programmer writing a large variable over a smaller variable and corrupting the memory of all variables following. The disadvantage is that it constrains the programmer from passing data between dissimilar types. Turbo pascal changed all this by allowing you to typecast between any two types of the same size, for example a Pointer and a longint are both 4 bytes long so we can do the following; Var Ptr : Pointer; Long: Longint; begin Long := Longint(Ptr); end; And the Long variable will contain a copy of the bytes of information in the Pointer variable, this may not sound particularly usefull. But consider the WordRec, it allows you to extract the high and low Bytes from a word, Visualise this example for Colours. Var MyColour : Word; PNL #10 Page 21 March, 1992 MyBackground : Byte; Const Black = 0; White = 7; begin WordRec(MyColour).Hi := Black; WordRec(MyColour).Lo := White; MyBackground := WordRec(MyColour).Hi; end; Then we have the Pointer array types which can be type cast over a pointer to access the pointers contents as an array of Bytes, or Words. Remember you can typecast types of the same size, and all pointers are 4 bytes in size regardless of the size of the block they point to. So in the following you can access the ith byte of a dynamic variable. Var BlockPtr : Pointer; Index : Word; begin GetMem(BlockPtr,2000); For Index := 1 to 1000 do pWordArray(BlockPtr)^[Index] := Index; end; This allows you to allocate a block of 1000 words (2 bytes each in size) numbered from 1 to 1000, each initialised with their index number. STRING POINTERS The PString type, the NewStr Function and the DisposeStr procedure are usefull items, they are used throughout TurboVision and they allow you to allocate and dispose of dynamic Strings easilly. You simply pass the newstring function a String variable and it will return a Pointer to a copy of that string stored in Dynamic Memory. You pass that pointer to DisposeStr and it will return the Dynamic Memory used back to available heap memory. Two nice tricks are that NewStr('') returns a nil pointer wasting no Space, and dispose doesn't abort with an error if you dispose a nil pString. Sure you could implement this stuff yourself, and most of us have. However it is all done in Objects, and it links in well with usefull routines like the String Collection. COLLECTIONS Often as programmers we want to be able to store a collection of items, we can use an array, the limitation being that the size must be determined at compile time, and can't change, and can't be greater than 64k. The other option is to create a linked list structure, this is middling difficult to create, but almost impossible to debug if you make mistakes, and accessing an item requires traversal through preceding items, hardly optimised. Turbo vision introduces an Object called (AHA!) a Collection. It is in fact a object encapsulate mutated resizing index array that allows indexed access like an array, with the resizability of a linked list, and because it indexes pointers to your objects you can store any dynamic PNL #10 Page 22 March, 1992 variable or Object. It's use is relatively simple, you simply create a descendant of TCollection and overwrite it's freeItem method, to tell the collection mechanisms how to get rid of an Item's memory, so that for example when you dispose of the whole collection it knows how to dispose of the memory for each item in the collection. Initialise the Collection with two words, and Insert pointers to your items already stored on the heap. The two variables are used internally by the collection when building the index table, the first variable is your best guess as to the starting number of items you will store and the second is the amount you want the table to grow by each time you add more variables than the current index can hold. I generally use 10 and 10, if I don't know the nature of the collection. Have a look at StrList for a simple example of how to store a collection of strings. If you look at my main program loop it initialises a dynamic variable pointing to a my new collection type, it then uses the newStr function to store a string constant in the heap, and uses the collection method insert(p : Pointer) to insert the pointer to that string into the collection. It's a shorhand method that could be represented in longhand as follows; StrVar := 'Four'; StrPtr := NewStr(StrVar); MyStringList^.Insert(StrPtr); Then my main loop calls a new method I created for this object called Print_Sentence, which then calls a method in Tcollection called ForEach(Action : Pointer), this calls a far local (Important can not be global) procedure pointer (PrintWord) for each Item in the collection. Finally I dispose of my special collection using the TCollection.Done destructor, which calls FreeAll, which in turn Deletes the index item and calls freeItem for Each Item. Which is why we overwrote FreeItem, to show our collection how to dispose of an item. I Cheated a bit, there is actually a StringCollection Object provided in Objects, that already knows all about pStrings, and sorts them into the bargain, but I thought as an example it would be good to show a little of the mechanism of creating a specialised collection type. But there is much more to collections, the standard freeItem method is not just abstract, it actually typecasts an object of type TObject over the pointer to your Item, and calls IT's virtual destructor. Why, well all turbo vision objects are in fact descended from a TObject, and if you had inserted a TV object (Or any of your own objects descended ultimately from a TObject), a vanilla TCollection would know all about how to dispose of such an item, and we would not need to descend from TCollection and override TCollection.FreeItem. Unfortunately a pstring is not an object and thus can never be descended from TObject, however a TCollection IS descended from TObject, and thus you could have a Collection of Collections, or (and this is the good bit) a collection of various objects, as long as each was descended from a TObject. I show an example of a Collection of Collections in the included file PNL #10 Page 23 March, 1992 ReadIni.Pas, which is a simple unit to read a Windows type INI File. It reads a Text file (See example Test.INI) and creates a collection of TagCollections, each being a collection of parameters, ie: TAGCollection = Object(tCollection) | +- PARAMCollection = Object(tCollection) TAG : PString | +- PARAMItem = Object(TObject) Param : pString Vars : pString; As the paramItem is a descendant of TObject, no special hadling is required to add it to a PARAMcollection, which has it's own TAG string, then all PARAMCOllections (One for each GROUP TAG) are stored in a TAGCollection. There is sparse documentation in the Source of READINI, To fully explain the code here would bulk up this article, so I'll leave it as a demonstration of what you can do with collections, and it could be a usefull unit as it stands without you modifying anything. STRINGLISTS The String List is another usefull object in the OBJECTS unit. It is a way of collecting a bunch of strings indexed by words. The way it works is this, you create a program to make a StringList using a tStrListMaker Object, which once Constructed with an INIT, simply requires you to use tStrListMaker.Put(Index,TextString) which adds TextString to the Object indexed by INDEX. You then save it to a file (More about this in Streams and Resources). Now you create another program that reads a tStringList from this File, then you simply ask the object for the text string that corresponds to a particular index number. The procedure is simple, I have included the sample files Make_Err, Err_Desc, and Test to describe the procedure. You'll want to read on about streams and resources as they are used in these files. However I'll describe a few uses for String Lists. In the example I have given Make_Err simply puts a Text String detailing each Runtime error, and stores the description indexed with the run time error code. Desc_Err simply finds out the exitcode and if non-Zero it prints out a description. So what you say, well let's say for example that you have to create a version of your program for a swedish audience. You simply create a new ERRORS.STM file to put with your program, and you don't need to change and recompile your code. The REAL advantage is that now that the Europeans are finally getting their act together with a common economic bloc, there will be a lot of demand for programs that are multi lingual. If you use a stringList for ALL text strings in your code, you'll be able to have users select upon startup the language they wish to converse with the program in, and simply activate THAT string List. As a favour to me could those of you who are fluent in Non-English languages, send in to the Newsletter a modified Make_Err for your language, we'll compile them all together and distribute a multi lingual Runtime error descriptor. PNL #10 Page 24 March, 1992 STREAMS AND RESOURCES About the most sophisticated file handling in Turbo pascal up until TP5.0, was the TEXT file type. With the TP5.0 demo files Borland introduced a sophisticated Object called a Stream that allows you to easilly perform polymorphic Object File Input/Output. Turbo power extended this with their own stream objects in Object Professional, and finally we have it integrated in Turbo Vision. There is a great discussion on Streams in Chapter 8 of the TV Guide, I shan't try to repeat this here, however I'll give a general run down on the use of Streams, and their application in the real world. I do plan to do a more detailed tretise on Streams in the future (given time) based on a unit I use called NetStrm, which extends the buffered stream to work on Networks. How does it work, well most turbo vision objects (And any Objects you want to use with Streams) have two extra methods, a constructor usually called Load, and a procedure called Store, which each take as a parameter a stream variable, and do pretty much what they say they do. The trickiness is that when you prepare to use objects in streams, you register the Objects you will use. Registering an Object entails sending information to the Objects unit information so that it knows where to find your objects Load and Store methods, a unique word called an ObjType, and the VMT link (Which within OOPS Pascal uniquely defines an Object Type) which allows the Objects unit to know the size of the dynamic object it must create during a load construction. When an object is stored to a file, it's ObjType number is first saved to the Stream, then the Store method is called to put it's information onto the file. Ok So what is the VMTLink for, well when you tell a stream to load an object, it reads the OBJType number, then searches it's list of registered objects and uses the VMTlink that it finds to do all the OOP trickiness to construct the Object, then uses the pointer to the Load constructor registered with the object, to fill the object with information from the file. So What? Well what this means is that your stream is asked to get an object, it will return to you a pointer to an object, after constructing it for you, WITHOUT KNOWING WHAT THE OBJECT IS. Ha, So what again. Well it means that you can store any registered descendant of TObject with a store and complimentary Load mechanism, and load it back again without having to know at compile time the information you are sending to the screen. Resources are simply streams with an index comprising of key strings, in the case of Make-Err and Desc_err I store the stringlist to a resource under the keystring "ERRORDESC". As an example of the use of Streams, Say you have 5 or 6 different record types that have to be input and output to a file. In the 'Olden days' you would use the case command to create a variant record, and each record you stored would be the size of the LARGEST record case. This way you simply declare a different Object for each record type, and you use only as much space as each record needs. Furthermore if you have put a method called EditRec into an abstract Object, and descended all your PNL #10 Page 25 March, 1992 record objects from it, and in each case overriden Editrec to do the individual record editing and manipulation you could simply do the following; Incognito := TStream.Get; Incognito^.EditRec; It's a lot easier if your object is a Turbo Vision View with record information, and editing dialogs, but then you'd have to use the Turbo Vision User Interface. Ah well, we'll all be using it sooner or later. Refference sources Turbo Vision Guide Borland Intl "Structured Programming" Dr. DOBBS journal November 1991 About the author Richard Morris is the CEO of KHIRON Software, a Queensland, Australia based Computer consultancy, that has been providing contract programmers, and computer support to small to medium business' sin 1985. Richard can be contacted via the following Fidonet: 3:640/372.5 IntlNet: 58:1100/378 Voice: (07) 812-3218 Post: C/- KHIRON Software P.O. Box 544, INDOOROOPILLY Qld 4068. PNL #10 Page 26 March, 1992 ================================================================= FREQUENTLY ASKED QUESTIONS IN THE PASCAL ECHO ================================================================= Q1. How do I pass an error level code when my program finishes? A1. The halt procedure takes an optional parameter of type word. Thus - halt(1); terminates the program with an errorlevel of 1. If halt is used without a parameter it is the same as - halt(0); Note: When a program is terminated using the halt procedure any exit procedure that has previously been set up is executed. Q2. How do I empty the keyboard buffer? A2. There are several ways that this can be achieved. However the safest is - while Keypressed do ch := ReadKey; This requires that a variable ch of type char is declared and the crt unit be used. To do it without using a variable - while Keypressed do while ReadKey = #0 do; or if using TP6 with extended syntax enabled - while KeyPressed do ReadKey; If you do not wish to incur the substantial overhead involved with the use of the CRT unit and there is no requirement for the program to run under a multi-tasker - var head : byte absolute $40:$1c; tail : byte absolute $40:$1e; tail := head; Q3. When I redirect the screen output of my programs to a file the file is empty and the output still appears on the screen. What am I doing wrong? A3. You are probably using the CRT unit and its default method of writing to stdout is by direct screen writes. In order to enable output to be redirected all writes must be done by DOS. Setting the variable DirectVideo to false has no effect on redirection as all it does is use the BIOS for screen writes - not DOS. PNL #10 Page 27 March, 1992 To enable redirection you must not use the CRT unit OR assign(output,''); rewrite(output); This will make all output go through DOS and thus can be redirected if desired. To restore the default situation - AssignCRT(output); rewrite(output); Q4. How do I make a string that is lower or mixed case all uppercase? A4. There are several ways to convert lower case characters to upper case. Here are some of them. As a procedure (excluding asm code this is the fastest way) procedure StrUpper(var st: string); var x : byte; begin for x := 1 to length(st) do st[x] := UpCase(st[x]); end; As a function (slower but sometimes more convenient) - function StrUpper(st: string): string; var x : byte; begin StrUpper[0] := st[0]; for x := 1 to length(st) do StrUpper[x] := UpCase(st[x]); end; Both the above are suitable for the English language . However from version 4.0 onwards, DOS has had the facility to do this in a way that is country (language) specific. I am indebted to Norbert Igl for the basic routine. I have modified his code slightly. For the anti-goto purists this is a good example of a goto that is convenient, efficient, self-documenting and structured. The dos calls would make this method the slowest of all. function StrUpper(s: string): string; { Country specific string-to-uppercase conversion. } { Requires DOS unit } label fail; var regs : registers; x : byte; begin PNL #10 Page 28 March, 1992 if lo(DosVersion) >= 4 then begin with regs do begin ax := $6521; ds := seg(s); dx := ofs(s[1]); cx := length(s); msdos(regs); if odd(flags) then { the attempted conversion failed so } goto fail; end; { with } end { if DOS >= 4.0 } else fail: for x := 1 to length(s) do s[x] := UpCase(s[x]); StrUpper := s; end; { StrUpper } Q5. When I include ANSI codes in a string and write that string to the screen the actual codes appear on the screen, rather than the results they are supposed to achieve. A5. In order for ANSI codes to be interpreted, screen writes must be directed through DOS and there must have been a suitable driver loaded via the config.sys file at boot time. All output can be directed through DOS and the driver by - Not using the crt unit OR - assign(output,''); rewrite(output); in which case ALL screen writes are "ANSI code sensitive" OR - You can set up write procedures that will be "ANSI code sensitive". (You will need an initialisation procedure to set this up.) var ansi : text; procedure AssignANSI(var ansifile : text); begin assign(ansifile,'CON'); rewrite(ansifile); end; { AssignANSI } procedure WriteANSI(var st: string); begin write(ansi,st) end; { WriteANSI } PNL #10 Page 29 March, 1992 procedure WriteLnANSI(var st: string); begin writeANSI(st); writeln(ansi); end; { WriteANSI } ObviousLy, if the ANSI.SYS driver (or an equivalent) is not installed none of the above can work. Setting the variable DirectVideo in the CRT unit to false will not achieve the desired result as this merely turns off direct screen writes and uses the BIOS for all screen output. Q6. When I try to shell to DOS nothing happens. What am I doing wrong? A6. In order to be able to execute any child process there must be sufficient memory available for it to load and execute. Unless you advise differently at compile time, a Turbo Pascal program grabs all available memory for itself when it is loaded. To reserve memory for a child process use the compiler memory directive - {$M 16384,0,0) the default is - {$M 16384,0,655360} The first figure - StackMin - is the amount of memory to be allocated for the stack: Minimum is: 1024 Default is: 16384 Maximum is: 65520 The next figure - HeapMin -is the minumum amount of memory to be allocated for the heap. If there is less memory available than this figure the program will not load. Minimum is: 0 Default is: 0 Maximum is: 655360 In practice it will be the amount of free memory less the space required for the stack, less the code space of the program. You should set this to 0 unless your program uses the heap. In that case, set it to the lowest possible figure to prevent heap allocation errors. In most cases it is best to leave it at zero and do error checking within the program for sufficient memory at allocation time. The last figure is the crucial on as regards child processes. It should always be low enough to leave memory left over for a child process and high enough not to cause problems for the program when allocating heap memory. PNL #10 Page 30 March, 1992 Minimum is: HeapMin Default is: 655360 Maximum is: 655360 If less than the requested amount is available no error is reorted. Instead all available memory is allocated for heap use. Q7. How do I shell to DOS? A7. SwapVectors; exec(GetEnv('COMSPEC',''); SwapVectors; Read previous section on memory allocation. I find that it is a good idea to write my own Exec function which will do everything that is needed for me. I have it return an integer value that is the DosError code. function Exec(p1,p2: string); begin SwapVectors; Dos.Exec(p1,p2); SwapVectors; Exec := DosError; end; This enables me to have a statement such as - ReportError(Exec(GetEnv('COMPSEC'),'')); Now you can have an empty ReportError procedure or you can make it report the error - whatever is suitable for you application. Q8. When I execute a child process redirection does not work. Why? A8. Redirection of a child process's output only works if it is run under another copy of the command processor. So - exec('YourProg.exe',' > nul'); will not work but exec(GetEnv('COMSPEC'),'/c YourProg > nul'); will work. Q9. How do I read an errorlevel from a child process? A9. After executing a child process the errorlevel returned can be read by calling the DosExitCode function which returns a word. The low byte is the errorlevel. A full description is in the manual. If the command interpreter is the child process and it in turn executes a child process then the errorlevel of the second child process cannot be read without resorting to some trickery. PNL #10 Page 31 March, 1992 Q10. When I read a text file that has lines exceeding 255 characters I lose all those characters from the 256th one on each time there is a line that exceeds that length. How can I prevent this? A10. Turbo Pascal's readln procedure reads a line up to the 255th character then skips to the next line. To get around this you should declare a buffer at least as large as the longest possible line and then use the read procedure. The best size for the buffer is a multiple of 2048 bytes. const BufferSize = 2048; LineLength = 78; type textbuffer = array[1..BufferSize] of char; var st : string; f : text; buffer : textbuffer; function ReadTxtLn(var tf: text; var s: string; max: byte): integer; { Reads a string of a maximum length from a text file } var len : byte absolute s; begin len := 0; {$I-} while (len < max) and not eoln(tf) do begin inc(len); read(tf); end; if eoln(tf) then readln(tf); ReadTxtLn := IOResult; {$I+} end; { ReadTxtLn } begin assign(f,filename); reset(f); SetTextBuf(f,buffer); while not eof(f) and (ReadTxtLn(f,st,LineLength) = 0) do writeln(st); close(f); end. Q11. How do I convert nul terminated asciiz strings to Turbo Pascal strings? A11. Here is a function that will do that - function Asc2Str(var s; max: byte): string; { Converts an ASCIIZ string to a Turbo Pascal string } { with a maximum length of max. } var starray : array[1..255] of char absolute s; len : integer; PNL #10 Page 32 March, 1992 begin len := pos(#0,starray)-1; { Get the length } if (len > max) or (len < 0) then { length exceeds maximum } len := max; { so set to maximum } Asc2Str := starray; Asc2Str[0] := chr(len); { Set length } end; { Asc2Str } Q12. How can I tell if a particular bit of a variable is set or not? How can I set it? How can I turn it off? How can I make a large bit map and then determine if a particular bit - say bit 10000 is on/of? A12. This question, or a variation of it, is one of the most commonly asked questions in the echo and there are several ways of doing what is wanted. None are necessarily right or wrong. The way I will describe is designed to take up as little code/data space as possible. I do not attempt to explain the theory behind these functions as this can be obtained from any good book. The use of sets can be the best bit manipulation method if you have control over the data being used. Here is an example of a byte variable for a BBS program which sets various user access level flags. Bit 0 = Registered User 1 = Twit 2 = Normal 3 = Extra 4 = Privileged 5 = Visiting Sysop 6 = Assistant Sysop 7 = Sysop type status_type = (Registered, Twit, Normal, Extra, Privileged, VisitingSysop, AssistantSysop, Sysop); status_level = set of status_type; var access_flags : status_level; Let us assume you have someone who logs on and you wish to determine his user access level. After reading access_flags from the user data file - if Sysop in access_flags then .... To set the sysop flag - access_flags := access_flags + [Sysop]; PNL #10 Page 33 March, 1992 To reset the sysop flag - access_flags := access_flags - [Sysop]; However on many occasions using a set may not be a suitable method. You may simply need to know if bit 5 is set or not. Here is the method that I consider the best - function BitIsSet(var V, bit: byte): boolean; begin BitIsSet := odd(V shr bit); end; To set a bit - procedure SetBit(var V: byte; bit: byte); begin V := V or (1 shl bit); end; To reset a bit - procedure ResetBit(var V: byte; bit: byte); begin V := V and not(1 shl bit); end; To toggle (flip) a bit - procedure ToggleBit(var V: byte; bit: byte); begin V := V xor (1 shl bit); end; Now a bit map can be made up from an array of bytes. If stored on the heap you can test any bit up to number 524159 (zero based). Here's how. type map = array[0..maxsize] of byte; { set maxsize to number of bits div 8 -1 needed in the bit map } function BitSetInBitMap(var x; numb : longint): boolean; { Tests the numb bit in the bitmap array } var m: map absolute x; begin BitSetInBitMap := odd(m[numb shr 3] shr (numb and 7)); end; procedure SetBitInBitMap(var x; numb: word); { Sets the numb bit in the bitmap array } var m: map absolute x; begin m[numb shr 3] := m[numb shr 3] or (1 shl (numb and 7)) end; procedure ResetBitInBitMap(var x; numb : longint); { Resets the numb bit in the bitmap array } PNL #10 Page 34 March, 1992 var m: map absolute x; begin m[numb shr 3] := m[numb shr 3] and not(1 shl (numb and 7)); end; procedure ToggleBitInBitMap(var x; numb : longint); { Toggles (flips) the numb bit in the bitmap array } var m: map absolute x; begin m[numb shr 3] := m[numb shr 3] xor (1 shl (numb and 7)); end; Q13. How can I find a particular string in any file - text or binary? A13. The Boyer-Moore string search algorithm is considered to be the fastest method available. However in a rare worst-case scenario it can be slightly slower than a linear brute-force method. The following demonstration program will show how it works and could easily be modified to allow for command line paramters etc. program BMSearchDemo; type bigarray = array[0..32767] of byte; baptr = ^bigarray; BMTable = array[0..255] of byte; const KeyStr : string = 'Put whatever you want found here'; fname : string = 'f:\Filename.txt'; var Btable : BMtable; buffer : baptr; f : file; result, position : word; offset : longint; finished, Strfound : boolean; procedure MakeBMTable(var t : BMtable; var s); { Makes a Boyer-Moore search table. s = the search string} { t = the table } var st : BMtable absolute s; slen: byte absolute s; x : byte; begin FillChar(t,sizeof(t),slen); for x := slen downto 1 do if (t[st[x]] = slen) then t[st[x]] := slen - x end; function BMSearch(var buff,st; size : word): word; PNL #10 Page 35 March, 1992 { Not quite a standard Boyer-Moore algorithm search routine } { To use: pass buff as a dereferenced pointer to the buffer} { st is the string being searched for } { size is the size of the buffer } { If st is not found, returns $ffff } var buffer : bigarray absolute buff; s : array[0..255] of byte absolute st; len : byte absolute st; s1 : string absolute st; s2 : string; count, x : word; found : boolean; begin s2[0] := chr(len); { sets the length to that of the search string } found := false; count := pred(len); while (not found) and (count < (size - len)) do begin if (buffer[count] = s[len]) then { there is a partial match } begin if buffer[count-pred(len)] = s[1] then { less partial! } begin move(buffer[count-pred(len)],s2[1],len); found := s1 = s2; { if = it is a complete match } BMSearch := count - pred(len); { will stick unless not found } end; inc(count); { bump by one char - match is irrelevant } end else inc(count,Btable[buffer[count]]); { no match so increment maximum } end; if not found then BMSearch := $ffff; end; { BMSearch } begin new(buffer); assign(f,fname); reset(f,1); offset := 0; MakeBMTable(Btable,KeyStr); repeat BlockRead(f,buffer^,sizeof(buffer^),result); position := BMSearch(buffer^,KeyStr,result); finished := (result < sizeof(buffer^)) or (position <> $ffff); if position = $ffff then inc(offset,result); Strfound := position <> $ffff; until finished; close(f); if Strfound then writeln('Found at offset ',offset) PNL #10 Page 36 March, 1992 else writeln('Not found'); end. Q14. How can I put a apostrophe in a string? A14. Just put in extra apostrophes. If you want st to be equal to the string - The word 'quoted' is in quotes do this - st := 'The word ''quoted'' is in quotes'; if you want the following to be written to screen - 'This is a quoted string' do this - writeln('''This is a quoted string'''); Q15. What are the best books to purchase to help me learn Turbo Pascal? A15. There are many good books for learners. Here are a few - Complete Turbo Pascal - Third Edition - Jeff Duntemann Mastering Turbo Pascal 6 - Tom Swann Turbo Pascal - The Complete Reference - O'Brien. For advanced users there are also many good books. Here are a few that I have found useful - (Those marked with an asterisk are not purely for Turbo Pascal) Turbo Pascal 6 - Techniques and Utilities - Rubenking Turbo Pascal Internals - Tischer * PC System Programming for Developers - Tischer * Undocumented DOS - Schulman Any learner would be well advised to obtain a well known library such as Technojock's Turbo Toolkit (TTT) which is shareware and study the source code. Trevor Carlsen (TeeCee) PNL #10 Page 37 March, 1992 ================================================================= Running 80286 Turbo Pascal Programmes on 8088/8086 Computers ================================================================= [ Editor's note: Intentionally, the author uses English spelling instead of American spelling ] Introduction When Borland released Turbo Pascal Version 6.0 in 1990, they added many impressive features to the compiler which reaffirmed Turbo Pascal as the de facto standard Pascal compiler for PCs. One such feature is the ability to generate iAPX286 instructions for added speed on AT or higher machines; unfortunately, they forgot (neglected?) to add a check to the RTL (runtime library) for whether or not an iAPX286 was actually present. Any programme compiled in the {$G+} state will undoubtedly "hang" when executed on an 8088 or 8086-based computer. In what seems an afterthought, Borland added a small demonstration programme called "TEST286" in the \DEMOS subdirectory of your Turbo Pascal subtree, and wrote a small mention in the "HELPME!.DOC" file. Following Borland's own advice, however: "If you want to put code like this in a program with {$G+} enabled, put the test and halt code in the initialization section of the first unit in the main program's USES clause." What follows is a description of a unit (provided) which does exactly that. Place the unit's label immediately after the "uses" keyword before any other unit labels, and forget about it; at runtime when the initialisation code is executed and no 80286, 80386 or 80486 is detected, a message is displayed informing the user of the reason why the programme immediately thereafter exits (returning an errorlevel of one). Directives Overview This section merely explains the logic behind each of the compiler directives used for the benefit of novices, and can be ignored by more competant programmers. As presented, the unit is compiled in the {$G-} state to ensure proper execution on computers based on the 8088 or 8086 processor. Further, it is compiled with far calls and overlay ability enabled (viz., {$F+} and {$O+}) so upon completion in large applications, the code can be swapped out (although this shouldn't be too necessary as the entire unit is less than one kilobyte). The routine exhibited no problems on the test machines (an 80486, an 80386 AST and an NEC V20-based XT), so has all debugging facilities disabled ({$D-,I-,L-,R-,S-}). This should not prove a problem in any programmes in which you have debugging enabled, unless you wish to trace through the code. In addition to not including debugging information, the "usual" code optimisation switches are enabled ({$B-,V-}). Since word-aligned data has no effect on the 8088, yet increases execution speed on all 80x86 PNL #10 Page 38 March, 1992 processors, it is enabled ({$A+}). There is no use of the "extended syntax" option (again, a Version 6.0 enhancement), so it is disabled with {$X-}. The addition of {$E-} and {$N-} is merely to "complete" the switch directives, and indicate that there is no need for any additional numeric or emulated floating point processing. Unit Description As there are no functions or procedures to call (variables to read and alter, constants to use, et al.), the "interface" section of the unit remains void (empty). Both the errorlevel returned, and the message displayed are defined as constants so that they can be easily altered by an external setup or configuration utility. The errorlevel can be any value from zero (0) to two hundred and fifty-five (255), and the error message can be upto eighty (80) characters in length. For users of Object Professional's OpClone unit, the string constant "i286 config data 1.00" is provided. The one internal variable declared, "Is286Able", is used to pass the result of the detection routine to the remainder of the code; this was necessary as no separate function is used (to keep execution speed to the bare minimum), and standard functions and procedures (such as WriteLn() and Halt()) are not callable from within the built-in inline assembler (one short-coming of BASM). The core of the original function provided in TEST286 is: asm pushf { Push flags register onto the stack } pop bx { Pop a word from the stack, store in BX } and bx, 00FFFh { Logical AND operands, result in BX } push bx { Push word contents of BX on the stack } popf { Pop from stack into flags register } pushf { Push flags register onto the stack } pop bx { Pop a word from the stack, store in BX } and bx, 0F000h { Logical AND operands, result in BX } cmp bx, 0F000h { Compare operands, update status flags } mov ax, 00h { Store word 0x0000 (zero) in AX } jz @@1 { If the zero flag is set, jump to "@@1" } mov ax, 01h { Store word 0x0001 (one) in AX } @@1: { Internal label "@@1", end of routine } end; The essential logic here is that for processors earlier than the 80286 (viz., 8088, 8086, 80188, 80186, V20 and V30) bits 12 to 15 of the CPU flags register cannot be cleared. This routine merely tries to clear those bits (without disturbing the others), and if it is unable to do so, assumes an iAPX286 or higher (i386, i486) is not present. For the supplied unit, the core is: asm xor ah, ah { Logical XOR operands, result in AH } pushf { Push flags register onto the stack } PNL #10 Page 39 March, 1992 pop bx { Pop a word from the stack, store in BX } and bx, 00FFFh { Logical AND operands, result in BX } push bx { Push word contents of BX on the stack } popf { Pop from stack into flags register } pushf { Push flags register onto the stack } pop bx { Pop a word from the stack, store in BX } and bx, 0F000h { Logical AND operands, result in BX } cmp bx, 0F000h { Compare operands, update status flags } je @@1 { If the zero flag is set, jump to "@@1" } inc ah { Increment the contents of AH by one } @@1: { Internal label "@@1", store result of } mov [Is286Able], ah { detection in "Is286Able" variable } end; Logically, the two code segments are identical (ignoring the last "mov [Is286Able], ah" instruction above). If we compare their logic structure in English: (Original Code) 1. Clear flag bits 12-15. 2. Compare - did they clear? 3. Set the result to zero (false). 4. If the comparison in #3 resulted in the zero flag being set, ie., the bits did NOT clear, jump to the end of the routine. 5. The bits DID clear, so set the result to one (true). (New Code) 1. Set the result to zero (false). 2. Clear flag bits 12-15. 3. Compare - did they clear? 4. If the comparison in #3 resulted in the zero flag being set, ie., the bits did NOT clear, jump to the end of the routine. 5. The bits DID clear, so increment the result to one (true). Up to this point, the comparison has been discussed as resulting in the zero flag being set or cleared; to understand why, you must remember that comparisons are conducted by subtraction, so that if two items equal each other numerically, their difference is zero (hence the the zero flag is set). To indicate more accurately the logic of the routine, the "jz" (jump if zero set) instruction was replaced with the "je" (jump if equal - ie., zero set) instruction. Both are functionally identical. Saving Clock Cycles The differences remain now with two instructions: "xor ah, ah" and "inc ah". Since only eight bits are needed for the result, AH is used rather than AX in its entirety. The zeroing of the register with XOR instead of MOV ("xor ah, ah" instead of "mov ah, 0") saves one clock cycle on 8086 processors (none on 80286, 80386 or 80486 processors) and works since performing a logical exclusive OR (as opposed to a logical inclusive OR) on a number with itself always results in zero. Moving the placement of the "set-result-to-false" instruction to the start not only makes more sense, but is necessary as the XOR instruction modifies the zero flag (which then could not be used in the ensuing jump instruction). PNL #10 Page 40 March, 1992 Rather than loading AH with one ("mov ah, 1" indicating "true"), the increment instruction is used ("inc ah") as, with XOR, it saves an extra valuable clock cycle on 8086 processors. Admittedly, if the host CPU is an 8086 the increment instruction is never reached, but it does not hurt to optimise at the instruction level. Remaining Code The last five lines of the routine proper are in Pascal (as explained above, BASM doesn't allow calling standard procedures and functions). These merely check the status of the "Is286Able" variable, and if the assembly code set it to "false", print a message before exiting. Test Programme A driver programme to test the unit is simply: program Testi286; {$G+} uses i286; {-If a '286 is absent, the init code of the unit will exit} begin WriteLn('Obviously an 80286 or higher is in this machine.') end. { Testi286 } Conclusion While many will claim the saving of two (well, one) clock cycles is no more than academic, this was not the main aim of the unit. The "i286" unit provides a very easy to use, "plug 'n' play" method of detecting and exiting which requires no further effort on the part of the programmer. David J. N. Begley 58:2100/142@intlnet, 3:712/211.3@fidonet Department of Computing, Faculty of Science and Technology University of Western Sydney, Nepean PNL #10 Page 41 March, 1992 ================================================================= Conclusion ================================================================= That's it for now. In the next issue, I will publish the first part of what I call the Beginner's Toolbox. It is a set of units to help beginners to program easily. It will include input/output routines, a help system, a menuing system, etc... I thank everybody who participated in the rebirth of PNL. Richard Morris, the editor over-the-pond has collected most of the articles here. Without him, there would have been only 2 articles here by March 1st, 1992. Again, I beg you all to send in articles. Everybody has his/her own techniques to program, comments to share with others. You can send in book reviews or software reviews, The newsletter depends on your contribution... Anybody interested in publishing an article can request the Article Specifications with the magic name ARTSPEC on FidoNet 1:167/405 - It will also be available soon from Richard Morris, FidoNet 3:640/372.5. If you would like to receive back issues of PNL directly from me, send a diskette and $2.00 for shipping. Don't forget to include your address. Send your order to: Alex Boisvert 86 Bryant St. Sherbrooke, Quebec Canada J1J 3E4 If you are a SysOp that will regularly carry PNL and would like to have your bulletin board listed as such, here, send me a message either by postal mail or at one of the electronic addresses given on the title page, with your bulletin board's name, phone number, and your name. Distribution List The following is the phone numbers to bulletin boards known to carry PNL. If you would like your bulletin board's name and number added to or deleted from this list, please send me a message at one of my many addresses. I can not guarantee whether a listed board will have any particular issue, however. Thieve's World ......................... Phone: (713) 463-8053 Hippocampus ............................ Phone: (203) 484-4621 Turbo City BBS ......................... Phone: (209) 599-7435 The Final Frontier BBS.................. Phone: (518) 761-0869