Maintaining Software Portability Stephen W. Bartlett July 8, 1982 I. Introduction Currently, most of our software was designed and is implemented on the Terak 8510/a. This situation will not be the status quo much longer. We are presently moving a respectable amount of software to microcomputers such as the IBM Personal Computer and the Apple ][. Needless to say, not all micros are created equal, and so portable software becomes necessary. The less system-dependent a dialog or utility program is, the less time and effort it will require to implement that software on a new system. This document describes different microcomputers and operating systems, and gives some techniques on how to code software so that it is more portable. II. General Considerations There are a few general issues to consider when coding a piece of software. Beware of calling U.C.S.D. intrinsics whose first four characters are "UNIT." The way in which they are implemented is almost certainly different for each different microcomputer and version of U.C.S.D. Pascal. It is better to hide the calls to these routines away in some low-level procedure (so that the number of calls is kept minimum) rather than sprinkle references to them throughout the software. When coding something which relies in some way (however small) on the way in which the screen is handled, comment it heavily and either make the constants used global or mention in the main file that system-dependent constants are declared in such-and-such file. Do the same when reading any character from the keyboard that does not correspond to a printable character. It is a good idea to put as much as possible of the system-dependent code in one file. Even if it amounts to only a couple of routines, it is generally easier for someone to implement your software on a new system if they have to modify only one file. Another good idea is to use the screen layout sheets that are available for the IBM and Terak. These come in handy when trying to decide on the placement of textports and graphports for the module. Each time the layout changes, use another sheet to sketch the new layout and give the name of the ports, their boundaries, and their dimensions. If a textport and graphport have the same boundaries, their boundary declarations should be constants, and their definitions should be kept together. This will help if the ports have to be changed for some reason or collapsed into one port (when the Ports unit is implemented). Example: ... CONST Left = 0; Top = 0; Width = 80; Height = 5; VAR PatternGraphs: GRAPHPORT; { PatternGraphs and PatternText } PatternText: TEXTPORT; { have the same boundaries. } ... TxpDefine(PatternText, Left, Top, Width, Height, []); GrDefine(PatternGraphs, Left, Top, Width, Height); ... If a textport and graphport overlap each other, make a note of it on the layout sheet and in the code itself. This will make it easier to see what side effects occur when changes are made to either one. If and when revisions are made, the coder revising the module will have an easier time figuring out where all the ports are without having to draw them up again. III. What Is System-Dependent? Compared to the other common systems discussed below, Terak I.5 is the most peculiar. It is unfortunate that, at the time of our software developement on the Terak 8510/a, we did not implement a significant percentage of that software on another system. Although steps were taken towards portability, many dependencies on the Terak exist in a good deal of our software. The following is a partial list of U.C.S.D. system-dependencies for the microcomputers we use. 1. PAGE(OUTPUT) does what it is supposed to on surprisingly few systems. 2. almost everything involving low-level graphics is system-dependent. Even DrawSeg, DrawLine, and DrawBlock may differ in the way they are used. Allocating the memory for, activating, and clearing the graphics screen are all done differently on different systems. 3. initializing an asynchronous unitread by calling UNITREAD with a last parameter value of 1 works on the I.5 Terak only. On all other systems, one calls UNITBUSY (versions II through III) or UNITSTATUS (versions IV and up) to see if a key is waiting, and then synchronously read with UNITREAD. 4. the value of UNITBUSY changes after initializing an asynchronous unitread under I.5. 5. the serial port is REMOTE: (unit 8) on some systems, and REMIN: (unit 7) / REMOUT: (unit 8) on others. 6. the arrow keys send a one-character sequence on the Terak and Apple ][e, while on most other micros they send multiple key sequences (varying from machine to machine in the values sent). 7. the use of MARK and RELEASE is system-dependent, as they are not implemented under IV.0 as they are under I.5. 8. MEMAVAIL returns a meaningful number on versions III and below; on versions IV and up, use VARAVAIL (which is not implemented on earlier versions). IV. A Good Technique Moving dialogs between microcomputers is a much simpler task if the code employs the following technique when the code is system-dependent. This technique takes advantage of the way U.C.S.D. Pascal comments are handled. Number one: there are two sets of comment delimiters that are identical in their effects. Number two: comments may be nested, provided one uses both types of delimiters. Number three: a comment may be opened not just once, but any number of times before it is closed by a single occurance of a matching end-of-comment delimiter. Example: (* This (* is a comment (* that spans the capitalized "this" through here --> *) Let's consider a common piece of I.5 code in a program using the unit TxtPort. PROCEDURE WhatzIt; ... VAR ... HeapTop: ^INTEGER; ... BEGIN MARK(HeapTop); ... TxpDefine(Question, ...); ... TxpDefine(Answer, ...); ... RELEASE(HeapTop); END; Suppose this routine was part of a dialog that is to be moved to the IBM Personal Computer. After much fussing, recompiling, and headache on the part of the coder doing the move, WhatzIt would probably resemble this: PROCEDURE WhatzIt; ... VAR ... BEGIN ... TxpDefine(Question, ...); ... TxpDefine(Answer, ...); ... VARDISPOSE(Answer, SIZEOF(TxPort) DIV 2); VARDISPOSE(Question, SIZEOF(TxPort) DIV 2); END; That's a lot of thinking and editing there. Suppose WhatzIt looked like the following to begin with: PROCEDURE WhatzIt; ... VAR ... (* I.5 dependent variables *) HeapTop: ^INTEGER; (* End I.5 *) ... BEGIN (* I.5 dependent section to save the top of the heap. *) MARK(HeapTop); (* End I.5 *) ... TxpDefine(Question, ...); ... TxpDefine(Answer, ...); ... (* I.5 dependent section to get rid of the textports defined above. *) RELEASE(HeapTop); (* End I.5 *) (* IV.0 dependent section to get rid of the textports defined above. *UNUSED) VARDISPOSE(Answer, SIZEOF(TxPort) DIV 2); VARDISPOSE(Question, SIZEOF(TxPort) DIV 2); (* End IV.0 *) END; Now all that is required to change WhatzIt from I.5 to IV.0 is to delete the word UNUSED from the IV.0 section and put it into the corresponding place in all of the I.5 sections. This technique may be used in other places where system-dependent code is used. It is a very general way of converting software from one implementation to another; its use should not be restricted to the above application. Let's look at an instance of how this technique could be used when a module utilizes the system-dependent DrawBlock routine. But first, an explanation of why system-dependent graphics is not transportable without modification. Graphics The problem of transporting graphics occurs because the resolution and aspect ratio differs from screen to screen. A screen's graphic resolution is the number of individual pixels we can work with vertically and horizontally. The aspect ratio is the ratio of the number of pixels vertically per inch to the number horizontally per inch. To illustrate a difference between screens, the graphics resolution on the Terak is 320 across by 240 down. On the IBM, it's 640 across by 200 down. The aspect ratio is 1:1 on the Terak and it is 2:5 on the IBM. The resolution and aspect ratio also affect the text. However, the effect is on the appearance and not on the placement which is why we're not that concerned about text. Most of the machines in the lab support 80 across by 24 (or 25) down text screens. One exception is the Apple ][ which is 40 across by 24 down - also notice that we rarely use the Apple ][ to bring up modules. When a module utilizes graphport routines to draw graphics on the screen, or when the graphics don't rely on the screen's resolution being a certain number of pixels across and down, then the module will transport fairly easily because the graphports unit will handle the mapping of pixels between machines. However, you may come across complicated graphics or animation within your module requiring the use of DrawBlock and/or other system-dependencies. The technique applied to the use of DrawBlock: DrawBlock uses a data file which is stored on the disc. Usually, this data file contains the picture that will be drawn on the screen. When a module uses DrawBlock on one machine, for example the Terak, it will need a different data file in order to run on a different machine like the IBM. So, to make transportability easier, the coder should declare two sets of constants - one for use on the Terak and the other for use on the IBM, and turn off compilation of the set not being used. Example: (compilation on Terak) (* TERAK constants *) CONST DataFileName = 'GI.CAR.8510'; KeyedFileName = 'GI2.TERAK.KFIL'; GrScrnWidth = 320; { for TERAK only, IBM DrawBlock does not need this constant } CarXWidth = 19; CarYWidth = 9; CharWidth = 4; CharHeight = 10; (* end of TERAK constants *) (* IBM constants *UNUSED) CONST DataFileName = 'GI.CAR.IBM'; KeyedFileName = 'GI2.IBM.KFIL CarXWidth = 39; CarYWidth = 9; CharWidth = 8; CharHeight = 8; (* end of IBM constants *) TYPE WhichCar = (Bumper, NoBumper); { the car can be one of two types: with or without a bumper } CarPicture = PACKED ARRAY[0..CarYWidth, 0..CarXWidth] OF BOOLEAN; { space for a single car } Parking Lot = ARRAY[WhichCar] OF CarPictures; VAR CarArray: ParkingLot; { array used by DrawBlock } ... PROCEDURE Initialize; BEGIN ... RESET(CarFile, DataFileName); ... IF NOT KFOpen(KeyedFileName, 1) ... END; { Initialize } ... PROCEDURE DrawCar(WhichOne: WhichCar; XPos, YPos: INTEGER); { Draws the car from the CarArray to the screen at coordinates (XPos, YPos). We must call a procedure that calls DrawBlock because of a bug in the interpreter. } CONST XORMode = 2; Procedure BugFix; Begin DrawBlock(CarArray[WhichOne], CarXWidth + 1, 0, 0, (* TERAK *) GrScreen^, GrScrnWidth, (* end of TERAK section *) XPos, YPos, CarXWidth + 1, CarYWidth + 1, XORMode); End; BEGIN BugFix; END; { DrawCar } ... { Suppose I want to draw the car starting in the second column and the fifth row (This is where the CharWidth and CharHeight constants come in handy and you don't have to count pixels!) } DrawCar(Bumper, 2*CharWidth { 2nd col }, 5*CharHeight { 5th row }); ... NOTE: as soon as dialogs start using the Ports unit, many of the system- depencencies in our code will vanish. Until then, we have to make do. revised by: Naomi M. Salvador July 13, 1983