Permission to reprint or excerpt is granted only if the following line appears at the top of the article: ANTIC PUBLISHING INC., COPYRIGHT 1986. REPRINTED BY PERMISSION. PROFESSIONAL GEM by Tim Oren Column #15 - Coping with GEMDOS While it's fun playing with windows and object trees, one of the day-to-day realities of working with the ST is its operating system, GEMDOS. A successful application should insulate the user from the foibles and occasional calamities of the machine's file system. The GEM environment provides some minimal tools for doing this, but a good deal of responsibility still rests with you, the programmer. This column (#15 in the ST PRO GEM series) tries to address the GEM/DOS integration problem by providing you some stock code for common functions, along with a discussion of some of the worst "gotchas" lurking for the unwary. The download for this column is GMCL15.C, and it can be found in DL3 of PCS-58. You should obtain and list this file before proceeding. A BIT OF HISTORY. There has been a good deal of confusion in the Atari press and among developers over what GEMDOS is, and how it relates to TOS and CP/M-68K. It's important to clear this up, so you can get a true picture of what GEMDOS is intended to do. The best way is to tell the story of GEMDOS' origins, which I can do, because I was there. As most developers are aware, GEM was first implemented on the IBM PC. PC GEM performed two functions. The first was a windowed graphics extension to the PC environment. The second was a visual shell, the Desktop, which ran on top of the existing operating system, PC-DOS. When work started on moving GEM to the ST, there were two big problems. First, no STs actually existed. Second, there was no operating system on the 68000 with which GEM and the Desktop could run. Unix was too large, and CP/M-68K lacked a number of capabilities, such as hierarchical files, which were needed to support GEM. Work on porting the graphics parts of GEM to the 68000 had to start immediately to meet schedules. Therefore, CP/M-68K running on Apple Lisa's was used to get this part of the project off the ground. Naturally, the Alcyon C compiler and other tools which were native to this environment were used. In parallel, an effort was begun to write a new operating system for the 68000, which would ultimately become the ST's file system. It was designed to be a close clone of PC-DOS, since it would perform the same functions for GEM in the new environment. At this point, the term TOS was introduced. TOS really meant "the operating system, whatever it may be, that will run on the ST", since not even the specifications, let alone the code, were complete at that time. The first engineer to work on "TOS" at Digital Research was Jason Loveman. This name leaked to the press, and in some distorted fashion generated a rumor about "Jason DOS", which was still just the same unfinished project. As "TOS" became more solid, the developer's tools were ported to the new environment one by one, and the GEM programming moved with them. CP/M-68K was completely abandoned, though the old manuals for C and the tools lived on and are still found in the Atari developer's kit. All of this work had been done on Lisas or Compupro systems fitted with 68000 boards. At this point, workable ST prototypes became available. An implementation of "TOS" for the target machine was begun, even before the basic operating system was fully completed. The other intent for the new operating system was to be a base for GEM on other 68000 systems as well as the ST. Because of this, Digital Research named it GEMDOS when it was finally complete, thus providing the final bit of nomenclature. "TOS" as now found in the ST is in fact a particular implementation of generic GEMDOS, including the ST specific BIOS. So, GEMDOS is a PC-DOS clone, but, not quite. There are enough differences to cause problems if they are ignored. (Remember, it looks like a duck, and quacks like a duck, but it's not a duck.) GOING FOR IT. As a first example, consider the routines open_file() and create_file() at the beginning of the download. They make use of the GEMDOS calls Fopen() and Fcreate(). You will notice that these names are not the ones specified in the Digital Research GEMDOS manual. Developers who have used PC GEM will also observe that they are radically different from the function names in the PC-DOS bindings. In fact, all of the GEMDOS function calls on the ST are defined as macros in the file osbind.h, distributed with the developer's kit. At compile time they are turned into calls to the assembly language routine gemdos(), part of the osbind.o binary. So, if you find the naming conventions to be particularly offensive for some reason, just edit the appropriate macros in osbind.h. In DRI's PC-DOS bindings, any error codes were returned in the global variable DOS_ERR. In the GEMDOS bindings, the operation result or an error code is returned as the value of the calling function. In the case of Fopen() and Fcreate(), the result is a valid file handle if it is positive. A negative result is always an error code, indicating that the operation failed. An application which encounters a GEMDOS error should display an alert, and query for retry or abort. The type of loop structure exemplified by open_file() and create_file() should be usable with most GEMDOS functions which might fail. The AES provides a function, form_error, which implements a set of "canned" error alerts appropriate to the various possible errors. However, this is where the fun starts. For unknown reasons, the form_error on the ST expects to see PC-DOS, not GEMDOS, error codes as it's input! Therefore you need a routine to translate one into the other. The routine dos_error() in the download provides this function. The GEMDOS errors are in the same sequence as those for PC-DOS, but their numerical order is reversed and shifted. Notice also that dos_error() does NOT perform the translation if the error code is less than -50. These codes have no PC-DOS equivalent; computing a bogus translation will cause form_error to crash. Instead, they are passed through verbatim, resulting in a "generic" alert which gives only the error number. The other major task in integrating a GEM application with the file system is selecting file names for input and output. Again, the AES provides some assistance with the fsel_input call, which invokes the standard file selector dialog. There are several drawbacks to the standard file selector. One is that the "ITEM SELECTOR" title is constant and cannot be changed by the application. This could cause confusion for the user, since it may not be clear which of several functions, closely spaced in the FILE menu, was actually invoked. While it might be possible to find and "rewire" the AES resource that defines the file selector, it is unlikely that such an approach would be portable to a later version of ST GEM. A viable approach to eliminating confusion is to display a small "marquee" box, with a message defining the operation, on the screen just above the file selector. To do this, you must initialize the location of the box so that it is outside of the file selector's bounds, and then draw it just before invoking the file selector. This way they will appear together. Before returning to its main event loop, the application should post a redraw message for the "marquee" area. The AES will merge this redraw with the one generated by fsel_input, and the result will be received by the application's evnt_multi. Another problem with the file selector is that it resets your application's virtual workstation clip rectangle without warning. There are other AES functions, such as objc_draw, which also do this, but the file selector can be troublesome because it may be the only AES call used by some VDI-based ST applications. The veteran developer will also notice that the file selector takes and returns the path and filename as two separate strings, while the GEMDOS file functions require a fully pathed file name. Also, the file selector doesn't remember its "home" directory; you are responsible for determining the default directory, and keeping track of any changes. The remainder of the download and column is devoted to set of utilities which should alleviate some of the "grunt work" of these chores. The top level routine in this collection is get_file(). It is called with two string arguments. The first must point to a four byte string area containing the desired file name extension (three characters plus a null). The second is the default file name. If the default file name is non-null, then get_file() invokes parse_fname() to break it into path and name. Parse_fname() also adds the necessary "wild card" file specification to the path, using the extent name given as input. If no default file was supplied, or the default did not contain a path, the routine get_path() is invoked to find the current default directory and construct a legal path string for it. The results of these manipulations are supplied to fsel_input. Notice that the result of the file selector is returned via its third argument, rather than as a function value. If the result is TRUE, get_file() merges the temporary path and file string, storing the result via the second input parameter. This result string is suitable for use with Fopen, and may be resubmitted to get_file() when the next operation is invoked by the user. Parse_fname() is straight-forward C. It looks backward along the file to find the first character which is part of the path. The tail of the filename is copied off, and its former location is overlaid with the wild card specification. Get_path() is a bit more interesting. It makes use of two GEMDOS functions, Dgetdrv() and Dgetpath() to obtain the default disk drive and directory, respectively. Note that Dgetpath() will return a null string if the current default is the root, but it puts a back-slash at the beginning of the path otherwise. This forces a check for insertion in the root case, since the file selector wants to see something like "A:\*.RSC", rather than "A:*.RSC". After making this fix, get_path() concatenates the wild card specification derived from the input extent. The last routine in the download is new_ext(). This utility is useful if your application uses more than one associated file at a time. For instance, the Resource Construction Set uses both an RSC and a DEF file, with the same base name. New_ext() takes a fully formed file name, and replaces its old extent with the new one which you supply. This lets you quickly generate both file names after one call to the file selector. Notice that new_ext() looks BACKWARD along the name to find the delimiting period, since this character can also be part of a subdirectory name in the path. So we reach the end of the code and this column. Hopefully both will keep you profitably occupied for a while. July's column will return to graphics topics, with a look at writing customized rubber box and drag box routines, and ways to implement your own "pop-up" menus. August will bring techniques for displaying progress indicators, associating dialog and menu entries with keystrokes, and customizing objc_edit. I CAN'T HEAR YOU! The Feedback mailbag has been noticably flat of late. There have been a number of compliments on the column, which are much appreciated, and some suggestions for topics which fall outside the bounds of this series. The latter have been passed on to Antic for possible inclusion in their new ST quarterly, START. One recurring problem is finding the downloads. A number of the earlier columns say they are in PCS-132 (the old SIG*ATARI), and one says PCS-57 (mea culpa). In fact, ALL of the downloads are now in DL3 of PCS-58 (ATARI16). Filenames for first nine columns are all in the form GEMCLx.C, where x is the column's digit. For reasons unknown to me, the next two files were named GEMC10.C and GEMC11.C; the latest two downloads are called GMCL13.C and GMCL15.C. The latter naming pattern should continue into the future. Undoubtedly, one reason for the shortage of questions is the amazing ability to get a quick answer on the Developer's SIG, PCS- 57. This is a good sign of a strong Atari community on Compuserve. However, the SIG message style doesn't really lend itself to lengthy explanation, so suggestions for longer topics are always welcome here. Finally, I am now beginning the process of collecting these columns and some additional material into a book. In doing so, it would be helpful to know if you feel that any part of GEM has been slighted in my discussions. If so, let me know. Your suggestions will appear in future columns and finally make their way into the book. /*------------------------------*/ /* includes */ /*------------------------------*/ #include "portab.h" /* portable coding conv */ #include "machine.h" /* machine depndnt conv */ #include "osbind.h" /* BDOS defintions */ #include "gemdefs.h" /*------------------------------*/ /* open_file */ /*------------------------------*/ WORD open_file(file_name) BYTE *file_name; { LONG dos_hndl; FOREVER { dos_hndl = Fopen(file_name, 0); if (dos_hndl >= 0) return ((WORD) dos_hndl); if ( !dos_error((WORD) dos_hndl) ) return (-1); } return (-1); /* Appease lint */ } /*------------------------------*/ /* create_file */ /*------------------------------*/ WORD create_file(file_name) BYTE *file_name; { LONG dos_hndl; FOREVER { dos_hndl = Fcreate(file_name, 0); if (dos_hndl >= 0) return ((WORD) dos_hndl); if ( !dos_error((WORD) dos_hndl) ) return (-1); } return (-1); /* Appease lint */ } /*------------------------------*/ /* dos_error */ /*------------------------------*/ WORD dos_error(tos_err) WORD tos_err; { WORD f_ret; graf_mouse(ARROW, 0x0L); if (tos_err > -50) { tos_err += 31; tos_err = -tos_err; } f_ret = form_error(tos_err); return (f_ret); } /*------------------------------*/ /* get_file */ /*------------------------------*/ WORD get_file(extnt, got_file) BYTE *extnt, *got_file; { WORD butn, ii; BYTE tmp_path[64], tmp_name[13]; tmp_name[0] = '\0'; tmp_path[0] = '\0'; if (*got_file) parse_fname(got_file, tmp_path, tmp_name, extnt); if (!tmp_path[0]) get_path(&tmp_path[0], extnt); fsel_input(tmp_path, tmp_name, &butn); if (butn) { strcpy(got_file, tmp_path); for (ii = 0; got_file[ii] && got_file[ii] != '*'; ii++); got_file[ii - 1] = '\0'; strcat (got_file, "\\"); strcat(got_file, tmp_name); return (TRUE); } else return (FALSE); } /*------------------------------*/ /* parse_fname */ /*------------------------------*/ VOID parse_fname(full, path, name, extnt) BYTE *full, *path, *name, *extnt; { WORD i, j; BYTE *s, *d; for (i = strlen(full); i--; ) /* scan for end of path */ if (full[i] == '\\' || full[i] == ':') break; if (i == -1) strcpy(name, full); /* "Naked" file name */ else { strcpy(name, &full[i+1]); for (s = full, d = path, j = 0; j++ < i + 1; *d++ = *s++); strcpy(&path[i+1], "*."); strcat(path, extnt); } } /*------------------------------*/ /* get_path */ /*------------------------------*/ VOID get_path(tmp_path, spec) BYTE *tmp_path, *spec; { WORD cur_drv; cur_drv = Dgetdrv(); tmp_path[0] = cur_drv + 'A'; tmp_path[1] = ':'; Dgetpath(&tmp_path[2], 0); if (strlen(tmp_path) > 3) strcat(tmp_path, "\\"); else tmp_path[2] = '\0'; strcat(tmp_path, "*."); strcat(tmp_path, spec); } /*------------------------------*/ /* new_ext */ /*------------------------------*/ VOID new_ext(o_fname, n_fname, ext) BYTE *o_fname, *n_fname, *ext; { WORD ii, jj; strcpy(n_fname, o_fname); for (ii = (jj = strlen(n_fname)) - 1; ii && n_fname[ii] != '.'; ii--); if (!ii) n_fname[ii = jj] = '.'; strcpy(&n_fname[++ii], ext); }