ASSEMBLY LINE: File Handling, Part II
by Charles F. Johnson
Charles F.Johnson, by using some as yet undiscovered laws of nature, has managed to find the time to be both a professional musician and a professional programmer. In his musical career, he has played with such artists as Chicago, George Duke, Al jarreau and Stanley Clarke. His programming accomplishments include Mouse-Ka-Mania, Desk Manager, ARC Shell and, along with his partner John Eids-voog, G + Plus and MultiDesk. He and John are the owners of CodeHead Software.
With this edition of Assembly Line, I'll be taking over the writing chores from Douglas Weir (who's done a terrific job up to now). Douglas's last column dealt with opening disk files and reading data from them. This issue's column will take up more or less where he left off and explain how to access GEM's File Selector through assembly language.
This month's example program actually does something useful: It copies a file from any directory to any directory. To do this, we need to open files, create new files, read and write data and close everything properly. To make things a little more exciting, the example program will use the GEM File Selector to choose the source and destination filenames.
Danger, Will Robinson!
I would be remiss in my duties as a columnist if I didn't take a few moments here for the obligatory warnings. Be very careful when experimenting with untested code that writes to disk. As Douglas mentioned in his last column, you should do your testing on a "work" disk (one that you don't care about), and definitely not on a hard disk or a floppy that contains any data you don't want to lose. It's very tempting to just type in that code and run it. I know, but trust me, the very first time you let your guard down, Murphy's Law will jump out of the bushes and grab you! It's only a few seconds' work to pop out the disk with your source code and pop in your work disk—a few seconds that can save you hours of future grief.
The Incredible Shrinking Program
When GEMDOS loads a program into memory and starts it, the program is allocated of all the available memory in the computer. In other words, when our program runs, it doesn't matter where we put our end statement. As far as GEMDOS is concerned, we own all of the memory from the start of our program to the last byte of free RAM, and no one else can use it. Which means that if a desk accessory that's running concurrently with our program needs to grab some temporary memory to perform a function, it will not be able to do it.
Also, some operating system calls (particularly some of the AES/VDI calls) need to use some temporary memory and won't work if they can't get it. Therefore, the first thing any program should do when it runs is tell GEMDOS to take back all the rest of the available memory and reserve only the actual memory the program needs to run. The GEMDOS call that does this is called Mshrink, because it "shrinks" the memory used by the program and frees up the rest for use by any other process that needs it.
The Mshrink call passes two parameters on the stack, both longwords: the starting address of the area to reserve and the number of bytes to reserve. The starting address will usually be the base page of the program. You may remember from the last Assembly Line that when GEMDOS executes a program, the address of the program's base page can be found at 4(sp). The first thing our example program does is get this address from the stack and put it into register d1. Since the program's .text, data and .bss segments immediately follow the base page, we can find the total number of bytes in our program by subtracting the address of the base page from the address of the end of the program (which is given the label prgend). These values are then passed to the Mshrink call.
The second line of code in our program is responsible for setting up the stack frame that our program will use. (Earlier Assembly Line columns have explained the usage and maintenance of the 68000's stack pointer.) When a program is run, GEMDOS sets aside a small stack area for the program's use; but this "default" stack is of very limited size and may not be adequate for many purposes. Therefore it's usually better to define an area in your .bss segment (the uninitialized data segment) that will be used for your program's stack. The new stack frame is set simply by loading its effective address into register a7 (which most assemblers will also let you call sp).
Who's afraid off the big bad File Selector?
As mentioned above, this month's example program uses the GEM File Selector to select two filenames, the source and destination files for a copy operation. The File Selector is a handy "canned" routine in the ST. It lets you quickly find files in any drive or directory and select one of them for use by a program without ever touching the keyboard. It's a godsend for those who hate to type long pathnames when they need to access a file (which includes me). And fortunately for the assembly programmer, fsel__input is also the easiest-to-use function in the entire AES library. (I like to think of the File Selector as the programmer's best friend.) I'm going to save in-depth discussions of AES and VDI for future columns; to use the File Selector, knowing some of the bare essentials will do for now.
It doesn't take a lot of work to set up the necessary data structures for fsel__input. Near the end of our example program, in its .bss segment, you will find six arrays labeled contrl, global, intin, intout, addrin and addrout. These arrays are used by the system when you call any AES functions. Just before the .bss segment of the example program, you will also find the label aespb, that contains a table of pointers to the six AES arrays. This table of pointers is called the "AES parameter block" and is used to tell the system where to find your data arrays.
Each AES function has a specific number; to call a function, you place its number (or "opcode") in the first word of the contrl array. Depending on which AES function you're using, other parameters in these arrays may also need to be set before the call. After initializing the arrays, you actually execute the AES call by loading the address of the AES parameter block (aespb) into register d1 and the number $C8 into register d0, and performing a trap #2 instruction. ($C8 is a "magic number" that tells the system that this is an AES call; VDI calls also go through trap #2, and the system differentiates between the two types of calls by checking the magic number in d0. The VDI's magic number is $73.)
The fsel__input call requires two parameters (both longwords) to be passed to it in the addrin array. These parameters are the addresses of the two text strings that will be displayed on the File Selector's "Directory:" and "Selection:" lines. The selection line should be a minimum of 13 bytes long, just long enough to hold a zero-terminated filename without any path. It doesn't need to be initialized. In our example program, the selection line is labeled copy__file.
The directory line should be long enough to contain nested subdirectories; 80 bytes is a reasonable amount to set aside. In our example program, the directory line is labeled copy__dir. Unlike the selection line, however, the directory line needs to be set up before passing it to fsel__input. After the Mshrink call, our program initializes the directory line.
Final approach to the File Selector
First, we use GEMDOS function $19 (Dgetdru) to find out the current drive. This function returns a number from 0 to 15 in d0, signifying drives A through P. We convert this value to an ASCII letter by adding the ASCII value of the letter "A" (65) to it. Then we use the lea instruction to load the effective address of our directory line (which is an 80-byte area in our .bss segment labeled copy_dir) into register a6 and move the ASCII drive byte from d0 to the directory line, using indirect addressing with post increment. Then we move in the colon which should always follow the drive letter, again with post increment addressing. This leaves us with a6 pointing to the first byte past the colon.
Now, we use GEMDOS function $47 (Dgetpath) to get the full pathname of the current directory. This function passes two parameters. The first parameter is a word signifying the drive whose path is being determined. To get the path for the current drive, this value should be 0; otherwise it should be a value from 1 to 16. (Notice that this value is one greater than the corresponding values returned from Dgetdru.) The second parameter for Dgetpath is a longword address where it should store the returned pathname string. Since a6 still contains the address immediately following the colon in copy__dir, we simply push a6 on the stack and the system will put the current path into our directory line for us, terminated nicely with a 0.
We're still not finished setting up the directory line—now we have to add the rest of the search specification to the end of the string. Since we have no way of knowing how many characters are in the current path, we have to search forward from the beginning of our directory line until we find the 0 at the end. This is done with a short, simple loop that tests each byte in the string (again, using indirect addressing with post increment) and repeats until the zero flag is set. When we find the end, we once again use post-increment indirect addressing to copy the rest of the search spec ("*.*") on to the end of the string, with another simple loop which repeats until the zero flag is set.
Behold the dreaded File Selector!
Once this directory line initialization stuff is out of the way, our path (pardon the pun) is clear to call the File Selector and let the user enter the source filename for our copy operation. To do this, we've created a subroutine called (imaginatively enough) fsel__input. This subroutine takes two longword parameters: the address of the directory line (copy__dir) and the address of the selection line (copy__file). The parameters are passed in registers a4 and a5, respectively.
The first thing the fsel__input subroutine does is set up the AES contrl array. All AES calls require five words to be placed in the contrl array, as follows:
contrl = AES function number. (Opcode)
contrl + 2 = Number of words passed in intin.
contrl + 4 = Number of words returned in intout.
contrl + 6 = Number of longwords passed in addrin.
contrl + 8 = Number of longwords returned in addrout.
For the fsel__input call, these five words are:
contrl = 90
contrl + 2 = 0
contrl + 4 = 2
contrl + 6 = 2
contrl + 8 = 0
The fsel__input subroutine first places these values into the contrl array, and then moves a4 and a5 into addrin and addrin + 4. (Why " + 4"? These are long-words, remember?) When the arrays are set up, we call the subroutine labeled aes. This routine moves the address of the AES parameter block (described above) into register d1, the AES magic word $C8 into dl, and executes a trap #2 instruction. At this point, if everything has been done right, the File Selector will appear in all its debatable glory, showing the files in the current directory of the current drive that match the search specification in our directory line. The trap #2 call will not return until the user of our program selects a file, or clicks on the File Selector's cancel button.
Please note that while the File Selector is active, it actually changes the data in our directory and selection lines! When we return from the fsel__input call, the file and path selected by the user will be right there, waiting for us to make a filename out of it. But first, we have to find out whether the user really selected a file, or whether she/he clicked on the cancel button.
The names of the AES data arrays reflect their functions; intin contains the integer input parameters, intout contains the integer output parameters, addrin contains the address input (longword) parameters and addrout contains the address output parameters. According to our table above, fsel__input returns two parameters in the intout array. The first element of intout will contain an error status code and will be 0 if an error occurred and greater than 0 if no error occurred. The array element intout + 2 will hold a value that indicates which button the user selected. If intout + 2 contains a 0, the cancel button was selected; if it contains a 1, the OK button was selected or the file chosen was double-clicked.
Our fsel__input subroutine, after returning from the aes routine, checks the contents of intout + 2. If any value other than 1 is present, the routine sets d0 to - 1 and returns, signalling to the caller that an exit condition exists. If this happens in our example, the code then prints a farewell message and exits. If intout + 2 contains a value of 1, the user clicked on the File Selector's OK button, or selected a file by double-clicking it, and fsel__input returns with d0 set to 0.
Next we test the first byte of our selection line (using the tst instruction, which, as you recall sets the 68000 condition codes according to the contents of an effective address operand without changing it). If the first byte is 0, the user clicked on the OK button, but with a blank selection line; in this case, we again return with - 1 in d0, causing the calling routine to exit, stage left. If the first byte is not 0, we have a valid filename on the selection line, and we'll return with d0 set to 0. (Note that in our example program, we aren't handling any errors returned in intout.)
Constructing a filename
Now that we got out of fsel__input in one piece, what do we do? As mentioned above, the fsel__input call changes the data in the directory and selection lines. Our job now is to parse this data and build a full pathname out of it, which will be the source filename for our copy operation.
We're going to store the full pathname of the source file in a location in our .bss segment labeled source. Since we'll also need to construct a full pathname for the destination file, we've written a subroutine for this purpose called make__name. This routine takes three parameters, passed in registers a0, a1 and a2: the address of the area in which to build the complete pathname (source), the address of the fsel__input directory line, and the address of the fsel__input selection line.
The make__name subroutine first copies the entire directory line to source, until it finds the zero terminator. Then it searches backward (using indirect addressing with pre-decrement) until it finds the last backslash in source, and moves the contents of the selection line to one byte past this location. When this is complete, the full pathname of the source file is contained in source.
The next section of code in the example program repeats the above steps to get the destination file from the user and build a complete pathname at the location labeled dest, by passing different parameters to the fsel__input and make__name subroutines.
To be continued
Hmmm. Looks like our discussion of fsel__input has gobbled up all the Assembly Line space for this issue. Next month, we'll continue taking apart our example program and maybe even get to discussing the file read/write routines. Until then, type in the example program, examine the code for yourself, and think about ways to improve it.