|
Table of Content | Chapter Twelve (Part 6) |
| CHAPTER TWELVE: PROCEDURES: ADVANCED TOPICS (Part 5) |
| 12.4 - Passing Procedures as Parameters |
| 12.4 Passing Procedures as Parameters |
Many programming languages let you pass a procedure or
function name as a parameter. This lets the caller pass along various actions to perform
inside a procedure. The classic example is a plot procedure that graphs some
generic math function passed as a parameter to plot.
Standard Pascal lets you pass procedures and functions by declaring them as follows:
procedure DoCall(procedure x);
begin
x;
end;
The statement DoCall(xyz); calls DoCall
that, in turn, calls procedure xyz.
Passing a procedure or function as a parameter may seem like an easy task - just pass the address of the function or procedure as the following example demonstrates:
procedure PassMe;
begin
Writeln('PassMe was called');
end;
procedure CallPassMe(procedure x);
begin
x;
end;
begin {main}
CallPassMe(PassMe);
end.
The 80x86 code to implement the above could look like the following:
PassMe proc near
print
byte "PassMe was called",cr,lf,0
ret
PassMe endp
CallPassMe proc near
push bp
mov bp, sp
call word ptr [bp+4]
pop bp
ret 2
CallPassMe endp
Main proc near
lea bx, PassMe ;Pass address of PassMe to
push bx ; CallPassMe
call CallPassMe
ExitPgm
Main endp
For an example as simple as the one above, this technique works fine. However, it does not always work properly if PassMe needs to access non-local variables. The following Pascal code demonstrates the problem that could occur:
program main;
procedure dummy;
begin end;
procedure Recurse1(i:integer; procedure x);
procedure Print;
begin
writeln(i);
end;
procedure Recurse2(j:integer; procedure y);
begin
if (j=1) then y
else if (j=5) then Recurse1(j-1, Print)
else Recurse1(j-1, y);
end;
begin {Recurse1}
Recurse2(i, x);
end;
begin {Main}
Recurse1(5,dummy);
end.
This code produces the following call sequence:
Recurse1(5,dummy)-->Recurse2(5,dummy)-->Recurse1(4,Print)-->Recurse2(4,Print)-->Recurse1(3,Print)-->Recurse2(3,Print)-->Recurse1(2,Print)-->Recurse2(2,Print)-->Recurse1(1,Print)-->Recurse2(1,Print)-->
Print
will print the value of Recurse1's
i variable to the standard output. However, there are several activation
records present on the stack that raises the obvious question, "which copy of i
does Print display?" Without giving it much thought, you might conclude
that it should print the value "1" since Recurse2 calls Print
when Recurse1's value for i is one. Note, though, that when Recurse2
passes the address of Print to Recurse1, i's value
is four. Pascal, like most block structured languages, will use the value of i at
the point Recurse2 passes the address of Print to Recurse1.
Hence, the code above should print the value four, not the value one.
This creates a difficult implementation problem. After all,
Print cannot simply access the display to gain access to the global variable i
- the display's entry for Recurse1 points at the latest copy of Recurse1's
activation record, not the entry containing the value four which is what you want.
The most common solution in systems using a display is to
make a local copy of each display whenever calling a procedure or function. When passing a
procedure or function as a parameter, the calling code copies the display along with the
address of the procedure or function. This is why Intel's enter instruction
makes a copy of the display when building the activation record.
If you are passing procedures and functions as parameters, you may want to consider using static links rather than a display. When using a static link you need only pass a single pointer (the static link) along with the routine's address. Of course, it is more work to access non-local variables, but you don't have to copy the display on every call, which is quite expensive.
The following 80x86 code provides the implementation of the above code using static links:
wp textequ <word ptr>
Dummy proc near
ret
Dummy endp
; PrintIt; (Use the name PrintIt to avoid conflict).
;
; stack:
;
; bp+4: static link.
PrintIt proc near
push bp
mov bp, sp
mov bx, [bp+4] ;Get static link
mov ax, ss:[bx-10] ;Get i's value.
puti
pop bp
ret 2
PrintIt endp
; Recurse1(i:integer; procedure x);
;
; stack:
;
; bp+10: i
; bp+8: x's static link
; bp+6: x's address
Recurse1 proc near
push bp
mov bp, sp
push wp [bp+10] ;Push value of i onto stack.
push wp [bp+8] ;Push x's static link.
push wp [bp+6] ;Push x's address.
push bp ;Push Recurse1's static link.
call Recurse1
pop bp
ret 6
Recurse1 endp
; Recurse2(i:integer; procedure y);
;
; stack:
;
; bp+10: j
; bp+8: y's static link.
; bp+6: y's address.
; bp+4: Recurse2's static link.
Recurse2 proc near
push bp
mov bp, sp
cmp wp [bp+10], 1 ;Is j=1?
jne TryJeq5
push [bp+8] ;y's static link.
call wp [bp+6] ;Call y.
jmp R2Done
TryJeq5: cmp wp [bp+10], 5 ;Is j=5?
jne Call1
mov ax, [bp+10]
dec ax
push ax
push [bp+4] ;Push static link to R1.
lea ax, PrintIt ;Push address of print.
push ax
call Recurse1
jmp R2Done
Call1: mov ax, [bp+10]
dec ax
push ax
push [bp+8] ;Pass along existing
push [bp+6] ; address and link.
call Recurse1
R2Done: pop bp
ret 6
Recurse1 endp
main proc
push bp
mov bp, sp
mov ax, 5 ;Push first parameter.
push ax
push bp ;Dummy static link.
lea ax, Dummy ;Push address of dummy.
push ax
call Recurse1
pop bp
ExitPgm
main endp
There are several ways to improve this code. Of course,
this particular program doesn't really need to maintain a display or static list because
only PrintIt accesses non-local variables; however, ignore that fact for the
time being and pretend it does. Since you know that PrintIt only accesses
variables at one particular lex level, and the program only calls PrintIt
indirectly, you can pass a pointer to the appropriate activation record; this is what the
above code does, although it maintains full static links as well. Compilers must always
assume the worst case and often generate inefficient code. If you study your particular
needs, however, you may be able to improve the efficiency of your code by avoiding much of
the overhead of maintaining static lists or copying displays.
Keep in mind that thunks are special cases of functions
that you call indirectly. They suffer from the same problems and drawbacks as procedure
and function parameters with respect to accessing non-local variables. If such routines
access non-local variables (and thunks almost always will) then you must exercise care
when calling such routines. Fortunately, thunks never cause indirect recursion (which is
responsible for the crazy problems in the Recurse1 / Recurse2
example) so you can use the display to access any non-local variables appearing within the
thunk.
|
Table of Content | Chapter Twelve (Part 6) |
Chapter Twelve: Procedures: Advanced
Topics (Part 5)
27 SEP 1996