.AP .LM 6 .RM 76 .CENTER 82;FORTRAN AS A SYSTEM PROGRAMMING LANGUAGE ON VAX/VMS .SKIP 2 .CENTER 82;Raymond P. Bovet .CENTER 82;National Center for Atmospheric Research .CENTER 82;Boulder, Colorado .SKIP 4 .CENTER 82;^&ABSTRACT\& .SKIP 1 .LM 11 .RM 70 This paper discusses a number of issues related to using FORTRAN for system programming on the VAX. Although FORTRAN is clearly not the ideal language for such work, in many installations it is the only high level language available. Examples of areas in which FORTRAN works well are presented together with warnings about the areas in which FORTRAN is very hard or impossible to use. Specific techniques such as the use of %LOC and %VAL are discussed. Working examples are provided for all topics covered in the paper. .SKIP 4 .LM 6 .RM 56 .CENTER 62;INTRODUCTION .SKIP 1 ^&Definition of System Programming\& .SKIP 0 In this paper system programming is taken to mean writing routines which make explicit use of operating system facilities rather than using only standard features of the language in which they are written. Thus I do not restrict system programming to mean just writing operating systems, compilers, and the like. .SKIP 1 ^&Choice of Language\& .SKIP 0 An ever-increasing choice of programming languages is available on the VAX. Many of these are better suited to system programming tasks than FORTRAN. In this section I will briefly discuss some of the advantages and disadvantages of various languages for system programming. .SKIP 1 MACRO allows the programmer unrestricted access to hardware. In addition, the ability to write macros gives the user a tremendous flexibility. The third advantage of MACRO is that it comes free once you buy VMS. One disadvantage of MACRO is that many programmers are frightened by writing in assembly language. Another is that, in my opinion at least, they are right! It takes substantially longer to write, debug, and modify routines written in assembly language. .SKIP 1 FORTRAN is the universal language of scientific programming. As a high level language it is relatively easy to modify and debug (although not nearly as good as the more modern languages). In scientific and engineering environments it is always a part of the system so no extra expense is involved in obtaining it. Also, programmer training problems are minimized. Unfortunately, FORTRAN also has a number of disadvantages as far as system programming is concerned. One is that FORTRAN cannot produce reentrant routines. Another is the lack of pointer variables. Also, it has no "record" structure nor any macro facility. .SKIP 1 PASCAL is a much more recent language than FORTRAN. As such, it incorporates block structure, reentrant routines, and strong typing. Next to FORTRAN, PASCAL is probably the best known language in technical fields. The biggest disadvantage of PASCAL is its cost. At least in my installation, this cost would have to justified solely on the basis of improved productivity of system programming. .SKIP 1 PRAXIS is a recent newcomer in the field of programming languages. Its design incorporates many of the elements of ADA. It is currently available free from the Structured Languages SIG. In contrast to most such freebies, it will apparently be maintained by a major software organization. PRAXIS does not include a macro facility (as far as I know) but does have facilities for calling routines with a variable number of arguments, and using keyword specification of arguments. The disadvantages of PRAXIS are that it is not well-known yet, and that it is currently only available for PDP-11 and VAX. I have high hopes for PRAXIS. .SKIP 1 BLISS is a very powerful high level language. It includes block structure and a macro facility (the only high level language that I know of that does), but still allows the programmer to be very close to the hardware. BLISS is the langauge DEC uses for system programming. It also has the advantage of being available for all of DEC's machines. One disadvantage of BLISS is that it looks weird until you get used to it. The principal problem though is that it is extremely expensive. .SKIP 1 ^&The VAX/VMS Calling Sequence\& .SKIP 0 One of the outstanding features of the VAX/VMS architecture is the standard VAX/VMS calling sequence. This standard allows routines written in any language to call routines written in any other language on the VAX. Calling routines may use either the CALLG or CALLS instructions to pass an argument list which may be located on the stack or at some arbitrary memory location. The called routine never needs to know how it was called. The standard allows for passing of arguments by immediate value, by reference (address), or by descriptor. .SKIP 1 In immediate value, the actual value of the parameter is passed to the routine. Passing arguments by reference involves placing the address of the parameter in the argument list. In the third method, passing arguments by descriptor, the address of a descriptor (which tells what type the argument is, its address, and its size) is placed on the argument list. .SKIP 1 One especially nice feature of the calling standard is that it specifies that all VAX native mode languages must supply mechanisms for passing arguments using any of these three techniques. Because of this, a routine can be written using whichever argument passing technique appears most appropriate for it without concern for whether it can be accessed from other languages. .SKIP 2 .CENTER 62;SYSTEM SERVICES .SKIP 1 The principal method for calling on operating system facilities in VAX/VMS is through the system services. Since these may be called via the standard calling sequence it is clear that they can be called from any language on the VAX. All system services return a status value (if they were written in FORTRAN they would be FUNCTION subprograms as opposed to SUBROUTINEs) which should always be checked. .SKIP 1 All the system services follow a common convention as to the type of parameter passing they expect. For input values of 32 bits or less, passing is by immediate value. Tables and quadword input variables are passed by reference, and character string inputs are passed by descriptor. All output values are passed by reference except for character strings which are passed by descriptor. This convention works nicely with FORTRAN as it follows exactly what FORTRAN does except for longword or shorter input values which FORTRAN normally passes by reference instead of by value. .SKIP 1 It is nice to use symbolic names which are predefined in the system for many of the input values and for all of the status values returned by the system services. Fortunately, these are also accessible via FORTRAN. .SKIP 1 ^&Special FORTRAN functions\& .SKIP 0 VAX-11 FORTRAN includes four special functions which are of great use in dealing with system services. These are the %LOC, %VAL, %REF, and %DESCR functions described below. .SKIP 1 The %LOC function returns the address of its argument. Thus it is very useful in situations where the address of something must be inserted into a table of some sort. For example, the %LOC function can be used to generate descriptors by hand (although there is little reason for doing this). As we shall see, %LOC can also be very helpful in importing the value of a symbolic name. Unlike the following functions, %LOC can appear anywhere in a program. .SKIP 1 The %VAL function can only be used inside an argument list to a subprogram. Its purpose is to instruct the compiler to pass its argument to the subprogram by immediate value. This is just what you need for 32 bit or shorter input parameters to system services. .SKIP 1 The %REF function is also only legal within an argument list to a subprogram. Its purpose is to tell the compiler to pass its argument to the subprogram by reference (address). The only time you might need this in FORTRAN is with a character string which would otherwise be passed by descriptor. This turns out to be a useful thing to do if the subprogram is expecting characters in the old FORTRAN 66 style. .SKIP 1 Finally we come to the %DESCR function. Again, this function can only be used within a call to a subprogram. It tells the compiler to generate a call by descriptor. Personally, I have yet to find a use for this since character strings are passed by descriptor by default. .SKIP 1 ^&Simple Examples\& .SKIP 0 It is probably most effective to give some examples of using system services from FORTRAN at this point. These examples are written as complete routines so that you can try them out exactly as they stand. .SKIP 1 .LITERAL PROGRAM CURRENT_TIME C This program types out the current time LOGICAL SYS$GETTIM,SYS$ASCTIM,I C binary system time is quadword INTEGER*4 NOW(2) C full system time fills 23 characters CHARACTER*23 TIME C first, get the binary representation C of the current time C output quadword passed by reference I = SYS$GETTIM(NOW) IF(.NOT.I)STOP 'COULDN''T GET CURRENT TIME' C always check for error C next, convert the binary time to ascii C output longword by ref, output char string C by descr, input quadword by ref I = SYS$ASCTIM(LENGTH,TIME,NOW,) IF(.NOT.I)STOP 'COULDN''T CONVERT TIME' C Now, display the result C 7 is the Ascii code for bell. TYPE *,'At the sound of the tone the'// & ' time will be',TIME(1:LENGTH),CHAR(7) STOP END .END LITERAL .SKIP 1 Note that in this example we didn't have to do anything special to force the argument passing mechanism to match that expected by the system services we called. It should be noted that we had to explicitly declare SYS$GETTIM and SYS$ASCTIM as LOGICAL so that FORTRAN wouldn't try to do some weird conversions on their results. .SKIP 1 In the next example, we will demonstrate the use of %VAL to pass a longword input value. Also, we compare the result of the system service to a symbolic value. .SKIP 1 .LITERAL SUBROUTINE P(N_FLAG) C C This subroutine is a very inefficient C implementation of Dijkstra's P operation C on a semaphore. C Event flags are used as semaphores, and C the caller passes the number of an event C flag as the argument to this subroutine. C C this time consider result integer INTEGER*4 SYS$SETEF C symbol comes from outside EXTERNAL SS$_WASSET C ERROR is a statement function which is C TRUE iff the status code is an error. LOGICAL ERROR ERROR(I) = MOD(I,2).NE.1 C Atomically test and set the semaphore C event flag passed by immed. val. 10 I = SYS$SETEF(%VAL(N_FLAG)) C Always check for errors IF(ERROR(I))THEN C If there is one, give them error no. TYPE *,I STOP 'UNABLE TO SET EF IN P' END IF C Was the semaphore already set ? C if so, try again IF(I.EQ.%LOC(SS$_WASSET))GOTO 10 C we got it. Now we have exclusive access C to the resource. Return to caller. RETURN END .END LITERAL .SKIP 1 Notice that in this example we had to declare SS$__WASSET as EXTERNAL so that the compiler wouldn't just assume it was to be the name of another variable within the subroutine. Further, when we wanted to refer to the value of the symbol SS$__WASSET we had to specify %LOC(SS$__WASSET). This is because the compiler assumes that what the linker inserts for an external symbol is its address. Also, in this example we do print out the error number. This brings us to the next topic. .SKIP 1 ^&Checking Status Values\& .SKIP 0 As I have mentioned, you should always check the status values that are returned from system services. The reason is that whenever you assume that some system service just can't fail, sooner or later some subtle change you make causes it to fail. These errors can be very hard to find if you don't check the status value. So, what you want most of the time is a mechanism that is easy to code in your program which checks whether a system service call succeeded and gives you a reasonable error message if it didn't. .SKIP 1 The solution I have adopted is to create a subroutine named IFERR. IFERR takes as its only argument a status value. If the status value represents a success, IFERR merely returns to its caller. If the status value is an error, IFERR signals the error. Unless you do something special (like set up exception handlers) the net effect is to print out the error message associated with the error followed by a traceback of where the error occurred. Then the program exits. This solution does what you want and is very easy to code. If, for example, you just want to call SYS$GETTIM (as in the first example we gave above) you code it as follows: .LITERAL CALL IFERR(SYS$GETTIM(NOW)) .END LITERAL Note that a single statement suffices to both call the system service and check the result. Further, we don't even have to declare what the datatype of SYS$GETTIM is. If you need to test the result of the system service against some specific value you can do it as in the following example: .SKIP 1 .LITERAL PROGRAM SET_NAME C C This program asks the user to supply a C new process name and sets it for them. C INTEGER*4 SYS$SETPRN EXTERNAL SS$_DUPLNAM C 15 is max length for a process name CHARACTER*15 NAME C Find out what name they want 10 TYPE *,'Desired process name : ' ACCEPT 100,L,NAME 100 FORMAT(Q,A) C Attempt to set new name C We must save the result to see if C this name is already used. I = SYS$SETPRN(NAME(1:L)) IF(I.EQ.%LOC(SS$_DUPLNAM))THEN TYPE *,'That name is already in use.' GOTO 10 END IF C Any other errors are fatal. CALL IFERR(I) C If no error, tell them it worked. TYPE *,'Your new process name is ' & //NAME(1:L) STOP END .END LITERAL .SKIP 1 Notice that here we did have to declare SYS$SETPRN as an integer since we assigned its value to another variable. .SKIP 1 The routine IFERR as described above could easily be written in FORTRAN. However, there are several compelling reasons for writing it in MACRO. One is that IFERR should be reentrant in case it gets called from within an AST. This is hard to implement in FORTRAN. Also, it is nice to add an additional feature to IFERR. One problem with the status codes returned by system services is that the error SS$__EXQUOTA - quota exceeded is generated whenever any of the quotas is exceeded. Often it is hard to tell which quota caused the problem. Thus, IFERR checks whether the error is SS$__EXQUOTA. If it is, IFERR also types out the current quota amounts available. Usually it is easy to determine which quota caused the failure by examining these numbers. The full listing of IFERR in MACRO follows: .SKIP 1 .LITERAL .title iferr ; ; This routine tests its argument (BLBS) ; to see whether it is an error or a ; success. If it is an error we SIGNAL ; the error so that it will cause image ; exit if it gets to the system estab- ; lished handlers. Typically, the ; result will be to output the error ; message associated with the error, ; a traceback of where it happened, ; and then stop. ; $JPIDEF $SSDEF iferr:: .word 0 ; if bottom bit set, success blbs @4(ap),done ; did we exceed our quotas ? cmpl @4(ap),#ss$_exquota bnequ noquo ; Yes. Show them current counts. calls #0,quota_err ; set up status result as arg to lib$stop noquo: pushl @4(ap) ; signal the error calls #1,lib$stop ; user can continue only if they ; unwind the stack (this level ; is assumed to be unable to continue). done: ret quota_err:: ; first get current counts .word 0 $getjpi_s itmlst=items pushl r0 pushl sp ; make sure getjpi succeeded calls #1,iferr popl r0 good: subl2 prccnt,prclm $faol_s ctrstr=string,outlen=rlen,- outbuf=buffer,prmlst=astcnt pushl r0 pushl sp ; make sure faol succeeded calls #1,iferr popl r0 good2: movzwl rlen,buffer ; modify descriptor to use resultant length ; This part borders on "tricky code". We want ; to let $putmsg actually write out the current ; quotas since it seems to have a magic way of ; doing this even if the open file quota has ; been exceeded. The technique we use is to ; call $putmsg with a dummy status (in this ; case ss$_normal), but specify an action ; routine which will be executed after the ; message code has been retrieved and stuffed ; into a buffer. The tricky part is that in the ; action routine we overwrite the descriptor of ; the buffer $putmsg used for our bogus message ; with a descriptor for the stuff we really want ; written out. pushl #ss$_normal pushl #1 movl sp,r0 $putmsg_s msgvec=(r0),actrtn=fake_it pushl r0 pushl sp ; make sure putmsg succeeded. calls #1,iferr good3: ret fake_it: ; see comments above .word 0 ; here's where we overwrite putmsg's descriptor movq buffer,@4(ap) movl #ss$_normal,r0 ret ; set up item list for call to getjpi .macro defitem item .word 4 .word jpi$_'item .long item .long 0 .endm defitem rlen: .word 0 nlen = 400 buff: .blkb nlen buffer: .long nlen .long buff items: defitem astcnt defitem biocnt defitem bytcnt defitem diocnt defitem filcnt defitem prccnt defitem prclm defitem tqcnt ; end of item list .long 0 astcnt: .long 0 biocnt: .long 0 bytcnt: .long 0 diocnt: .long 0 filcnt: .long 0 prclm: .long 0 tqcnt: .long 0 prccnt: .long 0 string: .ascid - ?Current quotas still available:? - ?!/AST (asynch. traps) !SL? - ?!/BIO (buffered I/Os) !SL? - ?!/BYT (buffered I/O bytes) !SL? - ?!/DIO (direct I/Os) !SL? - ?!/FIL (open files) !SL? - ?!/PRC (subprocesses) !SL? - ?!/TQE (timer queue entries) !SL? .end .END LITERAL .SKIP 1 ^&Complicated Examples\& .SKIP 0 Unfortunately, there are some system services which are considerably more difficult to deal with than the previous examples indicate. In particular, some system services require tables as input parameters. This can become troublesome especially since FORTRAN has no record structure. GETJPI is one such service. However, as the following example shows, the table format it requires can still be dealt with relatively easily. .SKIP 1 .LITERAL PROGRAM SHOW_PROC C C This program types out your current C process name, PID, and image name. C INTEGER*4 IOSB(2),TABLE(10),PROCESS_ID CHARACTER*15 PROCESS_NAME CHARACTER*80 IMAGE_NAME EXTERNAL JPI$_PRCNAM,JPI$_PID EXTERNAL JPI$_IMAGNAME C Ask for process name C We insert 2 words into a longword table C by shifting the second one and ORing in the C first one. TABLE(1) = ISHFT(%LOC(JPI$_PRCNAM),16).OR. & LEN(PROCESS_NAME) C We insert an address into the table with %LOC TABLE(2) = %LOC(PROCESS_NAME) TABLE(3) = %LOC(NAME_LENGTH) C Ask for Process ID (PID) TABLE(4) = ISHFT(%LOC(JPI$_PID),16).OR.4 TABLE(5) = %LOC(PROCESS_ID) C Specify no length return by putting in 0. TABLE(6) = 0 C Ask for image name TABLE(7) = ISHFT(%LOC(JPI$_IMAGNAME),16).OR. & LEN(IMAGE_NAME) TABLE(8) = %LOC(IMAGE_NAME) TABLE(9) = %LOC(IMAGE_LENGTH) C Insert zero to end list TABLE(10) = 0 C I have no idea what goes in the IOSB below ! CALL IFERR(SYS$GETJPI(%VAL(1),,,TABLE,IOSB, & ,)) TYPE 100,PROCESS_ID,PROCESS_NAME(1: & NAME_LENGTH) 100 FORMAT(' PROCESS ID : ',Z8, & '. YOUR PROCESS NAME IS : ',A) TYPE 101,IMAGE_NAME(1:IMAGE_LENGTH) 101 FORMAT(' FURTHERMORE, YOU ARE RUNNING : ',A) STOP 'ALL DONE' END .END LITERAL .SKIP 1 In the next example, things are still more complicated because some components of the table are bytes, others are words, and still others are longwords. Also, the bytes and words must be treated as unsigned quantities. .SKIP 1 .LITERAL PROGRAM SHOW_DEV C C This program gives error and utilization C stats for any device on the system. C C Note that all 3 of the following buffers C are the same size. BYTE BBUFFER(0:63) INTEGER*2 WBUFFER(0:31) INTEGER*4 LBUFFER(0:15) INTEGER*4 DESC(2) CHARACTER*80 DEVNAM,DNAM C Declare symbolic names for offsets within C the device information buffer. C Note that we assume that the word values C are aligned on word boundaries within C the buffer, and that longword values C are aligned on longword boundaries. C There is no guarantee that this C assumption will hold in future C relesases of VMS! EXTERNAL DIB$W_DEVNAMOFF,DIB$W_ERRCNT EXTERNAL DIB$L_OPCNT,DIB$W_UNIT C Equivalence all the buffers so we can easily C refer to byte, word, or longword entries C within them. EQUIVALENCE (BBUFFER,WBUFFER) EQUIVALENCE (BBUFFER,LBUFFER) C Make our own descriptor for the buffer. DESC(1) = 64 DESC(2) = %LOC(BBUFFER) 10 TYPE 100,'Give me device name : ' 100 FORMAT(1H$,A) READ(5,101,END=50)L,DEVNAM 101 FORMAT(Q,A) CALL IFERR(SYS$GETDEV(DEVNAM(1:L),,DESC,,)) C Device name is returned as a counted string. C Get offset to the counted string ICA = WBUFFER(%LOC(DIB$W_DEVNAMOFF)/2) C Do zero extend conversion from word to long ICA = ICA.AND.'FFFF'X C Get the count IC = BBUFFER(ICA) C Do a zero extend conversion again, C this time from byte to long. IC = IC.AND.'FF'X C Now extract the string DO 20 I = 1 , IC 20 DNAM(I:I) = CHAR(BBUFFER(ICA+I)) C Get the unit number IU = WBUFFER(%LOC(DIB$W_UNIT)/2) IU = IU.AND.'FFFF'X C And figure out how many characters it is. ILU = 1 IF(IU.GT.0)ILU = ALOG10(FLOAT(IU)) + 1. C Get error count I_ERROR = WBUFFER(%LOC(DIB$W_ERRCNT)/2) I_ERROR = I_ERROR.AND.'FFFF'X C And operation count I_OPS = LBUFFER(%LOC(DIB$L_OPCNT)/4) TYPE 102,DNAM(1:IC),IU,I_ERROR,I_OPS 102 FORMAT(' Device _',A,I,': has had',I6, & ' errors in ',I10,' operations.') GOTO 10 50 STOP 'ALL DONE' END .END LITERAL .SKIP 1 Notice the comment at the beginning of the program concerning our assumption that word quantities and longword quantities are word and longword aligned respectively in the buffer. This appears to be the case here. The only system service I know of in which this assumption does not hold true is in the CREPRC service where a list of quotas is required. The format of this list is a repetition of a byte followed by a longword. This means that the longwords are not longword aligned within the list. The only way to handle this situation in FORTRAN is to create a common block of bytes alternating with longwords. .SKIP 2 .CENTER 62;DYNAMIC ARRAYS .SKIP 1 Although it does not involve using a system service directly, the technique of creating arrays dynamically at run time on the VAX does lie beyond the scope of normal applications programming. This is a very useful thing to be able to do, especially when the size needed for the array cannot be determined at compile time. .SKIP 1 The way to manage dynamic memory allocation within a program is to call the two library routines LIB$GET__VM and LIB$FREE__VM. Although there are system services to handle create and delete additional address space, their direct use is not recommended. LIB$GET__VM requires two parameters. The first is the number of bytes of memory to allocate. The second is the address of a longword variable which will be set on return from LIB$GET__VM to contain the starting address of the allocated memory. Note that in contrast to system services, library routines pass input values of 32 bits or less by reference rather than by value. This makes calling them from FORTRAN especially easy. .SKIP 1 Once you have called LIB$GET__VM, your main problem is in getting FORTRAN to use the new memory space you have allocated. It turns out that about the only thing you can do with it is to pass it as a parameter to a subprogram. The subprogram will think that it was called with an ordinary array. After you have called LIB$GET__VM, you have a variable whose value is the starting address of the dynamic array. By telling FORTRAN to pass this variable by value you are actually passing the address of the dynamic array which is what the subprogram expects to see. .SKIP 1 Hopefully, the following example will make this more clear. .SKIP 1 .LITERAL PROGRAM DYNAMIC C C This program demonstrates the use of C dynamic arrays in FORTRAN. C C Find out how much memory to get 10 TYPE *,'How many bytes do you want ?' ACCEPT *,NBYTES C If <= 0 then quit IF(NBYTES.LE.0)STOP 'I QUIT' C Get the memory & check for errors CALL IFERR(LIB$GET_VM(NBYTES,IBASE)) C Now pass the memory to a subprogram. C Since the value of IBASE is the C address of the memory we just allocated, C pass it by value. CALL USE_IT(%VAL(IBASE),NBYTES) C Don't forget to give memory back when done! CALL IFERR(LIB$FREE_VM(NBYTES,IBASE)) GOTO 10 END SUBROUTINE USE_IT(BUFFER,N) C C This subroutine does nothing useful! C It thinks it gets called with a BYTE C array and the length of the array. C BYTE BUFFER(N) C Demonstrate that we can write into array DO 10 I = 1 , N 10 BUFFER(I) = 14 C Demonstrate that we can read array DO 20 I = 1 , N 20 TYPE *,BUFFER(I) C Nothing else to demonstrate ! RETURN END .END LITERAL .SKIP 1 .SKIP 2 .CENTER 62;USING RMS .SKIP 1 The easiest way to use RMS is simply to use the I/O statements which are a part of the FORTRAN language. However, these statements do not provide access to the full power of RMS. For example, RMS can purge the type ahead buffer on terminal reads, do a read with no echo from terminals, do a read with prompt from terminals, tell you the location of the current record in the file (RFA), or jump to a given location within a file (RFA). .SKIP 1 Fortunately, almost anything that can be done in RMS using MACRO can be duplicated in FORTRAN. The only reason that there is any difficulty is that RMS uses a number of different data blocks which are relatively hard to create without the aid of macros. These data blocks are the FAB, the RAB, etc. .SKIP 1 By specifying the USEROPEN keyword in the FORTRAN OPEN statement, you can let the compiler create all the data structures for you. Your useropen subprogram gets called with the addresses of a FAB, a RAB, and the unit number of the file to be opened. This subprogram can make any modifications it wants to any of the data structures before actually opening the file, and can save the address of the RAB for future reference. .SKIP 1 One complication is that we cannot EQUIVALENCE byte, word and longword versions of the data structures to each other to simplify references to items of varying sizes. This is because FORTRAN does not allow equivalencing formal parameters of subprograms. Thus, we have to treat the entire structure as a BYTE array. Another area of difficulty is that we often need to set and clear single bits within a word or longword. This can be conveniently done using the LIB$INSV routine which provides a high level language interface to the VAX insert bit field instruction. The following program shows an example of using the USEROPEN keyword. .SKIP 1 .LITERAL PROGRAM READ_WITH_PROMPT C C The purpose of this program is to C illustrate the useropen technique C for accessing RMS features not C normally available through FORTRAN. C C The user open routine must be declared EXTERNAL PROMPT_OPEN C Common area for communication to C user open routine. C We will pass it the address of the C prompt string and its size. COMMON/PROMPT/PROMPT_ADDR,PROMPT_LEN INTEGER*4 PROMPT_ADDR,PROMPT_LEN CHARACTER*80 PROMPT,LINE C Define the prompt C (Note that ASCII carriage return & C line feed are decimal 10 and 13). PROMPT = CHAR(10)//CHAR(13)//'LINE 00000 : ' C Fill in the common area PROMPT_ADDR = %LOC(PROMPT) PROMPT_LEN = 15 C Now, open the lun OPEN(UNIT=1,NAME='SYS$INPUT',READONLY, & TYPE='OLD',USEROPEN=PROMPT_OPEN) C Next, begin doing reads with prompts. ILINE = 0 10 CONTINUE C Encode the line number into the prompt ILINE = ILINE + 1 ENCODE(5,100,PROMPT(8:12))ILINE 100 FORMAT(I5) C and issue READ READ(1,101,END=20)LINE 101 FORMAT(A) C repeat until EOF GOTO 10 20 STOP 'THANKS' END INTEGER FUNCTION PROMPT_OPEN(IFAB,IRAB,ILUN) C C This subroutine modifies the RAB so C that read with prompt is specified C on all reads. C COMMON/PROMPT/PROMPT_ADDR(4),PROMPT_LEN(4) BYTE PROMPT_ADDR,PROMPT_LEN EXTERNAL RAB$L_PBF,RAB$B_PSZ,RAB$L_ROP EXTERNAL RAB$V_PMT INTEGER*4 SYS$OPEN,SYS$CONNECT C Dimension IRAB big enough to avoid problems C if they use /CHECK option on compile. BYTE IRAB(0:1000) C set PBF and PSZ fields in RAB DO 10 I = 1 , 4 10 IRAB(%LOC(RAB$L_PBF)+I-1) = PROMPT_ADDR(I) IRAB(%LOC(RAB$B_PSZ)) = PROMPT_LEN(1) C set prompt in record options C Note that LIB$INSV does not return a status CALL LIB$INSV(1,%LOC(RAB$V_PMT),1, & IRAB(%LOC(RAB$L_ROP))) C Now open the file and C connect the record stream. PROMPT_OPEN = SYS$OPEN(IFAB) C If open failed, don't bother with connect ! IF(MOD(PROMPT_OPEN,2).NE.1)RETURN PROMPT_OPEN = SYS$CONNECT(IRAB) RETURN END .END LITERAL .SKIP 1 .SKIP 2 .CENTER 62;ASTs .SKIP 1 One notion which is used throughout VMS is that of Asynchronous System Traps. The idea of these is that upon occurrence of specific (asynchronous) events, the system will interrupt the normal flow of execution of your program to call a special AST routine. The AST routine is executed to completion and control is returned to the mainline portion of your program. AST routines can be specified for execution upon completion of IO requests, after a specified time has elapsed, or when special events such as system power failure occur. .SKIP 1 There is no obvious problem in writing ASTs in FORTRAN. However, there are two points which can be tricky. First, FORTRAN routines are not reentrant. Because of this, you must ensure that any routines called as ASTs, or called by ASTs are never called by your mainline code. Otherwise your program will occasionally fail when the AST calls a routine which was being executed by the main thread of your program when the AST occurred. This type of error can be especially troublesome to isolate since it depends strongly on minute timing details. .SKIP 1 The second, minor, point to consider is that often an AST needs to reestablish itself as an AST for the next occurrence of the condition. This is slightly complicated by the fact that FORTRAN does not allow a routine to take %LOC of its own name. The easiest solution to this problem is to have the routine which first specified the AST store the address of the AST in a variable which can be passed to the AST either as the AST parameter or via a COMMON block. An example of an AST coded in FORTRAN follows: .SKIP 1 .LITERAL PROGRAM WAKE_UP C C The purpose of this program C is to wake you up by beeping C every second. C EXTERNAL WAIT_AST C Set up COMMON with time delay value C and address of the AST in it. COMMON/TIMER/TIME,WAIT_AST_ADDR INTEGER*4 TIME(2),WAIT_AST_ADDR C Remember that system times are quadwords C counted in 100 nanosecond ticks. C The low order half comes first. TIME(1) = 1 * 10000000 TIME(2) = 0 C Convert to delta time TIME(1) = - TIME(1) TIME(2) = -1 C Store AST address in COMMON WAIT_AST_ADDR = %LOC(WAIT_AST) C Issue first timer request CALL IFERR(SYS$SETIMR(,TIME, & %VAL(WAIT_AST_ADDR),%VAL(1))) C Now, just fiddle the time away. C The AST routine will handle everything! 10 GOTO 10 END SUBROUTINE WAIT_AST(M) C C This routine types out a message C and requests another wake up call C later on. C COMMON/TIMER/TIME,WAIT_AST_ADDR INTEGER*4 TIME(2),WAIT_AST_ADDR C Watch out, our parameter is passed C to us by value! Use %LOC to retrieve C that value. N = %LOC(M) C Type out message TYPE *,CHAR(7)//'Wake up call number',N C Request another timed AST CALL IFERR(SYS$SETIMR(,TIME, & %VAL(WAIT_AST_ADDR),%VAL(N+1))) RETURN END .END LITERAL .SKIP 1 .SKIP 2 .CENTER 62;CONCLUSIONS .SKIP 1 From these examples you might feel that any system programming you need to do under VMS can be done in FORTRAN. This is not the case. Before concluding, I would like to mention the areas of system programming where FORTRAN cannot be applied. .SKIP 1 The prime area of difficulty in using FORTRAN is that it is not reentrant. This means that special precautions must be taken when dealing with ASTs. Also, this clearly makes it unsuitable for applications such as writing device drivers. Another problem this introduces is that you must be careful in using utility routines written in FORTRAN in conjunction with routines written in a language such as PASCAL which allows recursion. .SKIP 1 The second unavoidable problem with FORTRAN is that it does not generate position independent data. This is a problem (although not an unsolvable one) if you want to create a shareable library. This is also a problem if you want to use the LIB$CALL__IMAGE routine used by the terminal independent screen formatting package for example. Any image activated by LIB$CALL__IMAGE must be truly position independent since its position in memory will only be determined at run time. .SKIP 1 The other problems with using FORTRAN for system programming are mainly annoying. It can be hard to find spelling errors in FORTRAN routines since the language does not require that all variables be declared. Also, the lack of a record structure and pointer variables can complicate life considerably. .SKIP 1 On the other hand, the CHARACTER data type in FORTRAN 77 is a real help in dealing with VMS since character descriptors are called for so often. .SKIP 1 My own experience over the past two years indicates that FORTRAN is indeed a viable language for system programming on the VAX. .SKIP 2 .CENTER 62;SOURCES OF FURTHER INFORMATION .SKIP 1 .LIST .LE;DEC's FORTRAN/MACRO Programmer Course has some good information on this stuff. .LE;The ^&VAX/VMS System Services Reference Manual\& has a section on calling system services from high level languages (section 2.2). .LE;The ^&VAX-11 FORTRAN User's Guide\& has an entire chapter (Chapter 6) devoted to FORTRAN call conventions. This chapter includes full descriptions and examples of use of %VAL, %REF, and %DESCR. It also includes information on calling system services. Section 3.5.9 and Appendix E are very helpful in describing the use of the USEROPEN keyword. .LE;The ^&VAX-11 Guide to Creating Modular Library Procedures\& is very interesting reading on the subject of creating library procedures and what they require. .END LIST