As you may recall, the try..endtry
statement surrounds a block of statements in order to capture any exceptions that occur during the execution of those statements. The system raises exceptions in one of three ways: through a hardware fault (such as a divide-by-zero error), through an operating system-generated exception, or through the execution of the HLA raise
statement. You can write an exception handler to intercept specific exceptions using the exception
clause. The program in Example 1-8 provides a typical example of the use of this statement.
Example 1-8. try..endtry
example
program testBadInput; #include( "stdlib.hhf" ) static u: int32; begin testBadInput; try stdout.put( "Enter a signed integer:" ); stdin.get( u ); stdout.put( "You entered: ", u, nl ); exception( ex.ConversionError ) stdout.put( "Your input contained illegal characters" nl ); exception( ex.ValueOutOfRange ) stdout.put( "The value was too large" nl ); endtry; end testBadInput;
HLA refers to the statements between the try
clause and the first exception
clause as the protected statements. If an exception occurs within the protected statements, then the program will scan through each of the exceptions and compare the value of the current exception against the value in the parentheses after each of the exception
clauses.[18] This exception value is simply a 32-bit value. The value in the parentheses after each exception
clause, therefore, must be a 32-bit value. The HLA excepts.hhf header file predefines several exception constants. Although it would be an incredibly bad style violation, you could substitute the numeric values for the two exception
clauses above.
If the program scans through all the exception
clauses in a try..endtry
statement and does not match the current exception value, then the program searches through the exception
clauses of a dynamically nested try..endtry
block in an attempt to find an appropriate exception handler. For example, consider the code in Example 1-9.
Example 1-9. Nested try..endtry
statements
program testBadInput2; #include( "stdlib.hhf" ) static u: int32; begin testBadInput2; try try stdout.put( "Enter a signed integer: " ); stdin.get( u ); stdout.put( "You entered: ", u, nl ); exception( ex.ConversionError ) stdout.put( "Your input contained illegal characters" nl ); endtry; stdout.put( "Input did not fail due to a value out of range" nl ); exception( ex.ValueOutOfRange ) stdout.put( "The value was too large" nl ); endtry; end testBadInput2;
In Example 1-9 one try
statement is nested inside another. During the execution of the stdin.get
statement, if the user enters a value greater than four billion and some change, then stdin.get
will raise the ex.ValueOutOfRange
exception. When the HLA runtime system receives this exception, it first searches through all the exception clauses in the try..endtry
statement immediately surrounding the statement that raised the exception (this would be the nested try..endtry
in the example above). If the HLA runtime system fails to locate an exception handler for ex.ValueOutOfRange
, then it checks to see if the current try..endtry
is nested inside another try..endtry
(as is the case in Example 1-9). If so, the HLA runtime system searches for the appropriate exception clause in the outer try..endtry
statement. Within the try..endtry
block appearing in Example 1-9 the program finds an appropriate exception handler, so control transfers to the statements after the exception( ex.ValueOutOfRange )
clause.
After leaving a try..endtry
block, the HLA runtime system no longer considers that block active and will not search through its list of exceptions when the program raises an exception.[19] This allows you to handle the same exception differently in other parts of the program.
If two try..endtry
statements handle the same exception, and one of the try..endtry
blocks is nested inside the protected section of the other try..endtry
statement, and the program raises an exception while executing in the innermost try..endtry
sequence, then HLA transfers control directly to the exception handler provided by the innermost try..endtry
block. HLA does not automatically transfer control to the exception handler provided by the outer try..endtry
sequence.
In the previous example (Example 1-9) the second try..endtry
statement was statically nested inside the enclosing try..endtry
statement.[20] As mentioned without comment earlier, if the most recently activated try..endtry
statement does not handle a specific exception, the program will search through the exception
clauses of any dynamically nesting try..endtry
blocks. Dynamic nesting does not require the nested try..endtry
block to physically appear within the enclosing try..endtry
statement. Instead, control could transfer from inside the enclosing try..endtry
protected block to some other point in the program. Execution of a try..endtry
statement at that other point dynamically nests the two try
statements. Although there are many ways to dynamically nest code, there is one method you are probably familiar with from your high-level language experience: the procedure call. In Chapter 5, when you learn how to write procedures (functions) in assembly language, you should keep in mind that any call to a procedure within the protected section of a try..endtry
block can create a dynamically nested try..endtry
if the program executes a try..endtry
within that procedure.
Whenever a program executes the try
clause, it preserves the current exception environment and sets up the system to transfer control to the exception
clauses within that try..endtry
statement should an exception occur. If the program successfully completes the execution of a try..endtry
protected block, the program restores the original exception environment and control transfers to the first statement beyond the endtry
clause. This last step, restoring the execution environment, is very important. If the program skips this step, any future exceptions will transfer control to this try..endtry
statement even though the program has already left the try..endtry
block. Example 1-10 demonstrates this problem.
Example 1-10. Improperly exiting a try..endtry
statement
program testBadInput3; #include( "stdlib.hhf" ) static input: int32; begin testBadInput3; // This forever loop repeats until the user enters // a good integer and the break statement below // exits the loop. forever try stdout.put( "Enter an integer value: " ); stdin.get( input ); stdout.put( "The first input value was: ", input, nl ); break; exception( ex.ValueOutOfRange ) stdout.put( "The value was too large, re-enter." nl ); exception( ex.ConversionError ) stdout.put( "The input contained illegal characters, re-enter." nl ); endtry; endfor; // Note that the following code is outside the loop and there // is no try..endtry statement protecting this code. stdout.put( "Enter another number: " ); stdin.get( input ); stdout.put( "The new number is: ", input, nl ); end testBadInput3;
This example attempts to create a robust input system by putting a loop around the try..endtry
statement and forcing the user to reenter the data if the stdin.get
routine raises an exception (because of bad input data). While this is a good idea, there is a big problem with this implementation: the break
statement immediately exits the forever..endfor
loop without first restoring the exception environment. Therefore, when the program executes the second stdin.get
statement, at the bottom of the program, the HLA exception-handling code still thinks that it's inside the try..endtry
block. If an exception occurs, HLA transfers control back into the try..endtry
statement looking for an appropriate exception handler. Assuming the exception was ex.ValueOutOfRange
or ex.ConversionError
, the program in Example 1-10 will print an appropriate error message and then force the user to re-enter the first value. This isn't desirable.
Transferring control to the wrong try..endtry
exception handlers is only part of the problem. Another big problem with the code in Example 1-10 has to do with the way HLA preserves and restores the exception environment: specifically, HLA saves the old execution environment information in a special region of memory known as the stack. If you exit a try..endtry
without restoring the exception environment, this leaves the old execution environment information on the stack, and this extra data on could cause your program to malfunction.
Although this discussion makes it quite clear that a program should not exit from a try..endtry
statement in the manner that Example 1-10 uses, it would be nice if you could use a loop around a try..endtry
block to force the reentry of bad data as this program attempts to do. To allow for this, HLA's try..endtry
statement provides an unprotected
section. Consider the code in Example 1-11.
Example 1-11. The try..endtry
unprotected section
program testBadInput4; #include( "stdlib.hhf" ) static input: int32; begin testBadInput4; // This forever loop repeats until the user enters // a good integer and the break statement below // exits the loop. Note that the break statement // appears in an unprotected section of the try..endtry // statement. forever try stdout.put( "Enter an integer value: " ); stdin.get( input ); stdout.put( "The first input value was: ", input, nl ); unprotected break; exception( ex.ValueOutOfRange ) stdout.put( "The value was too large, re-enter." nl ); exception( ex.ConversionError ) stdout.put( "The input contained illegal characters, re-enter." nl ); endtry; endfor; // Note that the following code is outside the loop and there // is no try..endtry statement protecting this code. stdout.put( "Enter another number: " ); stdin.get( input ); stdout.put( "The new number is: ", input, nl ); end testBadInput4;
Whenever the try..endtry
statement hits the unprotected
clause, it immediately restores the exception environment. As the phrase suggests, the execution of statements in the unprotected
section is no longer protected by that try..endtry
block (note, however, that any dynamically nesting try..endtry
statements will still be active; unprotected
turns off only the exception handling of the try..endtry
statement containing the unprotected
clause). Because the break
statement in Example 1-11 appears inside the unprotected
section, it can safely transfer control out of the try..endtry
block without "executing" the endtry
because the program has already restored the former exception environment.
Note that the unprotected
keyword must appear in the try..endtry
statement immediately after the protected
block. That is, it must precede all exception
keywords.
If an exception occurs during the execution of a try..endtry
sequence, HLA automatically restores the execution environment. Therefore, you may execute a break
statement (or any other instruction that transfers control out of the try..endtry
block) within an exception
clause.
Because the program restores the exception environment upon encountering an unprotected
block or an exception
block, an exception that occurs within one of these areas immediately transfers control to the previous (dynamically nesting) active try..endtry
sequence. If there is no nesting try..endtry
sequence, the program aborts with an appropriate error message.
In a typical situation, you will use a try..endtry
statement with a set of exception
clauses that will handle all possible exceptions that can occur in the protected section of the try..endtry
sequence. Often, it is important to ensure that a try..endtry
statement handles all possible exceptions to prevent the program from prematurely aborting due to an unhandled exception. If you have written all the code in the protected section, you will know the exceptions it can raise, so you can handle all possible exceptions. However, if you are calling a library routine (especially a third-party library routine), making a OS API call, or otherwise executing code that you have no control over, it may not be possible for you to anticipate all possible exceptions this code could raise (especially when considering past, present, and future versions of the code). If that code raises an exception for which you do not have an exception
clause, this could cause your program to fail. Fortunately, HLA's try..endtry
statement provides the anyexception
clause that will automatically trap any exception the existing exception
clauses do not handle.
The anyexception
clause is similar to the exception
clause except it does not require an exception number parameter (because it handles any exception). If the anyexception
clause appears in a try..endtry
statement with other exception
sections, the anyexception
section must be the last exception handler in the try..endtry
statement. An anyexception
section may be the only exception handler in a try..endtry
statement.
If an otherwise unhandled exception transfers control to an anyexception
section, the EAX register will contain the exception number. Your code in the anyexception
block can test this value to determine the cause of the exception.
The try..endtry
statement preserves several bytes of data whenever you enter a try..endtry
statement. Upon leaving the try..endtry
block (or hitting the unprotected
clause), the program restores the exception environment. As long as no exception occurs, the try..endtry
statement does not affect the values of any registers upon entry to or upon exit from the try..endtry
statement. However, this claim is not true if an exception occurs during the execution of the protected statements.
Upon entry into an exception
clause, the EAX register contains the exception number, but the values of all other general-purpose registers are undefined. Because the operating system may have raised the exception in response to a hardware error (and, therefore, has played around with the registers), you can't even assume that the general-purpose registers contain whatever values they happened to contain at the point of the exception. The underlying code that HLA generates for exceptions is subject to change in different versions of the compiler, and certainly it changes across operating systems, so it is never a good idea to experimentally determine what values registers contain in an exception handler and depend on those values in your code.
Because entry into an exception handler can scramble the register values, you must ensure that you reload important registers if the code following your endtry
clause assumes that the registers contain certain values (i.e., values set in the protected section or values set prior to executing the try..endtry
statement). Failure to do so will introduce some nasty defects into your program (and these defects may be very intermittent and difficult to detect because exceptions rarely occur and may not always destroy the value in a particular register). The following code fragment provides a typical example of this problem and its solution:
static sum: int32; . . . mov( 0, sum ); for( mov( 0, ebx ); ebx < 8; inc( ebx )) do push( ebx ); // Must preserve ebx in case there is an exception. forever try stdin.geti32(); unprotected break; exception( ex.ConversionError ) stdout.put( "Illegal input, please re-enter value: " ); endtry; endfor; pop( ebx ); // Restore ebx's value. add( ebx, eax ); add( eax, sum ); endfor;
Because the HLA exception-handling mechanism messes with the registers, and because exception handling is a relatively inefficient process, you should never use the try..endtry
statement as a generic control structure (e.g., using it to simulate a switch/case
statement by raising an integer exception value and using the exception clauses as the cases to process). Doing so will have a very negative impact on the performance of your program and may introduce subtle defects because exceptions scramble the registers.
For proper operation, the try..endtry
statement assumes that you use the EBP register only to point at activation records (Chapter 5 discusses activation records). By default, HLA programs automatically use EBP for this purpose; as long as you do not modify the value in EBP, your programs will automatically use EBP to maintain a pointer to the current activation record. If you attempt to use the EBP register as a general-purpose register to hold values and compute arithmetic results, HLA's exception-handling capabilities will no longer function properly (along with other possible problems). Therefore, you should never use the EBP register as a general-purpose register. Of course, this same discussion applies to the ESP register.
[18] Note that HLA loads this value into the EAX register. So upon entry into an exception
clause, EAX contains the exception number.
[19] Unless, of course, the program re-enters the try..endtry
block via a loop or other control structure.
[20] Statically nested means that one statement is physically nested within another in the source code. When we say one statement is nested within another, this typically means that the statement is statically nested within the other statement.