3.9 Procedure Pointers
The x86 CALL instruction is very similar to the JMP instruction. In particular, it allows the same three basic forms as the JMP instruction: direct calls (to a procedure name), indirect calls through a 32-bit general purpose register, and indirect calls through a double word pointer variable. The CALL instruction allows the following (low-level) syntax supporting these three types of procedure invocations:call Procname; // Direct call to procedure "Procname" (or stmt label). call( Reg32 ); // Indirect call to procedure whose address appears // in the Reg32 general-purpose 32-bit register. call( dwordVar ); // Indirect call to the procedure whose address appears // in the dwordVar double word variable.
HLA treats procedure names like static objects. Therefore, you can compute the address of a procedure by using the address-of ("&") operator along with the procedure's name or by using the LEA instruction. For example, "&Procname" is the address of the very first instruction of the Procname procedure. Therefore, all three of the following code sequences wind up calling the Procname procedure:call Procname; . . . mov( &Procname, eax ); call( eax ); . . . lea( eax, Procname ); call( eax );
Since the address of a procedure fits in a 32-bit object, you can store such an address into a dword variable; in fact, you can initialize a dword variable with the address of a procedure using code like the following:procedure p; begin p; end p; . . . static ptrToP: dword := &p; . . . call( ptrToP ); // Calls the "p" procedure if ptrToP has not changed.
Because the use of procedure pointers occurs frequently in assembly language programs, HLA provides a special syntax for declaring procedure pointer variables and for calling procedures indirectly through such pointer variables. To declare a procedure pointer in an HLA program, you can use a variable declaration like the following:static procPtr: procedure;
Note that this syntax uses the keyword PROCEDURE as a data type. It follows the variable name and a colon in one of the variable declaration sections (STATIC, READONLY, STORAGE, or VAR). This sets aside exactly four bytes of storage for the procPtr variable. To call the procedure whose address is held by procPtr, you can use either of the following two forms:call( procPtr ); // Low-level syntax. procPtr(); // High-level language syntax.
Note that the high level syntax for an indirect procedure call is identical to the high level syntax for a direct procedure call. HLA can figure out whether to use a direct call or an indirect call by the type of the identifier. If you've specified a variable name, HLA assumes it needs to use an indirect call; if you specify a procedure name, HLA uses a direct call.
Like all pointer objects, you should not attempt to indirectly call a procedure through a pointer variable unless you've initialized that variable with the address appropriately. There are two ways to initialize a procedure pointer variable: STATIC and READONLY objects allow an initializer, or you can compute the address of a routine (as a 32-bit value) and store that 32-bit address directly into the procedure pointer at run-time. The following code fragment demonstrates both ways you can initialize a procedure pointer.static ProcPtr: procedure := &p; // Initialize ProcPtr with the address of p. . . . ProcPtr(); // First invocation calls p. mov( &q, ProcPtr ); // Reload ProcPtr with the address of q. . . . ProcPtr(); // This invocation calls the "q" procedure.
Procedure pointer variable declarations also allow the declaration of parameters. To declare a procedure pointer with parameters, you must use a declaration like the following:static p:procedure( i:int32; c:char );
This declaration states that p is a 32-bit pointer that contains the address of a procedure having two parameters. If desired, you could also initialize this variable p with the address of some procedure by using a static initializer, e.g.,static p:procedure( i:int32; c:char ) := &SomeProcedure;
Note that SomeProcedure must be a procedure whose parameter list exactly matches p's parameter list (i.e., two value parameters, the first is an int32 parameter and the second is a char parameter). To indirectly call this procedure, you could use either of the following sequences:push( << Value for i >> ); push( << Value for c >> ); call( p ); -or- p( <<Value for i>>, <<Value for c>> );
The high level language syntax has the same features and restrictions as the high level syntax for a direct procedure call. The only difference is the actual CALL instruction HLA emits at the end of the calling sequence.
Although all of the examples in this section have used STATIC variable declarations, don't get the idea that you can only declare simple procedure pointers in the STATIC or other variable declaration sections. You can declare procedure pointer types in the TYPE section. You can declare procedure pointers as fields of a RECORD. Assuming you create a type name for a procedure pointer in the TYPE section, you can even create arrays of procedure pointers. The following code fragments demonstrate some of the possibilities:type pptr: procedure; prec: record p:pptr; // other fields... endrecord; static p1:pptr; p2:pptr p3:prec; . . . p1(); p2[ebx*4](); p3.p();
One very important thing to keep in mind when using procedure pointers is that HLA does not (and cannot) enforce strict type checking on the pointer values you assign to a procedure pointer variable. In particular, if the parameter lists do not agree between the declarations of the pointer variable and the procedure whose address you assign to the pointer variable, the program will probably crash if you attempt to call the mismatched procedure indirectly through the pointer using the high level syntax. Like the low-level "pure" procedure calls, it is your responsibility to ensure that the proper number and types of parameters are on the stack prior to the call.
3.10 Procedural Parameters
One place where procedure pointers are quite invaluable is in parameter lists. Selecting one of several procedures to call by passing the address of some procedure, selected from a set of procedures, is not an uncommon operation. Therefore, HLA lets you declare procedure pointers as parameters.
There is nothing special about a procedure parameter declaration. It looks exactly like a procedure variable declaration except it appears within a parameter list rather than within a variable declaration section. The following are some typical procedure prototypes that demonstrate how to declare such parameters:procedure p1( procparm: procedure ); forward; procedure p2( procparm: procedure( i:int32 ) ); forward; procedure p3( val procparm: procedure ); forward;
The last example above is identical to the first. It does point out, though, that you generally pass procedural parameters by value. This may seem counter-intuitive since procedure pointers are addresses and you will need to pass an address as the actual parameter; however, a pass by reference procedure parameter means something else entirely. consider the following (legal!) declaration:procedure p4( var procPtr:procedure ); forward;
This declaration tells HLA that you are passing a procedure variable by reference to p4. The address HLA expects must be the address of a procedure pointer variable, not a procedure.
When passing a procedure pointer by value, you may specify either a procedure variable (whose value HLA passes to the actual procedure) or a procedure pointer constant. A procedure pointer constant consists of the address-of operator ("&") immediately followed by a procedure name. Passing procedure constants is probably the most convenient way to pass procedural parameters. For example, the following calls to the Plot routine might plot out the function passed as a parameter from -2¼ to +2¼.Plot( &sineFunc ); Plot( &cosFunc ); Plot( &tanFunc );
Note that you cannot pass a procedure as a parameter by simply specifying the procedure's name. I.e., "Plot( sineFunc );" will not work. Simply specifying the procedure name doesn't work because HLA will attempt to directly call the procedure whose name you specify (remember, a procedure name inside a parameter list invokes instruction composition). However, since you don't specify a parameter list, or at least an empty pair of parentheses, after the parameter/procedure's name, HLA generates a syntax error message. Moral of the story: don't forget to preface procedure parameter constant names with the address-of operator.
3.11 Untyped Reference Parameters
Sometimes you will want to write a procedure to which you pass a generic memory object by reference without regard to the type of that memory object. A classic example is a procedure that zeros out some data structure. Such a procedure might have the following prototype:procedure ZeroMem( var mem:byte; count:uns32 );
This procedure would zero out count bytes starting at the address the first parameter specifies. The problem with this procedure prototype is that HLA will complain if you attempt to pass anything other than a byte object as the first parameter. Of course, you can overcome this problem using type coercion like the following, but if you call this procedure several times with lots of different data types, then the following coercion operator is rather tedious to use:ZeroMem( (type byte MyDataObject), @size( MyDataObject ));
Of course, you can always use hybrid parameter passing or manually push the parameters yourself, but these solutions are even more work than using the type coercion operation. Fortunately, HLA provides a far more convenient solution: untyped reference parameters.
Untyped reference parameters are exactly that - pass by reference parameters on which HLA doesn't bother to compare the type of the actual parameter against the type of the formal parameter. With an untyped reference parameter, the call to ZeroMem above would take the following form:ZeroMem( MyDataObject, @size( MyDataObject ));
MyDataObject could be any type and multiple calls to ZeroMem could pass different typed objects without any objections from HLA.
To declare an untyped reference parameter, you specify the parameter using the normal syntax except that you use the reserved word VAR in place of the parameter's type. This VAR keyword tells HLA that any variable object is legal for that parameter. Note that you must pass untyped reference parameters by reference, so the VAR keyword must precede the parameter's declaration as well. Here's the correct declaration for the ZeroMem procedure using an untyped reference parameter:procedure ZeroMem( var mem:var; count:uns32 );
With this declaration, HLA will compute the address of whatever memory object you pass as an actual parameter to ZeroMem and pass this on the stack.