Contents

3.7 Porting the IO Interface to VAX/VMS

Ada solves many portability problems, but there are always a few problems moving software from one system to another. These problems can be reduced if the program is written with portability in mind, but they can never be completely eliminated.

Almost all of the software in this book was developed on an IBM PC AT clone, using the Meridian Ada compiler. Moving to a genuine IBM PC AT with the Alsys compiler was no trouble at all. Most of the software was moved to a VAX running the DEC ada compiler under VMS without any modification. The only real trouble was moving some of the I/O routines to the VAX. That's not unusual. I/O typically causes portability problems.

At first it might seem surprising that some things that are easy to do on a microcomputer are hard to do on a minicomputer. A minicomputer is more powerful, but power does not always imply ease of use. A jack hammer is a powerful tool, but it is easier to use a less powerful 16 oz claw hammer for small jobs (like hanging a picture). The VAX is powerful, but for a small job, like a custom terminal interface, a smaller computer is easier to use.

There are two major differences between the PC and the VAX. The first difference is the number of users that are simultaneously supported. DOS on the IBM PC is a single-user operating system. This means it was designed to make it as easy as possible for the programmer to get direct access to system resources (disk files, terminal, printer, and so on). VMS is a multiuser system designed prevent users from interfering with each other. It does this by preventing direct access to system resources. The only way VMS lets you use system resources is through an operating system call which gives you limited access. When you are trying to directly control a peripheral, small single-user operating systems work for you, large multiuser operating systems work against you.

The second difference is that the DEC software is tightly coupled to DEC hardware. DEC terminals are not general purpose dumb terminals-- they have been specially designed to take some of the burden off DEC software. This specialization results in improved performance under normal circumstances, but lacks the flexibility needed for non-DEC applications.

Limited access provided by the operating system, and specialization of the hardware are typical problems encountered whenever software is ported from one system to another. This isn't a unique problem with VAX/VMS. It happens all the time. That's why it is important to try to hide I/O details in a package that provides a consistent virtual interface regardless of the underlying system. If you don't, you fight the same battle every time you port another application program.

3.7.1 VMS package

The VMS operating system doesn't want to encourage you to exchange individual character data with the terminal (it isn't as efficient as block transfers), so those system services don't exist. That's why I had to write a package similar to the Alsys DOS package, containing the three subprograms I needed. I called this package VMS. It is shown in Listings 48 through 51.

3.7.2 Raising Exceptions

The VMS package has to call some system services. These services return with a status variable telling if operation was successful. I don't know enough about the VMS operating system to know why the service request would fail, but I do know that I probably don't want to ignore the failure and try to proceed anyway. I need to do something about the failure, but I don't know what. In cases like these it is good idea to raise an exception and let the next higher level worry about it. (The technical term for this is, "passing the buck.")

If I raised a predefined exception, like CONSTRAINT_ERROR, that would really confuse someone if the error ever happened. I need to declare a user defined exception unique to this problem. I decided to call it VMS_IO_ERROR. That's not a very descriptive name. It doesn't tell what the problem is. It would be better to call it, TERMINAL_OFF_LINE, or something like that. I couldn't do that because I don't know what the problem is. All I know is that it is somehow related to a VMS I/O system service.

If I declared this exception in the package body, instead of the specification, then the VMS subprograms could still raise it. The exception would propagate out of the package as an anonymous (unnamed) exception because names inside a package body aren't visible outside the body. The only way the application program could handle this anonymous exception would be with a handler something like when others => Do_Something_Appropriate;. Since the application program doesn't know about it, it probably won't provide a handler for it, so the program will terminate with an unhandled exception if it is ever raised.

I was really tempted to do this, because I believe VMS_IO_ERROR has to be a fatal error resulting in the immediate termination of the program. I resisted the temptation because it isn't my place to decide the fate of someone else's application program. If I leave the exception buried in the body, a client will only see that the package VMS contains three subprograms and think that it doesn't raise any exceptions. What a nasty surprise when the program bombs with a cryptic error message like, "unnamed exception raised at PC = 00AF0166 never handled."

Placing the exception in the package specification warns the client that the exception can occur. The client can decide what to do about it. I doubt that there is much that can be done, because when VMS dies it cuts down on your options in a big way, but perhaps the client knows a clever solution. I don't want to deny the client the option to recover from the error.

3.7.3 CONTROL-C Powder Keg

Unfortunately VMS isn't as generous with options as I am. Whoever wrote the QIO service decided that nobody would ever want to pass CONTROL_C, CONTROL_Y, or CONTROL_Z back to a main program. Whenever a user presses one of these three control keys, the QIO service diverts the program flow to a VMS default handler which is reluctant to give control back to your program.

This is a serious problem on two counts. First, it forced me to pick a different character for the panic button. (I picked the exclamation point for no particular reason.) Now users have to remember the panic button is CONTROL_C on the PC, but it is the ! key on a VAX. I could solve this problem by changing the IBM PC versions so that the exclamation point is the panic character on those versions, too. That would make the user operation consistent regardless of the system, but it wouldn't solve the second part of the problem. Old timers like me are used to using CONTROL_C as a panic button. Whenever things run amok, we hit CONTROL_C by force of habit. The VMS intercept of the CONTROL_C leaves the user program running, and removes any possibility of the user regaining control.

There is a LIB$DISABLE_CTRL VMS service that might be used to disable CONTROL_C and CONTROL_Y, but it doesn't seem to do anything about CONTROL_Z. I tried to add it to the package body, but it got really messy. This isn't supposed to be a book about quirks in VMS, and these things don't really have much to do with Ada, so I decided to leave them out. If there is a VMS Wizard among the readers of this book, who can write an improved version of this package that isn't beyond the comprehension of mere mortals, I will be glad to include it in the next edition of this book. In the mean time, we just have to live with the danger.

3.7.4 Operating System Limitations

Situations like this one often lead to criticism of Ada. The charge is that Ada doesn't have enough low-level capability, or Ada's run-time system is inadequate. This isn't an Ada problem, it is an operating system problem. The CONTROL_C problem doesn't exist on DOS, and I would have the same problem on the VAX if I were writing this FORTRAN, C, or assembly. The fact that my Ada program doesn't handle CONTROL_C on VAX/VMS isn't because Ada is inadequate-- it's a VAX/VMS limitation. If you can show me how to get unfiltered characters from the keyboard in FORTRAN running under VMS, I bet I can show you how to do it in Ada using the same technique. Ada just makes operating system limitations more visible because Ada programs attempt more ambitions projects. (Can you imagine trying to write the FORM_TERMINAL in FORTRAN?)

3.7.5 INPUT task

Listing 49 shows the VMS package body. The input subprogram bodies simply call entries in a task called INPUT. A complete treatment of tasks is beyond the scope of this book, so I was hoping to avoid the subject completely, but VMS forced me to use this one. Here is a quick overview of this particular task.

The INPUT task (Listing 50) has three entries: Keypush, Ready, and Get. It is generally suspended, waiting for one of those three entries to be called. Keypush is an asynchronous system trap (AST) entry. It is asynchronous because it happens whenever a user presses a key. (That is, it doesn't always happen at a certain point in the program.) It tells the INPUT task that it must process the input character so the user can press another key. You can consider it to be the input port of a buffer between the asynchronous input from the user and the synchronous requests for data from the application program. The Get entry point is the output port of the buffer. It waits to supply data to the program until the program asks for it, or makes the program wait until data is available. (It synchronizes the data with the program.) The Ready entry point tells the application program if there is any unprocessed data in the buffer or not. This is useful in programs that can be doing other things while waiting for data. The program can poll the Ready entry point periodically and process input data (if it is there) at its convenience.

The INPUT task is filled with "secret sauce" unique to VMS. It would have taken me forever to figure this out myself, but fortunately Lee Lucas and Dave Dent had to solve this problem before I did, and I was able to take advantage of their work. They didn't come up with this all by themselves. They adapted an earlier program by Dee DeCristofaro for their use. I say this not only to give credit where credit is due (and shift blame away from myself if this is a dumb way to do it), but also to make a software engineering point. Even in those cases where software can't be reused without change, modular programming can make it easier to adapt software from one application to another. Lee and Dave structured their software in such a way that I was able to easily recognize the parts I needed and could extract them for my use.

DEC Ada comes with two unique packages for interfacing with VMS system services. The main one is STARLET. (The name simply means that some of the DEC programmers like to name software modules after constellations, stars, planets, and so on.) If you want to print a copy of the STARLET package specification, be sure you dump it to a high speed line printer with a nearly full box of paper. I might have named the package NOT_KITCHEN_SINK because that is one of the few things it doesn't contain. Despite all that, it doesn't include the definition of Cond_value_type, so a second package, CONDITION_HANDLING, is needed too.

Finally, a there is a package called SYSTEM which gives the definition of address types, and other system dependent declarations. Section 13.7 of the LRM allows some special features to be added to this package. The DEC version includes conversions of addresses and integers to unsigned long words, and a function Or, which acts as a bit set operation.

The task body INPUT just glues pieces of these DEC- specific packages together. The task begins by calling STARLET.Assign to assign the user's terminal, called SYS$COMMAND, to a CHANNEL. When it does this it assigns a value of STARLET.Channel_type to the local variable CHANNEL. It also assigns a value of CONDITION_HANDLING.Cond_value_type to a variable called ASG_STATUS. A boolean function called CONDITION_HANDLING.Success knows how to examine the ASG_STATUS and decide if the operation was successful or not. The Assign operation should always succeed, so the Success function should always return TRUE when it examines ASG_STATUS. If it doesn't, the INPUT task raises VMS_IO_ERROR and gives up. (I've never seen that exception raised, and hope I never will.) Once the channel is assigned, it is ready for use. It need not be assigned again.

It may bother you that we don't know what the value of CHANNEL or ASG_STATUS is. It shouldn't. We don't need to know if these are integers, strings, or enumeration types. Keeping this information hidden from us prevents it from distracting us. If we knew CHANNEL was an integer, we might get lazy and just assign it the value of 27 instead of using SYS$COMMAND. That might work when we logged on at our usual terminal, but not when we logged on at another one.

After the terminal is assigned to a channel, the task enters a loop that tells the VMS operating system to get a keystroke from the keyboard, waits for keystroke, and then gives it to the client program. This loop continues as long as the VMS package is in scope. Since the VMS package is normally WITHed by VIRTUAL_TERMINAL, which is WITHed into a package like SCROLL_TERMINAL, which is WITHed into the main program, the loop continues until the main program ends.

When the loop begins, no data has been received yet. Therefore, the variable NEW_DATA is set to FALSE. This fact will be used to guard an entry point later in the task.

The STARLET procedure Qio starts an I/O operation and returns immediately without waiting for completion. The operation uses the CHANNEL that was assigned to SYS$COMMAND, and the function to be performed is to read a virtual block. This function is modified by IO_M_NOECHO, which tells it not to echo the characters to the screen as they are received. Furthermore, the IO_M_NOFILTR tells it not to interpret CONTROL_R, CONTROL_U, or DELETE, as editing characters, and passes them along to the task. (Alas, it still filters CONTROL_C, CONTROL_Y, and CONTROL_Z as we have already mentioned.)

The success of the Qio procedure is stored in QIO_STATUS, and it is interpreted by CONDITION_HANDLING.Success just as the ASG_STATUS was. The status of the operation (presumably values like ready, pending, in progress, complete, transfer count) is stored in QIO_IOSB, which is of type STARLET.IOSB_type. QIO_IOSB will have a transfer count of 0 the first time it is read, but later, after the user has pressed a key, the transfer count will be 1. The value of this variable will change as the result of a direct memory access operation, or perhaps as the result of an interrupt service routine.

The pragma Volatile(QIO_IOSB) tells the optimizer that QIO_IOSB can change without program intervention. Without that pragma, an optimizer might realize that it had already read QIO_IOSB and saved the value in a register. It would keep rereading the register and always find the same result.

KEYINPUT is a string, where the Qio procedure will put the input data. Normally this string is several characters long for efficiency, but I want to process each character individually, so I made the string one character long. The parameters P1 and P2 tell the Qio procedure where the string is and how long it is.

The ASTADR parameter in Qio tells the procedure the address of the Keypush entry. When the user presses a key, Qio will transfer the value of the key to KEYINPUT(1). Then Qio calls the Keypush entry to let the application program know there is new data in KEYINPUT(1).

After the Qio operation has been successfully initiated, the task enters an inner loop. This inner loop allows three alternatives. It can (1) accept a Keypush, (2) report that no keys have been pressed yet, or (3) terminate. The second alternative can happen multiple times. The first alternative can happen only once because it sets NEW_DATA to TRUE, which exits the inner loop. The last alternative, terminate, can happen only once.

Eventually the user will press a key, and the task will enter a second inner loop. It is strikingly similar to the first inner loop, except its first alternative is to accept a Get instead of a Keypush. The second inner loop ends when the Get entry is called. The Get entry copies the input character to the OUT parameter and sets NEW_DATA to FALSE to exit the loop. The outer loop calls the Qio procedure again and the cycle repeats.

Most of the time, the INPUT task is suspended, waiting for something to happen. When the main program ends, it will be sitting on a select statement that includes a terminate alternative, so it will end when the main program ends.

3.7.6 OUTPUT package

The OUTPUT package (Listing 51) looks a lot like the INPUT task. In fact, originally the source file was created by editing the INPUT task source code. Let's look at the differences.

The obvious difference is that it is a package, not a task. Since the first version was derived from a copy of INPUT, it used a second task for OUTPUT and it worked just fine. I could have left it a task, but I elected to change it to a package.

I think it is better not to use a task, not because of the task switching overhead, but mostly for a philosophical reason. Tasks should be reserved for independent, concurrent activities. The INPUT task deserves to be a task because it can be considered to be a separate program continuously scanning the keyboard so the main program doesn't have to.

If I were sharing a printer with other users, it would make sense to make OUTPUT a task that buffers output characters, checks for printer availability, and sends the characters when the printer is ready. That would allow the main program to continue processing even though the printer isn't available. That isn't the case here. I'm sending this data to the user's terminal, which should always be available. Generally I'm sending a prompt and waiting for the user's response. There is no sense running ahead to look for a response before the prompt is sent. So OUTPUT shouldn't be a separate thread of control. It is just another step in a sequential process.

That's why the Put procedure uses STARLET.Qiow instead of STARLET.Qio. The w stands for wait. I don't want to run ahead until the output character is on its way to the user. I have to wait until the character is sent, so I might as well give up the processor and let someone else use it.

3.7.7 Enforcing Order

Why isn't Put just a procedure? Why did I stick it in a package? The key is in the last few lines of the package. The output channel has to be assigned before data can be sent to it.

This raises a portability issue. I want to port all my programs developed under DOS to VMS, and none of my existing application programs assign the output channel because it isn't necessary on DOS. If I didn't hide this channel assignment in the elaboration of some package, I'd have to change all my application programs to port them to VMS.

Even if there wasn't a portability problem, there would still be a compelling reason to stick Put in a package. There is a procedural order that must be enforced: First assign the output channel once, then use it many times. If I didn't use a package, I would have to depend on every application programmer that ever uses the VMS package to remember to assign the output channel before use. If the application programmer forgot to do that, who knows what error would happen. I have to make sure the procedures are called in the correct order.

When an application program running under VMS uses SCROLL_TERMINAL it WITHs in VIRTUAL_TERMINAL, which elaborates the package VMS, which elaborates OUTPUT, which runs STARLET.Assign. All this happens automatically so the application program doesn't have to remember to assign the output channel. Furthermore, it all happens before the application program gets control, so the application program can't write to the output before it is assigned.

Tasks can also be used to enforce order. The INPUT task assigned the channel first and then used select statements to assure that Get was not accepted until after a key was pressed.

I think failure to consider order of execution is a major problem in Ada programming. I suspect this is because most programmers are used to programming single-task programs that automatically enforce order because they have a single thread of control, so they aren't used to thinking about it. Programmers who are used to writing programs with multiple tasks are more likely to think about it, but they may be tempted to rely on secret things they have discovered about their operating system scheduling algorithm, because that's the way they've always done it. Their programs aren't likely to be portable.

The Ada language does contain features that enforce an order of execution. The INPUT task and OUTPUT package are examples of how to use these features. Use them wisely.

3.7.8 Hardware Limitations

There is also a hardware interface problem on the VAX. If you are using a VAX you are almost certainly using a VT52, VT100, VT220, VT240, or something that emulates a DEC terminal. When you look at all those extra keys on the right side of a VT100, and the row of function keys across the top of the VT220, you would think there would be no problem making it compatible with the VIRTUAL_TERMINAL. Although a DEC terminal appears to have all the capability you need, it doesn't. Many of those keys don't get past the keyboard. For example, function keys F1 through F5 on a VT240 are dedicated to Hold Screen, Print Screen, Set-Up, Data/Talk, and Break. Pressing these keys makes the terminal do something, but sends nothing to the computer, so no amount of clever software can process them. The INSERT and DELETE keys don't exist on a DEC terminal, either.

The situation is a little better if you use a dumb terminal instead of a DEC terminal. The Televideo 910, for example, has fewer keys than a DEC terminal, but most of them produce unique codes. Even so, there are still a few problems. The DOWN arrow key on a TV910 produces the same ASCII code as the LINE_FEED key. Therefore, you really don't have a DOWN arrow key, you just have two LINE_FEED keys with different legends on the key cap, and there is no way for software to tell them apart.

The possibility of missing keys is the reason the VIRTUAL_TERMINAL package specification defines special control codes. A terminal may not have an INSERT key, but it will certainly have a control key and an A key, so the user can press CONTROL-A to simulate the INSERT key to add text. Even if the terminal doesn't have a DELETE key, the user can press CONTROL-E to erase characters. The VIRTUAL_TERMINAL package will work on any terminal, but it may be awkward on some terminals because they don't have the required keys.

The FORM_TERMINAL.Create procedure is more awkward to use on a DEC terminal than an IBM PC because function keys F1, F2, and F3 don't exist on the DEC terminal. FORM_TERMINAL.Create uses F1 to mark the beginning of a protected field, F2 to mark the beginning of an unprotected field, and F3 to mark the end of either. When creating a form on a DEC terminal you have to use CONTROL_F followed by a 1, 2, or 3 to simulate those missing keys. The program works, but it isn't as convenient as it would be if those function keys existed.

If you port the virtual terminal to your physical terminal, you may have some unused keys that produce unique codes. You may want to define these keys to replace missing keys. Just modify the VIRTUAL_TERMINAL package body so the unused keys get mapped to the control codes specified in the package specification. (If you do this, it might be confusing if you ever change terminals, but that's a price you might want to pay for convenience.)

3.7.9 DEC VIRTUAL_TERMINAL

Despite all these problems, it is possible to port the VIRTUAL_TERMINAL package to DEC Ada. The DEC version is shown in Listings 52 and 53. In theory, I shouldn't' have to change the package specification, but I did because of the way VAX/VMS handles control characters, and because my VMS package may want to raise an exception that the PC version doesn't need to raise. Since I wrote the VMS package to look a lot like the Alsys DOS package, the VIRTUAL_TERMINAL package body for VAX/VMS is almost identical to the Alsys body.

All I/O specific code is confined to the VIRTUAL_TERMINAL package. The more powerful SCROLL_TERMINAL and FORM_TERMINAL packages are built on top of the VIRTUAL_TERMINAL package. Therefore, once the VIRTUAL_TERMINAL is running on VAX/VMS, the SCROLL_TERMINAL and FORM_TERMINAL can be ported to VAX/VMS without modification.


Contents | Next ...