14.8 Writing Class Methods and Procedures


14.8 Writing Class Methods and Procedures

For each class procedure and method prototype appearing in a class definition, there must be a corresponding procedure or method appearing within the program (for the sake of brevity, this section will use the term routine to mean procedure or method from this point forward). If the prototype does not contain the @external option, then the code must appear in the same compilation unit as the class declaration. If the @external option does follow the prototype, then the code may appear in the same compilation unit or a different compilation unit (as long as you link the resulting object file with the code containing the class declaration). Like external (non-class) procedures, if you fail to provide the code the linker will complain when you attempt to create an executable file. To reduce the size of the following examples, they will all define their routines in the same source file as the class declaration.

HLA class routines must always follow the class declaration in a compilation unit. If you are compiling your routines in a separate unit, the class declarations must still precede the implementation of the routines from the class (usually via an #include file). If you haven't defined the class by the time you define a routine like point.distance, HLA doesn't know that point is a class and, therefore, doesn't know how to handle the routine's definition.

Consider the following declarations for a point2D class:

 type      point2D: class      const           UnitDistance: real32 := 1.0;      var           x: real32;           y: real32;      static           LastDistance: real32;      method distance( fromX: real32; fromY:real32 ); returns( "st0" );      procedure InitLastDistance; endclass; 

The distance function for this class should compute the distance from the object's point to (fromX,fromY). The following formula describes this computation:

A first pass at writing the distance method might produce the following code:

 method point2D.distance( fromX:real32; fromY:real32 ); nodisplay; begin distance;      fld( x );                       // Note: this doesn't work!      fld( fromX );                   // Compute (x-fromX)      fsub();      fld( st0 );                     // Duplicate value on TOS.      fmul();                         // Compute square of difference.      fld( y );                       // This doesn't work either.      fld( fromY );                   // Compute (y-fromY)      fsub();      fld( st0 );                     // Compute the square of the difference.      fmul();      fsqrt(); end distance; 

This code probably looks like it should work to someone who is familiar with an object-oriented programming language like C++ or Delphi. However, as the comments indicate, the instructions that push the x and y variables onto the FPU stack don't work; HLA doesn't automatically define the symbols associated with the data fields of a class within that class's routines.

To learn how to access the data fields of a class within that class's routines, we need to back up a moment and discover some very important implementation details concerning HLA's classes. To do this, consider the following variable declarations:

 var      Origin: point2D;      PtInSpace: point2D; 

Remember, whenever you create two objects like Origin and PtInSpace, HLA reserves storage for the x and y data fields for both of these objects. However, there is only one copy of the point2D.distance method in memory. Therefore, were you to call Origin.distance and PtInSpace.distance, the system would call the same routine for both method invocations. Once inside that method, one has to wonder what an instruction like "fld( x );" would do. How does it associate x with Origin.x or PtInSpace.x? Worse still, how would this code differentiate between the data field x and a global object x? In HLA, the answer is it doesn't. You do not specify the data field names within a class routine by simply using their names as though they were common variables.

To differentiate Origin.x from PtInSpace.x within class routines, HLA automatically passes a pointer to an object's data fields whenever you call a class routine. Therefore, you can reference the data fields indirectly off this pointer. HLA passes this object pointer in the ESI register. This is one of the few places where HLA-generated code will modify one of the 80x86 registers behind your back: Anytime you call a class routine, HLA automatically loads the ESI register with the object's address. Obviously, you cannot count on ESI's value being preserved across class routine calls nor can you pass parameters to the class routine in the ESI register (though it is perfectly reasonable to specify "@use esi;" to allow HLA to use the ESI register when setting up other parameters). For class methods (but not procedures), HLA will also load the EDI register with the address of the classes's virtual method table. While the virtual method table address isn't as interesting as the object address, keep in mind that HLA-generated code will overwrite any value in the EDI register when you call a class method or an iterator. Again, "EDI" is a good choice for the @use operand for methods because HLA will wipe out the value in EDI anyway.

Upon entry into a class routine, ESI contains a pointer to the (non-static) data fields associated with the class. Therefore, to access fields like x and y (in our point2D example), you could use an address expression like the following:

 (type point2D [esi]).x 

Because you use ESI as the base address of the object's data fields, it's a good idea not to disturb ESI's value within the class routines (or, at least, preserve ESI's value across the code where you must use ESI for some other purpose). Note that if you call a method you do not have to preserve EDI (unless, for some reason, you need access to the virtual method table, which is unlikely).

Accessing the fields of a data object within a class's routines is such a common operation that HLA provides a shorthand notation for casting ESI as a pointer to the class object: this. Within a class in HLA, the reserved word this automatically expands to a string of the form "(type classname [esi])" substituting, of course, the appropriate class name for classname. Using the this keyword, we can (correctly) rewrite the previous distance method as follows:

 method point2D.distance( fromX:real32; fromY:real32 ); nodisplay; begin distance;           fld( this.x );           fld( fromX );                   // Compute (x-fromX)           fsub();           fld( st0 );                     // Duplicate value on TOS.           fmul();                         // Compute square of difference.           fld( this.y );           fld( fromY );                   // Compute (y-fromY)           fsub();           fld( st0 );                     // Compute the square of the difference.           fmul();           fsqrt(); end distance; 

Don't forget that calling a class routine wipes out the value in the ESI register. This isn't obvious from the syntax of the routine's invocation. It is especially easy to forget this when calling some class routine from inside some other class routine; don't forget that if you do this the internal call wipes out the value in ESI and on return from that call ESI no longer points at the original object. Always push and pop ESI (or otherwise preserve ESI's value) in this situation. For example:

      .      .      .      fld( this.x );           // ESI points at current object.      .      .      .      push( esi );             // Preserve ESI across this method call.      SomeObject.SomeMethod();      pop( esi );      .      .      .      lea( ebx, this.x );      // ESI points at original object here. 

The this keyword provides access to the class variables you declare in the var section of a class. You can also use this to call other class routines associated with the current object, e.g.,

 this.distance( 5.0, 6.0 ); 

To access class constants and static data fields, you generally do not use the this pointer. HLA associates constant and static data fields with the whole class, not a specific object (just like static fields in a class). To access these class members, use the class name in place of the object name. For example, to access the unitdistance constant in the point2d class you could use a statement like the following:

 fld( point2D.UnitDistance ); 

As another example, if you wanted to update the LastDistance field in the point2D class each time you computed a distance, you could rewrite the point2D.distance method as follows:

 method point2D.distance( fromX:real32; fromY:real32 ); nodisplay; begin distance;      fld( this.x );      fld( fromX );                  // Compute (x-fromX)      fsub();      fld( st0 );                       // Duplicate value on TOS.      fmul();                           // Compute square of difference.      fld( this.y );      fld( fromY );                     // Compute (y-fromY)      fsub();      fld( st0 );                       // Compute the square of the difference.      fmul();      fsqrt();      fst( point2D.LastDistance );      // Update shared (STATIC) field. end distance; 

To understand why you use the class name when referring to constants and static objects but you use this to access var objects, check out the next section.

Class procedures are also static objects, so it is possible to call a class procedure by specifying the class name rather than an object name in the procedure invocation; e.g., both of the following are legal:

      Origin.InitLastDistance();      point2D.InitLastDistance(); 

There is, however, a subtle difference between these two class procedure calls. The first call above loads ESI with the address of the origin object prior to actually calling the initlastdistance procedure. The second call, however, is a direct call to the class procedure without referencing an object; therefore, HLA doesn't know what object address to load into the ESI register. In this case, HLA loads NULL (zero) into ESI prior to calling the InitLastDistance procedure. Because you can call class procedures in this manner, it's always a good idea to check the value in ESI within your class procedures to verify that HLA contains a valid object address. Checking the value in ESI is a good way to determine which calling mechanism is in use. Later, this chapter will discuss constructors and object initialization; there you will see a good use for static procedures and calling those procedures directly (rather than through the use of an object).




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