14.12 HLA s _initialize_ and _finalize_ Strings


14.12 HLA's "_initialize_" and "_finalize_" Strings

Although HLA does not automatically call constructors and destructors associated with your classes, HLA does provide a mechanism whereby you can force HLA to automatically emit these calls: by using the _initialize_ and _finalize_ compile time string variables (i.e., val constants) HLA automatically declares in every procedure.

Whenever you write a procedure, iterator, or method, HLA automatically declares several local symbols in that routine. Two such symbols are _initialize_ and _finalize_. HLA declares these symbols as follows:

 val      _initialize_: string := "";      _finalize_: string := ""; 

HLA emits the _initialize_ string as text at the very beginning of the routine's body, i.e., immediately after the routine's begin clause.[9] Similarly, HLA emits the _finalize_ string at the very end of the routine's body, just before the end clause. This is comparable to the following:

 procedure SomeProc;      << declarations >> begin SomeProc;      @text( _initialize_ );           << procedure body >>      @text( _finalize_ ); end SomeProc; 

Because _initialize_ and _finalize_ initially contain the empty string, these expansions have no effect on the code that HLA generates unless you explicitly modify the value of _initialize_ prior to the begin clause or you modify _finalize_ prior to the end clause of the procedure. So if you modify either of these string objects to contain a machine instruction, HLA will compile that instruction at the beginning or end of the procedure. The following example demonstrates how to use this technique:

 procedure SomeProc;      ?_initialize_ := "mov( 0, eax );";      ?_finalize_ := "stdout.put( eax );" begin SomeProc;      // HLA emits "mov( 0, eax );" here in response to the _initialize_      // string constant.      add( 5, eax );      // HLA emits "stdout.put( eax );" here. end SomeProc; 

Of course, these examples don't save you much. It would be easier to type the actual statements at the beginning and end of the procedure than assign a string containing these statements to the _initialize_ and _finalize_ compile time variables. However, if we could automate the assignment of some string to these variables, so that you don't have to explicitly assign them in each procedure, then this feature might be useful. In a moment, you'll see how we can automate the assignment of values to the _initialize_ and _finalize_ strings. For the time being, consider the case where we load the name of a constructor into the _initialize_ string and we load the name of a destructor in to the _finalize_ string. By doing this, the routine will "automatically" call the constructor and destructor for that particular object.

The previous example has a minor problem. If we can automate the assignment of some value to _initialize_ or _finalize_, what happens if these variables already contain some value? For example, suppose we have two objects we use in a routine, and the first one loads the name of its constructor into the _initialize_ string; what happens when the second object attempts to do the same thing? The solution is simple: Don't directly assign any string to the _initialize_ or _finalize_ compile time variables; instead, always concatenate your strings to the end of the existing string in these variables. The following is a modification to the above example that demonstrates how to do this:

 procedure SomeProc;      ?_initialize_ := _initialize_ + "mov( 0, eax );";      ?_finalize_ := _finalize_ + "stdout.put( eax );" begin SomeProc;      // HLA emits "mov( 0, eax );" here in response to the _initialize_      // string constant.      add( 5, eax );      // HLA emits "stdout.put( eax );" here. end SomeProc; 

When you assign values to the _initialize_ and _finalize_ strings, HLA almost guarantees that the _initialize_ sequence will execute upon entry into the routine. Sadly, the same is not true for the _finalize_ string upon exit. HLA simply emits the code for the _finalize_ string at the end of the routine, immediately before the code that cleans up the activation record and returns. Unfortunately, "falling off the end of the routine" is not the only way that one could return from that routine. One could explicitly return from somewhere in the middle of the code by executing a ret instruction. Because HLA only emits the _finalize_ string at the very end of the routine, returning from that routine in this manner bypasses the _finalize_ code. Unfortunately, other than manually emitting the _finalize_ code, there is nothing you can do about this.[10] Fortunately, this mechanism for exiting a routine is completely under your control; if you never exit a routine except by "falling off the end" then you won't have to worry about this problem (note that you can use the exit control structure to transfer control to the end of a routine if you really want to return from that routine from somewhere in the middle of the code).

Another way to prematurely exit a routine, over which, unfortunately, you don't have any control, is by raising an exception. Your routine could call some other routine (e.g., a Standard Library routine) that raises an exception and then transfers control immediately to whomever called your routine. Fortunately, you can easily trap and handle exceptions by putting a try..endtry block in your procedure. Here is an example that demonstrates this:

 procedure SomeProc;      << declarations that modify _initialize_ and _finalize_ >> begin SomeProc;      << HLA emits the code for the _initialize_ string here. >>      try // Catch any exceptions that occur:           << Procedure Body Goes Here >>      anyexception           push( eax ); // Save the exception #.           @text( _finalize_ ); // Execute the _finalize_ code here.           pop( eax );          // Restore the exception #.           raise( eax );        // Reraise the exception.      endtry;      // HLA automatically emits the _finalize_ code here. end SomeProc; 

Although the previous code handles some problems that exist with _finalize_, by no means does it handle every possible case. Always be on the look out for ways your program could inadvertently exit a routine without executing the code found in the _finalize_ string. You should explicitly expand _finalize_ if you encounter such a situation.

There is one important place you can get into trouble with respect to exceptions: within the code the routine emits for the _initialize_ string. If you modify the _initialize_ string so that it contains a constructor call and the execution of that constructor raises an exception, this will probably force an exit from that routine without executing the corresponding _finalize_ code. You could bury the try..endtry statement directly into the _initialize_ and _finalize_ strings, but this approach has several problems, not the least of which is the fact that one of the first constructors you call might raise an exception that transfers control to the exception handler that calls the destructors for all objects in that routine (including those objects whose constructors you have yet to call). Although no single solution that handles all problems exists, probably the best approach is to put a try..endtry block within each constructor call if it is possible for that constructor to raise some exception that is possible to handle (i.e., doesn't require the immediate termination of the program).

Thus far this discussion of _initialize_ and _finalize_ has failed to address one important point: Why use this feature to implement the "automatic" calling of constructors and destructors because it apparently involves more work that simply calling the constructors and destructors directly? Clearly there must be a way to automate the assignment of the _initialize_ and _finalize_ strings or this section wouldn't exist. The way to accomplish this is by using a macro to define the class type. So now it's time to take a look at another HLA feature that makes is possible to automate this activity: the forward keyword.

You've seen how to use the forward reserved word to create procedure prototypes (see the discussion in the chapter on procedures), it turns out that you can declare forward const, val, type, and variable declarations as well. The syntax for such declarations takes the following form:

      ForwardSymbolName: forward( undefinedID ); 

This declaration is completely equivalent to the following:

      ?undefinedID: text := "ForwardSymbolName"; 

Especially note that this expansion does not actually define the symbol ForwardSymbolName. It just converts this symbol to a string and assigns this string to the specified text object (undefinedID in this example).

Now you're probably wondering how something like the above is equivalent to a forward declaration. The truth is, it isn't. However, forward declarations let you create macros that simulate type names by allowing you to defer the actual declaration of an object's type until some later point in the code. Consider the following example:

 type      myClass: class           var                i:int32;           procedure Create; returns( "esi" );           procedure Destroy;      endclass; #macro _myClass: varID;      forward( varID );      ?_initialize_ := _initialize_ + @string:varID + ".Create(); ";      ?_finalize_ := _finalize_ + @string:varID + ".Destroy(); ";      varID: myClass #endmacro; 

Note, and this is very important, that a semicolon does not follow the "varID: myClass" declaration at the end of this macro. You'll find out why this semicolon is missing in a little bit.

If you have the class and macro declarations above in your program, you can now declare variables of type _myClass that automatically invoke the constructor and destructor upon entry and exit of the routine containing the variable declarations. To see how, take a look at the following procedure shell:

 procedure HasmyClassObject; var      mco: _myClass; begin HasmyClassObject;      << do stuff with mco here >> end HasmyClassObject; 

Since _myClass is a macro, the procedure above expands to the following text during compilation:

 procedure HasmyClassObject; var      mco:                    // Expansion of the _myClass macro:           forward( _0103_ ); // _0103_ symbol is an HLA-supplied text symbol                              // that expands to "mco".      ?_initialize_ := _initialize_ + "mco" + ".Create(); ";      ?_finalize_ := _finalize_ + "mco" + ".Destroy(); ";      mco: myClass; begin HasmyClassObject;      mco.Create();           // Expansion of the _initialize_ string.      << do stuff with mco here >>      mco.Destroy();          // Expansion of the _finalize_ string. end HasmyClassObject; 

You might notice that a semicolon appears after "mco: myClass" declaration in the example above. This semicolon is not actually a part of the macro; instead it is the semicolon that follows the "mco: _myClass;" declaration in the original code.

If you want to create an array of objects, you could legally declare that array as follows:

 var      mcoArray: _myClass[10]; 

Because the last statement in the _myClass macro doesn't end with a semicolon, the declaration above will expand to something like the following (almost correct) code:

      mcoArray:               // Expansion of the _myClass macro:           forward( _0103_ ); // _0103_ symbol is an HLA-supplied text symbol                              // that expands to "mcoArray".           ?_initialize_ := _initialize_ + "mcoArray" + ".Create(); ";           ?_finalize_ := _finalize_ + "mcoArray" + ".Destroy(); ";           mcoArray: myClass[10]; 

The only problem with this expansion is that it only calls the constructor for the first object of the array. There are several ways to solve this problem; one is to append a macro name to the end of _initialize_ and _finalize_ rather than the constructor name. That macro would check the object's name (mcoArray in this example) to determine if it is an array. If so, that macro could expand to a loop that calls the constructor for each element of the array.

Another solution to this problem is to use a macro parameter to specify the dimensions for arrays of myClass. This scheme is easier to implement than the one above, but it does have the drawback of requiring a different syntax for declaring object arrays (you have to use parentheses around the array dimension rather than square brackets).

The forward directive is quite powerful and lets you achieve all kinds of tricks. However, there are a few problems of which you should be aware. First, because HLA emits the _initialize_ and _finalize_ code transparently, you can be easily confused if there are any errors in the code appearing within these strings. If you start getting error messages associated with the begin or end statements in a routine, you might want to take a look at the _initialize_ and _finalize_ strings within that routine. The best defense here is to always append very simple statements to these strings so that you reduce the likelihood of an error.

Fundamentally, HLA doesn't support automatic constructor and destructor calls. This section has presented several tricks to attempt to automate the calls to these routines. However, the automation isn't perfect and, indeed, the aforementioned problems with the _finalize_ strings limit the applicability of this approach. The mechanism this section presents is probably fine for simple classes and simple programs. One piece of advice is probably worth following: If your code is complex or correctness is critical, it's probably a good idea to explicitly call the constructors and destructors manually.

[9]If the routine automatically emits code to construct the activation record, HLA emits _initialize_'s text after the code that builds the activation record.

[10]Note that you can manually emit the _finalize_ code using the statement "@text( _finalize_ );".




The Art of Assembly Language
The Art of Assembly Language
ISBN: 1593272073
EAN: 2147483647
Year: 2005
Pages: 246
Authors: Randall Hyde

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net